文章总结(8)—JAVA体系
1. Spring
- Spring中BeanFactory和FactoryBean的区别
-
BeanFactory是一个工厂类,用于管理Bean的一个工厂,在Spring中,所有Bean都是由BeanFactory(也就是IOC容器)来进行管理的。
-
FactoryBean是一个工厂Bean,创建的bean是getObject方法返回的对象。一般用于创建比较复杂的bean。
- 如何自己实现一个IOC容器
IOC容器实现的是服务的发现和注册,其作用和SPI相似。
IOC容器创建.png- 配置文件扫描路径;
- 递归包扫描获取.class文件;
- 通过反射的方式,创建Object对象;
- 属性注入;
- 责任链的方式,执行各个BeanPostProcessor,生成代理对象;
- 将对象放入缓存池singletonObjects中;
Spring 是如何管理事务的,事务管理机制?
Spring的事务管理机制包括:声明式事务和编程式事务。
事务传播行为
事务的传播(propagation [ˌprɒpə'ɡeɪʃn])行为是指:如果在开始当前事务之前,一个事务上下文已经存在,此时我们可以有多个选项指定事务性方法的执行行为。
-
PROPAGATION_REQUIRED
:[adj 必须的]默认传播行为,指的是若当前存在事务,则加入该事务;如果当前没有事务,则创建一个新事务。 -
PROPAGATION_REQUIRES_NEW
:[v 需要新的]需要创建一个新的,若当前有事务,则将当前事务挂起。 -
PROPAGATION_SUPPORTS
:[v 支持]当前存在事务,就在事务中运行;当前不存在事务,则不在事务中运行。 -
PROPAGATION_NOT_SUPPORTED
[v 不被支持]不运行在事务中,当前有事务,则挂掉当前事务。 -
PROPAGATION_NEVER
:[adv 绝不]不运行在事务中,如果当前有事务,则抛出异常。 -
PROPAGATION_MANDARORY
[[ˈmændətəri]强制的]`必须运行在事务中,如果当前方法没有事务,则抛出异常。 -
PROPAGATION_NESTED
[[nestɪd] 嵌套的]当前存在事务,则创建一个事务作为当前事务的嵌套事务运行,如果当前没有事务,则创建一个新的事务。
Exception事务回滚的异常
默认配置下,Spring只有在抛出运行时异常(RuntimeException及其子类)或者Error异常时才会回滚,但是可以配置rollbackFor=Exception.class
将检查时异常进行回滚。
嵌套事务的回滚
【事务嵌套调用】事务A方法调用事务B方法;
【事务B出现异常】事务A捕获事务B的异常(其实事务A不想回滚);
【事务A回滚操作】最终事务A还是回滚了;
如果事务B失败不影响事务A,可以将事务B的传播行为设置为propagation=Propagation.REQUIRES_NEW
。
REQUIRES_NEW
:当前存在事务,则将事务挂起。如果没有事务,则创建新事务。
Spring AOP的理解,各个术语,他们是怎么相互工作的?
SpringAOP联盟(1)—Advisor,Advice,Pointcut,Advised、ProxyConfig
image.png image.pngSpring内部真正创建出代理对象是通过ProxyFactory。
AOP的原理
Spring的AOP实现原理是通过动态代理实现的。而Spring的AOP使用了两种动态代理:分别是JDK动态代理和CGLib动态代理。
Spring默认的策略:如果目标类是接口,则使用JDK动态代理,否则使用Cglib来生成代理。
JDK动态代理:
JDK代理注意涉及到java.lang.reflect
包下的两个类:Proxy和InvocationHandler。InvocationHandler是一个接口,通过实现该接口定义的横切逻辑,并通过反射机制调用目标类代码,动态将横切逻辑和业务逻辑编制在一起,Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。
CGlib可以在运行期扩展Java类与实现Java接口,本质上是通过字节码增强技术在运行期动态生成新的class。
JDK动态代理优缺点
优点:
- JDK动态代理是JDK原生的,不需要任何依赖既可使用;
- 通过反射机制生成代理类速度要比cglib操作字节码生成代理类速度更快;
缺点:
- 使用JDK动态代理,被代理的类必须实现接口;
- JDK动态代理执行代理方法时,需要通过反射机制回调,方法执行效率低;
CGlib动态代理优缺点
优点:
- CGlib代理类,无需实现接口;
- CGlib代理类执行代理方法时效率要高于JDK的动态代理;
缺点:
- CGlib代理类是操作字节码的方式,故生成代理类的速度要比JDK反射的速度要慢。
- CGlib代理类使用的是继承,也就意味着需要被代理的类是一个final类,无法使用CGlib代理。
三级缓存.png
- Spring的三级缓存
三级缓存的作用:
一级缓存:单例池,即常说的spring容器。
二级缓存:若允许循环依赖,将singletonFactory存入二级缓存。目的是延迟加载,真正发生循环依赖时对对象进行代理操作。
三级缓存:防止二级缓存多次处理,并且标示发生过循环依赖,以便后续spring自检。
bean创建的流程.png
- bean创建的流程
关键点:三级缓存中未获取到bean,然后登记Bean正在创建。
三级缓存调用流程.png
- 三级缓存的调用流程
关键点:有一个标识Bean是否在创建。这个标识决定了是去创建bean,还是去earlySingletonObjects(第三级缓存)
中获取bean。
而这个标识是第一次在三级缓存中未获取到对象时,登记Bean正在创建。
二级缓存做的工作.png
- bean什么时候被AOP代理
- 正常流程下生命周期回调方法结束后执行BeanPostProcessor的postProcessAfterInitialization进行AOP代理,生成代理对象。
- 循环依赖情况下,在二级缓存singletonFactories生成对象时,会执行SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference进行AOP代理,目的是将代理对象注入到Bean中,解决循环依赖。
- 为什么会出现自检异常
若循环依赖,提前会进行AOP代理,生成了earlySingletonObject代理对象。并且注入到Bean中。若正常流程下又获取到了新的代理对象,那么Spring便不知道以哪个代理版本为主。便会抛出自检异常。
BeanDefinition的创建.png
- 创建BeanDefinition流程
registry [ˈredʒɪstri] 乱着死捶
bean生命周期回调逻辑.jpeg
- bean生命周期回调的顺序
SpringMVC流程.png
- SpringMVC原理
组件 | 名称 | 作用 |
---|---|---|
DispatcherServlet | 前端控制器 | 接受请求,响应结果 |
HandlerMapping | 处理器映射器 | 根据请求URL查找handler,获取HandlerExecutionChain |
HandlerAdapter | 处理器适配器 | 按照特定规则,去执行对应的handler |
Handler | 处理器(Controller) | 接受用户请求,调用业务处理方法 |
ViewResolver | 视图解析器 | 进行视图解析,将逻辑视图解析为物理视图 |
View | 视图 | 将数据展示给用户的页面,例如JSP、freemarker |
Handler的注册
Spring源码篇(1)—RequestMappingHandlerMapping(Handler的注册)
Spring源码篇(2)—RequestMappingInfo与RequestCondition(Handler—映射)
SpringBoot2.x—定制HandlerMapping映射规则
项目启动后,Spring可以获取到容器中所有Bean对象。而RequestMappingHandlerMapping将处理@RequestMapping标签完成handler的注册(存储到map中)。
- 根据@Controller和@RequestMapping标签筛选Bean。
- 根据@RequestMapping标签来筛选Bean中所有方法,将@RequestMapping标签解析为RequestMappingInfo对象。
- RequestMappingInfo和method对象进行注册。
如何定制HandlerMapping映射规则
Request不仅可以与@RequestMapping属性进行匹配(当然匹配规则是固定的),还可以使用自定义规则进行匹配。
在@RequestMapping类/方法上使用自定义注解。这样在解析含义@RequestMapping注解的类/方法时,用户便可以解析自定义注解的值,从而创建自定义的RequestCondition
对象,选择出优先级最高的HandlerMethod对象。
handlingMapping的原理
根据上述描述,HandlerMapping实际上完成了两件工作:
- 根据请求获取到HandlerMethod,即定位到最优的Controller层的方法进行处理(即HandlerMethod);
- 将HandlerMethod与Interceptor封装为一个HandlerExecutionChain。
Spring生命周期回调原理?
Spring提供了destroy [dɪˈstrɔɪ]
方法用于生命周期的回调。其本质是使用ShutdownHook实现的。
JDK提供了Java.Runtime.addShutdownHook(Thread hook)方法,允许用户注册一个JVM关闭的钩子。这个钩子可以在以下几种场景被调用:
程序正常退出;
使用System.exit();
终端使用Ctrl+C触发的终端;
系统关闭;
使用kill pid命令干掉进程;
一般地发布系统会通过kill命令来停止服务。这个时候服务可以接收到关闭信号并执行钩子程序进行清理工作。
在使用ShutdownHook的时候,我们往往控制不了钩子的执行顺序。java.Runtime.addShutdownHook是对外公开的API接口。在前述场景里面,假若是独立注册钩子,在更复杂的项目里面是不是就没办法保证执行的顺序呢?曾在实际场景中遇到过这样的问题,从kafka队列消费消息,交给内部线程池去处理,我们自定义了线程池的拒绝策略为一直等待(为了保证消息确实处理),然后就会偶尔出现服务无法关闭的问题。原因正是线程池先被关闭,kafka队列却还在消费消息,导致消费线程一直在等待。
Java服务实现优雅的关闭:ShutdownHook/Signal回调
ShutdownHook的缺陷是无法控制钩子的执行顺序。
引申:除了shutdownHook外还有其他的方式实现服务关闭的回调吗?
Java同时提供了signal信号机制,我们的服务也可以接收到关闭信号。
使用Signal机制有以下原因:
ShutdownHook执行顺序无法保障,第三方组件也可能注册,导致业务自定义的退出流程依赖的资源会被提前关闭和清理;
Signal是非公开API,第三方组件基本很少使用,我们可以在内部托管服务关闭的执行顺序;
在完成清理工作后可以执行exit调用,保证资源清理不会影响ShutdownHook的退出清理逻辑;
这里核心的原因还是希望能完全保证服务关闭的顺序,避免出现问题。
引申:服务下线时,JDK线程池或者Spring线程池会shutdown()还是shutdownNow()去关闭线程池?
- JDK原始的线程池在服务下线的时候,不会调用shutdown()或者shutdownNow()相关API来销毁线程池;
- Spring的线程池在服务下线的时候,会调用destroy方法去调用shutdown()或者shutdownNow()方法关闭线程池。默认情况下Spring采用的是shutdownNow()关闭。
mybatis
mybatis的缓存
Mybatis一级缓存:sqlSession级别。
第一次发出查询sql语句,sql查询结果写入sqlSession的一级缓存中,缓存使用的数据结构是一个map。
key:MapperId+offset+limit+sql+入参
value:用户信息;
同一个sqlsession再次发出相同的sql,就从缓存中取出数据。如果两次中间出现commit操作(增改删),本sqlSession的一级缓存区域全部清空。
Mybatis二级缓存:mapper级别
二级缓存范围是Mapper级别(mapper是同一个命名空间),mapper以命名空间为单位创建缓存,结构是map。Mybatis的二级缓存通过CacheExecutor实现,CacheExecutor其实是Executor代理对象。所有的查询操作,在CacheExecutor中都会先匹配缓存是否存在。
image.pngJAVA基础
- 抽象类和接口的区别
- 性质不同
抽象类:是对具体对象的抽象;
接口:是一种行为规范;
- 其他特点
继承与实现:抽象类只能单继承,而接口可以多实现。
属性:抽象类中成员变量可以被不同的修饰符来修饰,而接口中的成员变量默认都是静态变量。
构造方法:抽象类中可以含有构造方法,构造方法的作用就是实例化成员变量。而接口中因为均是静态变量,所以没有构造方法。(注意构造方法作用就是实例化成员变量,new关键字才是创建对象)。
方法:抽象类中可以含有具体方法,而接口中只存在public的抽象方法。
- 抽象类中为什么存在构造方法(构造函数的作用)
构造函数的作用是类成员属性的初始化。
它和new关键字使用时,可以创建对象。
接口中无构造方法的原因在于接口中的属性均是静态常量。不需要进行初始化操作。
抽象类中存在构造函数便是因为抽象类中含有成员变量。便需要存在构造方法为其赋值。
只是抽象类的构造方法不能和new关键字一起使用,但是子类可以通过super()关键字来引用抽象父类的构造方法。
- 说一下你代码设计的思路
- 资源设施层(解耦):Repository层技术可能会发生一下的演变:单库单表->redis+DB查询->分库分表->拆分微服务RPC调用这么演变,技术类型可能经由:jdbc Template->mybatis->jooq等演变。所以基础设施层不能和domain层进行强依赖,传输对象不能和某个存储介质强绑定(例如DB的po对象)。所以我们需要在domain层来定义接口,由Repository层来进行实现。这样的话,就可以实现可插拔的替换底层存储介质的能力。
- 领域层(内聚):比较好的开发模式,是将某个功能全部内聚为一个领域服务。由这个领域服务对外提供所有的能力,这样就实现了某个功能的内聚性。
- 纵向抽取(开闭原则):回到某个领域服务中,此时domain层的代码可以实现纵向抽取。使用模板方法模式抽取大量公用逻辑,子类去实现个性化逻辑。然后通过枚举类来维护各个策略子类,通过入参路由到具体的子类来完成业务逻辑。
- 数据校验(内聚):数据校验分为两类,一类是无状态的校验(例如NPE校验和url)可以将逻辑写在DP中(以实现充血对象);一类是有状态的校验,必须借助其他类完成(例如校验id是否存在数据库中),可以将逻辑写在domain层;
- 防腐层(解耦):调用第三方接口时,为了不强依赖而破坏我们的代码,可以在中间加一层适配层,这一层的作用可以完成参数转换、结果缓存、兜底等逻辑。
记一次生产事故—JIT编译与CPU使用率飙升
为什么匿名内部类访问局部变量要使用final修饰
错误答案:局部变量如果没有用final修饰,他的生命周期和方法的生命周期是一样的,当方法弹栈,这个局部变量也会消失,那么如果局部内部类对象还没有马上消失想用这个局部变量,就没有了,如果用final修饰会在类加载的时候进入常量池,即使方法弹栈,常量池的常量还在,也可以继续使用。
分析:一个变量加上final难道可以延长生命周期吗?那么岂不是加上final便可以造成短暂的内存泄漏?实际上,传入的内部变量是匿名内部类的成员变量(通过构造函数传入)。
变量被回收不是因为方法被执行完毕,而是GC Root是否持有对象的引用,事实上变量作为局部内部类构造参数传入,仍然可达,故final并不是延长变量生命周期。
正确答案:为了保证内部类和外部类变量的一致性,在内部类中对变量的修改也不会影响到外部类的外部方法。
String#intern方法作用
String类型的常量池比较特殊。它的主要使用方法有两种:
- 直接使用双引号声明出来的String对象会直接存储在常量池中。
- 如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中
intern方法可以极大的减少内存使用空间。但是不当是的使用intern方法,就会导致性能急剧下降(内存泄露)。
Jackson2.x中内存泄露的风险点—封装的intern逻辑
String#intern内存泄露的具体案例
Jackson2.9+JDK1.8反序列化的对象为Map<Long,String>,但是key为userId,不收敛。这些userId的字符串都会进入常量池,由于G1的bug,GC时没有被回收,导致内存持续泄露。
key一般是固定的,若使用intern来处理,那么会大大节约反序列化时的空间,但是Map<Long,String>中的key因为是userId,所以会将大量的数据放入常量池中,从而导致内存泄露。
其他
- agent是什么?
虚拟机级别的AOP功能,实现了对字节码增强的功能。
- 如何破坏单例模式
- 反射破坏:反射是通过调用构造方法生成新的对象,所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例。
- Serializable序列化破坏: 就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象。