07-Spring(73)

1. 什么是循环依赖

(常问)

@Service
public class A {
    @Autowired
    private B b;
}

@Service
public class B {
    @Autowired
    private A a;
}

// 或者自己依赖自己
@Service
public class A {
    @Autowired
    private A a;
}
07ec03a627404339828b81ccd2fdfdcb

2. 如何解决循环依赖

关键就是提前暴露未完全创建完毕的 Bean

  • 在 Spring 中,只有同时满足以下两点可解决:
    1. 依赖的 Bean 必须都是单例
    2. 依赖注入的方式,必须不全是构造器注入,且 beanName 字母序在前的不能是构造器注入

1. 为什么必须都是单例

按照理解,如果两个 Bean 都是原型模式的话,那么创建 A1 需要创建一个 B1,创建 B1 的时候要创建一个 A2,创建 A2 又要创建一个 B2,创建 B2 又要创建一个 A3,创建 A3 又要创建一个 B3...

  • 具体做法就是:先创建 A,此时的 A 是不完整的(没有注入 B),用个 map 保存这个不完整的 A,再创建 B ,B 需要 A,所以从那个 map 得到“不完整”的 A,此时的 B 就完整了,然后 A 就可以注入 B,然后 A 就完整了,B 也完整了,且它们是相互依赖的
d2226364e69d428780ebdbeb94d47e64

2. 为什么不能全是构造器注入

无法提前暴露未完全创建完毕的 Bean

  • 在 Spring 中创建 Bean 分三步:
    1. 实例化(createBeanInstance)
    2. 属性注入(populateBean)
    3. 初始化(initializeBean)

如果全是构造器注入,比如A(B b),那表明在 new 的时候,就需要得到 B,此时需要 new B ,但是 B 也是要在构造的时候注入 A ,即B(A a),这时候 B 需要在一个 map 中找到不完整的 A ,发现找不到

3. 一个set,一个构造器注入?

  • 假设 A 是通过 set 注入 B,B 通过构造函数注入 A,此时是成功的
87f283aa0d3a4ca993f9cc7626818841
  • 假设 A 是通过构造器注入 B,B 通过 set 注入 A,此时是失败的
a9e2de83ada94008a40470fa531c61c0

但是 Spring 容器是按照字母序创建 Bean 的,A 的创建永远排在 B 前面

4. 解决循环依赖全流程

明确了 Spring 创建 Bean 的三步骤之后,我们再来看看它为单例搞的三个 map:

  1. 一级缓存,singletonObjects,存储所有已创建完毕的单例 Bean (完整的 Bean)
  2. 二级缓存,earlySingletonObjects,存储所有仅完成实例化,但还未进行属性注入和初始化的 Bean
  3. 三级缓存,singletonFactories,存储能建立这个 Bean 的一个工厂,通过工厂能获取这个 Bean,延迟化 Bean 的生成,工厂生成的 Bean 会塞入二级缓存

这三个 map 是如何配合的呢?

  1. 首先,获取单例 Bean 的时候会通过 BeanName 先去 singletonObjects(一级缓存) 查找完整的 Bean,如果找到则直接返回,否则进行步骤 2
  2. 看对应的 Bean 是否在创建中,如果不在直接返回找不到,如果是,则会去 earlySingletonObjects (二级缓存)查找 Bean,如果找到则返回,否则进行步骤 3
  3. 去 singletonFactories (三级缓存)通过 BeanName 查找到对应的工厂,如果存着工厂则通过工厂创建 Bean ,并且放置到 earlySingletonObjects 中
  4. 如果三个缓存都没找到,则返回 null

如果查询发现 Bean 还未创建,到第二步就直接返回 null,不会继续查二级和三级缓存。返回 null 之后,说明这个 Bean 还未创建,标记这个 Bean 正在创建中,然后再调用 createBean 来创建 Bean,而实际创建是调用方法 doCreateBean

  1. 实例化
  2. 属性注入
  3. 初始化

实例化 Bean 之后,会往 singletonFactories 塞入一个工厂,而调用这个工厂的 getObject 方法,就能得到这个 Bean

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
  • 不管循环依赖是否发生,往 singletonFactories 塞这个工厂,进行提前暴露
  • 然后就开始执行属性注入,这个时候 A 发现需要注入 B,所以去 getBean(B),此时又会走一遍上面描述的逻辑,到了 B 的属性注入这一步
  • 此时 B 调用 getBean(A),这时候一级缓存里面找不到,但是发现 A 正在创建中的,于是去二级缓存找,发现没找到,于是去三级缓存找,然后找到了
  • 并且通过上面提前在三级缓存里暴露的工厂得到 A,然后将这个工厂从三级缓存里删除,并将 A 加入到二级缓存中
  • 结果就是 B 属性注入成功。B 调用 initializeBean 初始化,最终返回,此时 B 已经被加到了一级缓存里
  • 回到了 A 的属性注入,此时注入了 B,接着执行初始化,最后 A 也会被加到一级缓存里,且从二级缓存中删除 A

重点就是在对象实例化之后,都会在三级缓存里加入一个工厂,提前对外暴露还未完整的 Bean,这样如果被循环依赖了,对方就可以利用这个工厂得到一个不完整的 Bean,破坏了循环的条件

3. 循环依赖三级缓存?

仅仅只是为了破解循环依赖,二级缓存够了,压根就不必要三级

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
            exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;
}




 




为什么要搞个三级缓存,且里面存的是创建 Bean 的工厂呢?

  • 如果 false,返回就是参数传进来的 bean,没任何变化
  • 如果 true,说明有 InstantiationAwareBeanPostProcessors ,且循环的 smartInstantiationAware 类型,如有这个 BeanPostProcessor 说明 Bean 需要被 aop 代理

跟三级缓存有什么关系,可以在要放到二级缓存时,判断这个 Bean 是否需要代理,直接放代理的对象不就完事儿了?

  • 正常代理对象的生成是基于后置处理器,是在被代理的对象初始化后期调用生成的所以如果你提早代理了其实是违背了 Bean 定义的生命周期
  • 所以 Spring 先在一个三级缓存放置一个工厂,如果产生循环依赖,那么就调用这个工厂提早得到代理对象,如果没产生依赖,这个工厂根本不会被调用,所以 Bean 的生命周期就是对的

4. Spring重要模块

Spring 最核心就是 IOC 容器,这个容器根据配置文件创建了各种 Bean 且编织它们之间的依赖关系,管理这些 Bean 的生命周期

最核心模块:

  • spring-core:核心类库,包括资源访问等一些工具类,别的模块都会依赖此模块
  • spring-beans:对 Bean 的支持,包括控制反转和依赖注入,核心工厂 BeanFacotry 就在这个模块
  • spring-context:Spring 的上下文,支持数据验证、国际化、事件传播等支持,ApplicationContext 就在这里,可以理解为运行时的 Spring 容器

基于上面的这些核心模块,Spring 也提供了很多重要的支持:

  • spring-aop:AOP 的支持
  • spring-jdbc:jdbc 的支持
  • spring-orm:orm 的支持
  • spring-webmvc:mvc 的支持

5. 什么是IOC

IOC(Inversion of Control),控制反转

首先要明确 IOC 是一种思想,而不是一个具体的技术,其次 IOC 这个思想也不是 Spring 创造的

  • 控制控制对象的创建。IOC 容器根据配置文件来创建对象,在对象的生命周期内,在不同时期根据不同配置进行对象的创建和改造
  • 反转:关于创建对象且注入依赖对象的这个动作。本来这个动作是由程序员在代码里面指定的。eg:对象 A 依赖对象 B,在创建对象 A 代码里,需要写好如何创建对象 B,这样才能构造出一个完整的 A
    • 而反转之后,这个动作就由 IOC 容器触发,IOC 容器在创建对象 A 的时候,发现依赖对象 B ,根据配置文件,它会创建 B,并将对象 B 注入 A 中

6. IOC好处

松耦合:对象的创建都由 IOC 容器来控制之后,对象之间就不会有很明确的依赖关系

  • 例如,对象 A 需要依赖一个实现 B,但是对象都由 IOC 控制之后,不需要明确地在对象 A 的代码里写死依赖的实现 B,只需要写明依赖一个接口
  • 然后,可以在配置文件里定义 A 依赖的具体的实现 B,根据配置文件,在创建 A 的时候,IOC 容器就知晓 A 依赖的 B,这时候注入这个依赖即可
  • 如果之后有新的实现需要替换,那 A 的代码不需要任何改动,只需要将配置文件 A 依赖 B 改成 B1,这样重启之后,IOC 容器会为 A 注入 B1
  • 这样就使得类 A 和类 B 解耦,very nice!

代理对象:创建对象由 IOC 全权把控,就能很方便的让 IOC 基于扩展点来“加工”对象。eg:要代理一个对象,IOC 在对象创建完毕,直接判断下这个对象是否需要代理,如果要代理,则直接包装返回代理对象

7. 什么是DI

DI(Dependency Injection,依赖注入)

  • IOC 的概念一致,只是从不同角度来描述罢了
  • 大致理解为容器在运行的时候,可以找到被依赖的对象,然后将其注入,通过这样的方式,使得各对象之间的关系可由运行期决定,而不用在编码时明确

8. 什么是Bean

可以认为,由 Spring 创建的、用于依赖注入的对象,就是 Bean

9. BeanFactory

  • BeanFactory 本身只是一个接口
  • BeanFactory 其实就是 IOC 的底层容器
  • 不过一般所述的 BeanFactory 指的是它实现类。eg:DefaultListableBeanFactory

10. FactoryBean

  • 其实是一个接口,并且有以下几个方法
public interface FactoryBean<T> {
    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}

如果一个对象实现了这接口,那它就成为一种特殊的 Bean,注册到 IOC 容器之后

  • getBean("A") AFactoryBean#getObject
  • getBean("&A") 得到 AFactoryBean

应用场景:假设你依赖一个第三方的类 A,而我们又不能修改第三方的类,并且这个对象创建比较复杂,那么你就可以创建一个 bean 来封装它

11. ObjectFactory

从命名角度来看,它是一个工厂

  • Spring 的循环依赖(后面会细说)里就用到它了,三级缓存的 map 里面存储的就是 ObjectFactory,用于延迟代理对象的创建
  • 其实就是封装对象的创建过程,像三级缓存里的 ObjectFactory 逻辑就是判断这个 Bean 是否有代理,如果有则返回代理对象,没有则返回原来传入的对象
@FunctionalInterface
public interface ObjectFactory<T> {

	/**
	 * Return an instance (possibly shared or independent)
	 * of the object managed by this factory.
	 * @return the resulting instance
	 * @throws BeansException in case of creation errors
	 */
	T getObject() throws BeansException;

}
 











12. ApplicationContext

开发基本上都是基于 ApplicationContext 操作的,而不是 BeanFactory,它也继承了 BeanFactory 的实现类,也属于一个 BeanFactory ,但是它内部还包装了一个 BeanFactory 的实现,这属于组合

  • 关于 Spring Beans 的一些操作都是委托内部的 BeanFactory 来操作的
bb1246219b5948e581c4b2874cb9d8f3

还扩展了很多其他功能:

  • AOP
  • 国际化
  • 资源管理
  • 事件
  • 注解
  • ...

13. Bean有几种作用域

  • singleton:默认是单例,含义不用解释了吧,一个 IOC 容器内部仅此一个
  • prototype:原型,多实例
  • request:每个请求都会新建一个属于自己的 Bean 实例,这种作用域仅存在 Spring Web 应用中
  • session:一个 http session 中有一个 bean 的实例,这种作用域仅存在 Spring Web 应用中
  • application:整个 ServletContext 生命周期里,只有一个 bean,这种作用域仅存在 Spring Web 应用中
  • websocket:一个 WebSocket 生命周期内一个 bean 实例,这种作用域仅存在 Spring Web 应用中

14. Spring几种注入方式

  • 构造器注入Spring 倡导构造函数注入,因为构造器注入返回给客户端使用的时候一定是完整的
  • setter注入:可选的注入方式,好处是在有变更的情况下,可以重新注入
  • 字段注入:平日用 @Autowired 标记字段
  • 方法注入:平日用 @Autowired 标记方法
  • 接口回调注入:就是实现 Spring 定义的一些内建接口,eg:BeanFactoryAware,会进行 BeanFactory 的注入

  • 其实官网上关于注入就写了构造器和setter
  • 像字段注入其实官方是不推荐的使用的,因为依赖注解,然后没有控制注入顺序且无法注入静态字段
  • 开发中字段注入却用的最多

15. AOP

AOP(Aspect Oriented Programming)面向切面编程

  • 将一些通用的逻辑集中实现,然后通过 AOP 进行逻辑的切入,减少了零散的碎片化代码,提高了系统的可维护性
    • 动态代理:即在运行时将切面的逻辑进去,按照上面的逻辑就是你实现 A 类,然后定义要代理的切入点和切面的实现,程序会自动在运行时生成类似上面的代理类
    • 静态代理在编译时或者类加载时进行切面的织入,典型的 AspectJ 就是静态代理

16. AOP默认的动态代理

Spring AOP 的动态代理实现分别是:

  • JDK 动态代理(Spring AOP 默认用的是 JDK 动态代理)
  • CGLIB(SpringBoot 2.x 版本已经默认改成了 CGLIB。设置 spring.aop.proxy-target-class=false 切换为 JDK 动态代理)

那为什么要改成默认用 CGLIB?

  • JDK 动态代理要求接口,所以没有接口的话会有报错,很令人讨厌,并且让 CGLIB 作为默认也没什么副作用,特别是 CGLIB 已经被重新打包为 Spring 的一部分了,所以就默认 CGLIB

1. 区别

  • JDK 动态代理是基于接口的,所以要求代理类一定是有定义接口的
  • CGLIB 基于 ASM 字节码生成工具,它是通过继承的方式来实现代理类,所以要注意 final 方法

2. 性能

  • jdk6 下,在运行次数较少的情况下,jdk 与 cglib 差距不明显,甚至更快一些;而当调用次数增加之后, cglib 表现稍微更快一些
  • jdk7 下,情况发生了逆转!运行次数较少(1,000,000),jdk 比 cglib 快了差不多30%;而当调用次数增加(50,000,000),jdk 比 cglib 快了接近1倍
  • jdk8 表现和 jdk7 基本一致

17. 拦截链的实现

  • Spring AOP 提供了多种拦截点,方便在目标方法执行前、后、抛错等地方进行一些逻辑的切入
  • 实际上这些注解都对应着不同的 interceptor 实现,然后 Spring 会利用一个集合把所有类型的 interceptor 组合起来,在代码里用 @Before@After@AfterReturning@AfterThrowing 这几个注解
  • 于是拦截链集合里就有了这些 interceptor(多了一个 expose...),由 Spring 扫描到注解自动加进来的
d818910b9f434e39b027624e2ae31e97
  • 然后通过一个对象 CglibMethodInvocation 将这个集合封装起来,紧接着调用这个对象的 proceed(),可看到集合 chain 被传入了
dcf4d869197b4228af20340932a8e51e

开始递归套娃CglibMethodInvocation#proceed 核心调用逻辑:

  • currentInterceptorIndex 变量,通过递归,每次新增这索引值,来得到下一个 interceptor ,并且每次都传入当前对象并调用 interceptor#invoke ,这样就实现了拦截链的调用
79db1180717c4f6d866b7f775e478e5f
  • MethodBeforeAdviceInterceptor 前置拦截举例
    • invoke() 的实现是先执行切入的前置逻辑,然后再继续调用 CglibMethodInvocation#proceed(也就是mi.proceed),进行下一个 interceptor 的调用
dee57449a7ed432881e99a541d811a80

1. 总结

  1. Spring 根据 @Before, @After, @AfterReturning, @AfterThrowing 这些注解,往集合里面加入对应的 Spring 提供的 MethodInterceptor 实现
  2. 然后通过一个对象 CglibMethodInvocation 将这个集合封装起来,紧接着调用这个对象的 proceed(),具体是利用 currentInterceptorIndex 下标,利用递归顺序地执行集合里面的 MethodInterceptor ,这样就完成了拦截链的调用

第一个索引位置的 ExposeInvocationInterceptor

  • 这个 Interceptor 作为第一个被调用,实际上就是将创建的 CglibMethodInvocation 这个对象存入 threadlocal 中,方便后面 Interceptor 调用时能得到这个对象,进行一些调用
e2bfa747fdaa4ef7bae2c6d4806d668b

18. AOP和AspectJ区别

  • Spring AOP 是动态代理
    • 运行时织入。只支持方法级别的织入
  • AspectJ 是静态代理
    • 编译时织入。编译时就准备完毕,在调用时没有额外的织入开销,性能更好些。支持字段、方法、构造函数等,它更加强大,当然也更加复杂

19. Bean的生命周期

7de7964ec4de4391902db160d6c29f79
46bbaa2dfeab4bc4a9187304a7041c6f

文字描述:

  • 实例化Bean
  • 根据属性,注入需要的 Bean
  • 如果 Bean 实现了 BeanNameAware 等 aware 接口,则执行 aware 注入
  • 如果有 BeanPostProcessor,则执行BeanPostProcessor#postProcessBeforeInitialization 方法
  • 如果 Bean 是 InitializingBean,则执行 afterPropertiesSet 方法
  • 如果有 initMethod ,则执行
  • 如果有 BeanPostProcessor,执行BeanPostProcessor#postProcessAfterInitialization 方法
  • 使用 Bean
  • 如果 Bean 是 DisposableBean,则执行 destroy 方法
  • 如果有 destroy 方法,则执行

20. MVC的理解

  • Spring MVC 是基于 Servlet API 构建的
  • 核心是 DispatcherServlet,即一个前端控制器
  • 几个重要的组件:处理器映射、控制器、视图解析器等
    • 由这几个组件让我们与 Servlet 解耦,不需要写一个个 Servlet,基于 Spring 的管理就可以很好的实现 web 应用

21. MVC工作原理

  • 当一个请求过来,由 DispatcherServlet 接待,它会根据处理器映射(HandlerMapping)找到对应的 HandlerExecutionChain(这里面包含了很多定义的 HandlerInterceptor,拦截器)
  • 然后通过 HandlerAdapter 适配器的适配(适配器模式了解一下)后,执行 handler,即通过 controller 的调用,返回 ModelAndView
  • 然后 DispatcherServlet 解析得到 ViewName,将其传给 ViewResolver 视图解析器,解析后获得 View 视图
  • 然后 DispatcherServlet 将 model 数据填充到 view ,得到最终的 Response 返回给用户
  • 常用的视图有 jspfreemarkervelocity

22. MVC父子容器是什么

官网图:

19da2bc18457461192e791a7d6016ddd
  • Spring 容器在启动时,不会有 SpringMVC 这个概念,只会扫描文件然后创建一个 context,此时就是父容器
  • 然后发现 web 服务需要生成 DispatcherServlet ,此时就会调用 DispatcherServlet#init,这个方法会生成一个新的 context,并把之前的 context 置为 Parent

  • 有父子之分,指责就更加清晰,子容器就负责 web 部分,父容器则是通用的一些 bean
  • 如果有些人没把 controller 扫包的配置写在 spring-servlet.xml,而写到了 service.xml 里,那就会把 controller 添加到父容器里,这样子容器里面就找不到,请求就 404 了
  • 子容器可以用父容器的 Bean,父容器不能用子容器的 Bean

23. Spring哪些设计模式

  • 工厂模式:BeanFactory
  • 模板方法:JdbcTemplateRestTemplate
  • 代理模式:AOP
  • 单例
  • 责任链模式:拦截器
  • 观察者模式:Spring 里的监听器
  • 适配器模式:SpringMVC 提到的 HandlerAdapter

24. Spring事务隔离级别

  1. DEFAULT:就是使用数据库定义的隔离级别
  2. 读未提交(READ UNCOMMITED)简称 RU:最宽松的限制,即一个事务的修改还未提交,另一个事务就能看到修改的结果,会产生脏读现象
  3. 读已提交(READ COMMITED)简称 RC:即一个事务只能读到另一个事务已经提交的修改,所以在一个事务里面的多次查询可能会得到不同的结果,因为第一次查询的时候,另一个事务还未提交,所以看不到修改的结果,第二次查询的时候,另一个事务提交了,因此读到了修改后的结果,所以两次查询结果不一致,称之为不可重复读
  4. 可重复读(REPEATABLE READ)简称 RR:它比 RC 更严格,即一个事务开始的时候读不到,那之后也读不到,也就是一个事务内的多次读结果是一致的,但是有幻读情况,即第一次读拿到了四行数据,第二次读拿到了五行数据,因为有新插入的行,不过 InnoDB 利用 MVCC 解决了大部分幻读的情况,利用 update 当前读再 select 的幻读无法解决
  5. 串行化(SERIALIZABLE):最严格的模式,即这个隔离级别的读写访问会把需要遍历到的记录上锁,这样另一个事务要访问对应的记录时候就被阻塞了,需要等待上一个事务提交之后,才能继续执行,所以叫串行
    • 在 InnoDB 中,非自动提交时,会隐式地将所有普通的 SELECT 语句转换为 SELECT... LOCK IN SHARE MODE 来满足这个隔离级别

25. Spring事务传播行为

  • PROPAGATION_REQUIRED(默认): 如果当前存在事务,则用当前事务,如果没有事务则新起一个事务
  • PROPAGATION_SUPPORTS: 支持当前事务,如果不存在,则以非事务方式执行
  • PROPAGATION_MANDATORY: 支持当前事务,如果不存在,则抛出异常
  • PROPAGATION_REQUIRES_NEW: 创建一个新事务,如果存在当前事务,则挂起当前事务
  • PROPAGATION_NOT_SUPPORTED: 不支持当前事务,始终以非事务方式执行
  • PROPAGATION_NEVER: 不支持当前事务,如果当前存在事务,则抛出异常
  • PROPAGATION_NESTED: 如果当前事务存在,则在嵌套事务中执行,内层事务依赖外层事务,如果外层失败,则会回滚内层,内层失败不影响外层

26. Spring事务传播行为作用

(控制事务的边界)

  • PROPAGATION_NESTED 举例:如果外层事务失败,则会回滚内层事务,内层事务失败不影响外层事务
    • 可以发现,外层事务失败会影响到内层事务,这说明从事务角度来看,外层到内层之间是没有边界的,因为外层会影响到内层的事务
    • 而内层失败则不影响外层,说明内层往外层之间事务是有边界的,使得影响无法传播出去
  • PROPAGATION_REQUIRED(默认):如果当前存在事务,则用当前事务,如果没有事务则新起一个事务
    • 说明这个配置下,多个方法之间不想要有边界,它们想在一个事务中,这样但凡有一个方法出错都会回滚,就能保证它们是“一体”的,它们想要相互影响

因此,每种事务传播行为都有其独特的使用场景和作用,能够灵活控制事务的边界,确保事务的一致性和隔离性

27. 请求如何到controller

  • 当请求打到 MVC 应用程序时,请求首先会到达一个叫做 DispatcherServlet 的核心控制器,它是所有请求的入口点
  • 然后请求会利用 HandlerMapping 找到具体的 handle,实际就是通过请求的 URL 查找对应的映射配置,得到对应的 controller
  • 不过再具体一些,HandlerMapping 找到的其实是 HandlerExecutionChain ,它包含了拦截器和 handler,请求需要先经过拦截器的处理才会到最后的 handler

28. Spring优点

  • 轻量级和非侵入性:不需要引入大量的依赖和配置
  • 面向切面编程(AOP):Spring 提供了强大的面向切面编程的支持,允许用户定义横切关注点,并将其与核心业务逻辑分离
  • 依赖注入(DI)和控制反转(IoC)容器:Spring 的核心是 IoC 容器,它实现了依赖注入模式,通过配置文件或注解来管理对象之间的依赖关系
  • 简化 RESTful 服务开发:Spring 框架提供了 Spring MVC 模块,支持 RESTful 风格的服务开发
  • 模块化和可扩展性:Spring 框架是模块化的,可以按需引入所需的模块,避免了不必要的复杂性和依赖
  • 简化 JDBC 操作:Spring 的 JDBC 模块提供了简化数据库操作的方式,通过 JdbcTemplate 和命名参数 JdbcOperations 等类,简化了 JDBC 的使用,减少了样板代码的编写

29. AOP相关术语

  1. 切面(Aspect):其实就是定义了一个 java 类,里面包含了通知(advice)和切点(pointcut),定义了在何处以及何时执行通知,将切面的一些东西模块化了
  2. 通知(Advice)
    • 前置通知(Before advice):在目标方法执行前执行
    • 后置通知(After returning advice):在目标方法成功执行后执行
    • 后置异常通知(After throwing advice):在目标方法抛出异常后执行
    • 后置最终通知(After (finally) advice):无论目标方法如何结束(正常返回或抛出异常),都会执行
    • 环绕通知(Around advice):在目标方法执行前后都执行,并且可以控制目标方法的执行过程,环绕通知可以用作日志打印或者权限校验
  3. 切点(Pointcut):切点是一个表达式,用于定义在哪些连接点上执行通知,简单理解就是通过这个表达式可以找到想要织入的哪些方法
  4. 连接点(Join point):连接点是程序执行过程中可以应用切面的点,例如方法的调用、方法的执行、异常的抛出等,可以拿到切入方法名等诸多属性

30. Spring通知类型

  1. 前置通知(Before advice):在目标方法执行之前执行的通知。它不会影响目标方法的执行流程,但可以在方法执行前执行一些逻辑,eg:验证参数
  2. 后置通知(After returning advice):在目标方法成功执行并返回结果后执行的通知。它可以访问目标方法的返回值,但无法修改返回值。通常用于记录日志或清理资源等操作
  3. 后置异常通知(After throwing advice):在目标方法抛出异常后执行的通知。它可以访问目标方法抛出的异常,并且可以根据异常类型进行相应的处理,eg:记录异常信息或执行异常处理逻辑
  4. 后置最终通知(After (finally) advice):无论目标方法如何结束(正常返回或抛出异常),都会执行的通知。它类似于Java中的finally块,在方法结束时执行一些清理工作,eg:释放资源或关闭连接
  5. 环绕通知(Around advice):环绕通知是最强大的一种通知类型,它可以在目标方法执行前后都执行,并且可以控制目标方法的执行过程。它需要负责调用目标方法,并且可以决定是否继续执行目标方法以及是否返回自定义的返回值

1. 不同版本执行顺序

Spring4版本

  1. 正常情况
    1. 环绕之前通知
    2. 前置通知Before
    3. 被增强的方法
    4. 环绕之后通知
    5. After最终通知
    6. AfterReturning 后置通知
  2. 异常情况
    1. 环绕之前通知
    2. 前置通知Before
    3. 被增强的方法
    4. After最终通知
    5. AfterThrowing 异常通知

Spring5版本

  1. 正常情况
    1. 环绕之前通知
    2. 前置通知Before
    3. 被增强的方法
    4. 环绕之后通知
    5. AfterReturning后置通知
    6. After最终通知
  2. 异常情况
    1. 环绕之前通知
    2. 前置通知Before
    3. 被增强的方法
    4. AfterThrowing异常通知
    5. After最终通知

31. IOC容器初始化过程

  1. 加载配置文件或配置类。IoC 容器首先需要加载应用程序的配置信息,这些配置信息可以是 XML 配置文件、Java 配置类或注解配置等方式
  2. 创建和配置 BeanFactory 或 ApplicationContext
  3. 加载 Bean 定义。通过 BeanDefinitionReader 读取和解析 Bean 定义,得到 BeanDefinition 注册到 BeanDefinitionRegistry 中
  4. 创建 Bean 实例。通过构造方法或者工厂方法实例化 Bean
  5. 处理 Bean 的依赖注入。对 Bean 的属性完成注入,这个过程会根据配置信息中的依赖关系,对 Bean 进行依赖注入
  6. BeanPostProcessor 处理。这些处理器会在 Bean 初始化生命周期中加入定义的处理逻辑,postProcessBeforeInitialization 和 postProcessAfterInitialization 分别在 Bean 初始化前后被调用
  7. 调用初始化方法。eg:实现 InitializingBean 的 bean 会被调用 afterPropertiesSet 方法

32. Bean注入容器方式

  1. XML配置方式
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
               http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <bean id="myService" class="com.example.MyService"/>
    </beans>
    
  2. 基于注解方式
    • 使用 @Component, @Service, @Controller, @Repository 注解在类或字段上进行依赖注入
    @Component
    public class MyComponent {
        // 业务逻辑
    }
    
    • 也可以通过 @Configuration + @Bean 注解将Bean注册到Spring容器
    @Configuration
    public class AppConfig {
    
        @Bean
        public ExampleBean exampleBean() {
            return new ExampleBean(dependencyBean());
        }
    
        @Bean
        public DependencyBean dependencyBean() {
            return new DependencyBean();
        }
    }
    
  3. 自动注入方式
    • 利用 @Autowired
    @Component
    public class MyComponent {
        @Autowired
        private MyService myService;
    }
    
    • 同理也可以使用 @Inject, @Resource

33. Spring自动装配方式

所谓的自动装配指的是 Spring 可以根据一些特定的规则(注解或配置),自动在各个组件之间建立关联,完成依赖注入。有以下几类:

  • byType:根据类型进行自动装配,这也是默认的自动装配规则
  • byName:通过名称匹配来进行自动装配,Spring 会在容器中查找与属性名称相同的 Bean 进行装配
  • 构造器自动装配:Spring 会自动选择匹配参数的构造函数来实现依赖注入

34. @Qualifier作用

  • 通常与 @Autowired 注解一起使用,用于告诉 Spring 容器应该注入哪个 Bean
  • 作用:如果有多个 Bean 类型一致,仅通过 @Autowired Spring 就不知道该注入哪个了,因此可以加个 @Qualifier 注解指定具体的 Bean 名称,消除歧义

35. @Bean,@Component区别

1. @Bean

  • 方法级别注解:用于在配置类中声明方法,这些方法将返回一个对象,该对象将由 Spring 容器管理
  • 手动定义Bean:可以手动定义 Bean 的创建和初始化过程,可以指定 Bean 的作用域、初始化方法、销毁方法等
  • 适用范围:主要用于 Java 配置(JavaConfig)方式,与 XML 配置相对应,通过 Java 代码的方式来配置 Spring 应用上下文
@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }
}

2. @Component

  • 类级别注解:用于标识一个类为 Spring 的组件,告诉 Spring 要将该类实例化为一个 Bean,并交给 Spring 容器管理
  • 自动扫描:在基于组件扫描的方式下,Spring 会自动扫描带有 @Component 及其派生注解的类,并将其注册为 Bean
  • 适用范围:适用于基于注解的配置方式(AnnotationConfig),通过标记类来声明 Bean
@Component
public class MyComponent {
}

36. bean注解区别

@Component、@Controller、@Repository、@Service 的区别?

1. @Component

  • 通用性注解:标识一个类为 Spring 的组件,告诉 Spring 要将该类实例化为一个 Bean,并交给 Spring 容器管理
  • 类级别注解: 通常用于标识任何普通的 Spring 管理的 Bean

2. @Controller

  • Web层注解:标识一个类为控制器(Controller),通常用于处理用户的 HTTP 请求,并返回相应的视图或数据
  • 类级别注解:用于标识控制器类,告诉 Spring 该类负责处理 HTTP 请求

3. @Repository

  • 持久层注解: 标识持久层组件的注解,通常用于数据访问层(DAO)的实现类
  • 类级别注解:用于标识数据访问层(DAO)的实现类,告诉 Spring 该类负责数据库操作,通常与持久化技术(如JPA、Hibernate)结合使用

4. @Service

  • 服务层注解: 标识服务层组件的注解,通常用于业务逻辑的实现类
  • 类级别注解:用于标识服务层(Service)的实现类,告诉 Spring 该类负责业务逻辑处理

37. 事务失效

五种常见导致失效:

  1. rollbackFor 没设置对:比如默认没有任何设置,则方法内抛出 IOException 则不会回滚,需要配置 @Transactional(rollbackFor = Exception.class)
  2. 异常被捕获了:比如 catch 了异常打了 log,这样事务无法正常获取到错误,因此不会回滚
  3. 同一个类中方法调用:因为事务是基于动态代理实现的,同类的方法调用不会走代理方法,因此事务自然就失效了
  4. @Transactional:应用在非 public 修饰的方法上,Spring 事务管理器判断非公共方法则不应用事务
  5. propagation 传播机制配置错误
    • 因为配置了 Propagation.REQUIRES_NEW,是新起了一个事务,即 addAddress 的事务和 addUserAndAddress 其实不是一个事务,因此两个事务之间当然就无法保证数据的一致性了
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void addUserAndAddress(User user, Address address) throws Exception {
    userMapper.save(user);
    addAddress(address);
}

@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void addAddress(Address address) {
    addressMapper.save(address);
}

38. Spring启动过程

Spring 框架的启动过程涉及到各种模块的初始化、依赖注入、AOP(面向切面编程)配置等步骤。下面是 Spring 容器启动的一般过程:

  1. 加载配置文件
    • 首先会读取配置文件(eg:XML 配置文件、Java Config 类等),包括配置数据库连接、事务管理、AOP 配置等
  2. 实例化容器
    • 根据配置文件中的信息创建容器 ApplicationContext,在容器启动阶段实例化 BeanFactory,并加载容器中的 BeanDefinitions
  3. 解析 BeanDefinitions
    • 解析配置文件中的 BeanDefinitions,即声明的 Bean 元数据,包括 Bean 的作用域、依赖关系等信息
  4. 实例化 Bean
    • 根据 BeanDefinitions 实例化 Bean 对象,将其放入容器管理
  5. 注入依赖
    • 将 Bean 之间的依赖关系进行注入,包括构造函数注入、属性注入等
  6. 处理 Bean 生命周期初始化方法
    • Spring 调用 Bean 初始化方法(如果有定义的话),对 Bean 进行初始化
    • Bean 实现了 InitializingBean 接口,Spring 会调用其 afterPropertiesSet()
  7. 处理 BeanPostProcessors
    • 容器定义了很多 BeanPostProcessor,处理其中的自定义逻辑。eg:postProcessBeforeInitialization 会在 Bean 初始化前调用, postProcessAfterInitialization 则在之后调用
  8. 代理切面处理
    • 根据配置注册 AOP 切面,生成代理对象,将切面织入到目标对象中
  9. 发布事件
    • 可能会在启动过程中发布一些事件。eg:容器启动事件
  10. 完成启动
    • 当所有 Bean 初始化完毕、依赖注入完成、AOP 配置生效等都准备就绪时,Spring 容器启动完成

39. 单例Bean并发安全问题

Spring 的单例 Bean 默认是非线程安全的,但只要我们避免在多个 bean 之间共享一些数据,就不会发生并发问题

原因:

  • 单例 Bean 的生命周期:Spring 容器在初始化时会创建并管理单例 Bean,这些 Bean 在整个应用程序生命周期内只会被创建一次,并且被多个线程共享使用
  • 多线程访问:如果单例 Bean 中包含共享的可变状态(实例变量),多个线程同时访问并修改这些共享状态时,可能会导致并发安全问题。eg:数据不一致、脏读、死锁等

40. Bean如何保证并发安全

大致分三种方式:

  1. 无状态Bean:如果单例 Bean 不包含可变状态,或仅包含只读状态,通常不会有并发安全问题。eg:所有方法都是无状态的纯函数,或使用的是本地变量
  2. 线程安全设计:确保单例 Bean 中的方法是线程安全的。可以通过同步方法、使用线程安全的数据结构(ConcurrentHashMap)、或使用 java.util.concurrent 包中的其他并发工具类
  3. @Scope("prototype"):对于需要状态的 Bean,可以将其作用域设置为原型(prototype),这样每次请求该 Bean 时,Spring 容器都会返回一个新的实例

41. @Primary作用

主要作用:解决 Bean 注入时的歧义问题,当一个接口或父类有多个实现时,Spring 无法确定该注入哪个具体的 Bean,此时可以使用 @Primary 来指明首选的 Bean

  1. 多实现类
  2. 注入优先级
public interface MyService {
    void performAction();
}

// 使用 @ Primary 注解后,此时会优先注入 MyServiceImpl1
@Primary
@Component
public class MyServiceImpl1 implements MyService {
    @Override
    public void performAction() {
        System.out.println("面试鸭表演1");
    }
}

@Component
public class MyServiceImpl2 implements MyService {
    @Override
    public void performAction() {
        System.out.println("面试鸭表演2");
    }
}

@Component
public class MyComponent {
    private final MyService myService;

    @Autowired
    public MyComponent(MyService myService) {
        this.myService = myService;
    }

    public void doSomething() {
        myService.performAction();
    }
}





 

 







 



















42. @Value作用

在 Spring 框架中,@Value 注解用于注入外部化的配置值到 Spring 管理的 Bean 中。通过 @Value 注解,可以将属性文件、环境变量、系统属性等外部资源中的值注入到 Spring Bean 的字段、方法参数或构造函数参数中

  • 配置文件注入:将属性文件中的值注入到 Bean 中
  • 系统属性和环境变量:将系统属性或环境变量的值注入到 Bean 中
  • 默认值设置:在属性不可用时,提供默认值
app.name=MyApp
app.version=1.0.0
@Component
public class AppConfig {

    @Value("${app.name}")
    private String appName;

    @Value("${app.version}")
    private String appVersion;

    public String getAppName() {
        return appName;
    }

    public String getAppVersion() {
        return appVersion;
    }
}



 


 










43. @Profile作用

在 Spring 框架中,@Profile 注解用于定义一组 Bean 的配置文件所属的环境。eg:dev 表示开发环境,prod 表示生产环境

@Configuration
@Profile("dev") // 配置类只在 "dev" profile激活时加载
public class DevConfig {
    @Bean
    public DataSource dataSource() {
        // 开发环境的DataSource
        return new EmbeddedDatabaseDataSource();
    }
}

 







@Configuration
@Profile("prod") // 配置类只在 "prod" profile激活时加载
public class ProdConfig {
    @Bean
    public DataSource dataSource() {
        // 生产环境的DataSource
        return new SomeProductionDataSource();
    }
}

 







# 可以使用命令行参数来激活特定的 Profile
java -jar yourapp.jar --spring.profiles.active=prod

44. @PostConstruct,@PreDestroy作用

在 Spring 框架中,@PostConstruct@PreDestroy 注解用于在 Spring 管理的 bean 的生命周期中的特定点执行方法

  • @PostConstruct:用于标记在依赖注入完成之后,bean 的初始化方法。也就是说,当 Spring 容器创建了一个 bean 的所有属性都设置好之后。注解的方法会被调用。通常用于执行一些初始化操作。eg:启动一个服务或初始化一些资源
import javax.annotation.PostConstruct;
import org.springframework.stereotype.Component;

@Component
public class SomeBean {

    private SomeResource resource;

    // 假设这是通过构造函数或者setter方法注入的
    public SomeBean(SomeResource resource) {
        this.resource = resource;
    }

    @PostConstruct
    public void init() {
        // 在这里执行初始化逻辑
        resource.initialize();
    }
}













 





  • @PreDestroy:用于标记在 bean 销毁之前执行的方法。当 Spring 容器关闭时,它会遍历所有的 bean,并调用注解的方法。这个方法通常用于执行清理工作。eg:关闭数据库连接、停止服务或释放资源
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;

@Component
public class SomeBean {

    private SomeResource resource;

    // 假设这是通过构造函数或者setter方法注入的
    public SomeBean(SomeResource resource) {
        this.resource = resource;
    }

    @PreDestroy
    public void cleanup() {
        // 在这里执行清理逻辑
        resource.cleanUp();
    }
}













 





45. @Request,ResponseBody作用

@RequestBody@ResponseBody 注解用于处理 HTTP 请求和响应的数据

  • @RequestBody 注解用于读取 HTTP 请求的正文内容,并将其转换为指定类型的 Java 对象。通常用于接收客户端发送的 JSON、XML 等格式的数据,并将这些数据自动映射到 Java 对象中
// 客户端发送了一个 JSON 对象
{
    "username": "user1",
    "password": "password123"
}
  • 服务器端的 Spring MVC 控制器可以这样接收这个 JSON 对象
@PostMapping("/login")
public ResponseEntity<?> handleLoginRequest(@RequestBody LoginRequest loginRequest) {
    // 使用 loginRequest 对象进行登录处理
    return ResponseEntity.ok("Login successful");
}
  • @ResponseBody 注解用于指示 Spring MVC 将控制器方法返回的对象直接写入 HTTP 响应正文中,而不是返回一个视图(View)。通常用于构建 RESTful Web 服务,其中服务器响应的数据格式通常是 JSON 或 XML
@GetMapping("/user")
@ResponseBody
public User getUser() {
    // 从数据库或其他地方获取用户信息
    return new User("user1", "User One");
}

46. @PathVariable作用

在 Spring 框架中,@PathVariable 注解用于从URL路径中提取变量值,并将这些变量注入到控制器处理方法的参数中。这在开发 RESTful Web 服务时非常有用,因为 REST 风格通常使用 URL 路径来表示资源的层次结构

// 请求地址:http://example.com/users/123
@GetMapping("/users/{userId}")
public ResponseEntity<User> getUserById(@PathVariable("userId") String userId) {
    User user = userService.getUserById(userId);
    if (user != null) {
        return ResponseEntity.ok(user);
    } else {
        return ResponseEntity.notFound().build();
    }
}


 







47. @ModelAttribute作用

@ModelAttribute 注解用于将请求的参数或表单数据绑定到控制器处理方法的参数上,或将其添加到模型(Model)中,以便在视图(View)中展示

  • Spring MVC 会自动将请求参数的名称与方法参数名称进行匹配,并将请求参数的值注入到相应的参数中
@GetMapping("/users")
public String handleGetRequest(@ModelAttribute User user) {
    // 使用 user 对象进行业务处理
    userService.saveUser(user);
    return "userView";
}

 




48. @ExceptionHandler作用

用于处理控制器中抛出的异常。通过使用 @ExceptionHandler,可以定义特定异常的处理方法,这些方法会在控制器抛出异常时被调用

  • 指定异常类型:指定一个或多个异常类型,方法只能处理注解中指定的异常
  • 返回值:异常处理方法可以返回多种类型的值,如 String、View、ModelAndView 或 ResponseEntity 等
  • 参数:异常处理方法可以包含参数,除了必须包含导致异常的 Exception 参数外,还可以包含 WebRequest 或 BindingResult 参数
@Controller
public class MyController {

    @GetMapping("/users")
    public String getUsers() {
        // 可能会抛出异常的业务逻辑
    }

    // 使用 @ExceptionHandler 来处理特定异常
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> handleUserNotFoundException(Exception ex) {
        // 异常处理逻辑
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
    }
}









 





49. @ResponseStatus作用

用于定义一个方法或异常类所对应的 HTTP 状态码和原因短语。使用这个注解时,Spring 会将该注解指定的状态码和原因短语设置到响应的 HTTP 状态行中

  • 结合 @ControllerAdvice 注解,@ResponseStatus 可以用于创建全局异常处理方法,从而对应用程序中的异常进行统一处理
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "ooxx未找到")
public class UserNotFoundException extends RuntimeException {
    // 异常类实现
}

50. @RequestHeader注解

用于提取 HTTP 请求头中的值,并将其注入到控制器方法参数中。这允许访问诸如 Accept、Content-Type、User-Agent 等请求头信息

@GetMapping("/header-info")
public String getHeaderInfo(@RequestHeader("User-Agent") String userAgent) {
    // 使用 userAgent 进行业务处理
    return "headerInfoView";
}

50. @CookieValue注解

用于从 HTTP 请求的 Cookie 中提取值,并将其注入到控制器方法参数中。这使得能够访问客户端存储在 Cookie 中的数据

@GetMapping("/cookie-info")
public String getCookieInfo(@CookieValue("sessionId") String sessionId) {
    // 使用 sessionId 进行业务处理
    return "cookieInfoView";
}

51. @SessionAttribute作用

用于操作 HTTP 会话(session)中的属性。这个注解允许读取和修改存储在会话中的属性

  • 注入会话属性:将会话中的属性值注入到控制器方法的参数中
@GetMapping("/dashboard")
public String showDashboard(@SessionAttribute("user") User user) {
    // 使用 user 对象进行业务处理
    return "dashboardView";
}
  • 修改会话属性:在控制器方法中修改会话属性的值
@PostMapping("/update-user")
public String updateUser(@SessionAttribute("user") User user, User updatedUser) {
    user.setName(updatedUser.getName());
    user.setEmail(updatedUser.getEmail());
    return "redirect:/dashboard";
}
  • 删除会话属性:在控制器方法执行完成后,从会话中删除指定的属性
@GetMapping("/logout")
public String logoutUser(@SessionAttribute("user") User user) {
    // 执行注销逻辑,例如清除用户信息
    return "loginView";
}

52. @Validated,@Valid作用

  • @Valid:来自于 Java 的 Bean Validation API(JSR-303),通常用于方法参数、成员属性上,对标注的对象进行验证。如果对象包含约束注解(如 @NotNull, @Size, @Email 等),Spring将自动验证这些约束
  • @Validated:是 Spring 提供的,它不仅可以用来验证方法参数,还可以用于类级别,以启用类级别的验证,而且还支持分组校验
    • 当用于类级别时。@Validated 会使得类中的所有方法参数都默认进行验证,而无需在每个方法参数上都使用 @Valid 注解
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotNull;

public class UserService {

    @Validated
    public void createUser(@NotNull User user) {
        // 方法参数 user 将被验证,因为类 UserService 被 @Validated 注解
    }

    // 其他方法...
}

// 分组校验,不同组验证不同的参数

public class User {
    @NotEmpty(groups = Create.class)
    private String name;

    @NotEmpty(groups = Update.class)
    private String email;
    // getters and setters
}





 

















53. @Scheduled作用

用于定义定时任务。允许在 Spring 应用程序中以一种声明式的方式配置和执行计划任务,而不需要编写传统的定时任务代码

  • 分布式任务调用可以采用开源框架。eg:XXL-JOB、Elastic-Job
  • 单体可以用 Spring 的 Scheduled
@Component
public class ScheduledTasks {

    // 这个任务将每隔5秒执行一次
    @Scheduled(fixedRate = 5000)
    public void reportCurrentTime() {
        System.out.println("The current time is: " + new Date());
    }

    // 这个任务将使用cron表达式来配置执行计划
    @Scheduled(cron = "0 * * * * ?")
    public void scheduleTaskWithCronExpression() {
        // 任务逻辑
    }
}




 





 




54. @Cacheable注解

在 Spring 框架中,@Cacheable@CacheEvict 注解是 Spring Cache 抽象的两个核心组件,用于控制方法级别的缓存行为

  • 用于声明某个方法的结果是可缓存的。当这个方法被调用时,Spring 会检查缓存中是否已经存在相应的值,如果存在,就直接返回缓存的值,而不是再次执行方法体
@Cacheable(value = "cacheName", key = "#id")
public SomeObject findSomeObjectById(String id) {
    // 执行查找对象的逻辑
}

54. @CacheEvict注解

  • 用于声明当某个方法被调用时,需要从缓存中移除一个或多个条目。通常用于在数据变更操作后清除缓存,以保证缓存数据的一致性
@CacheEvict(value = "cacheName", allEntries = true)
public void deleteSomeObject(String id) {
    // 执行删除对象的逻辑
}

55. @Conditional作用

用于根据条件注册Bean,这意味着Bean的创建依赖于特定的条件。如果条件不满足,即使定义了Bean,Spring容器也不会创建它

  • @Conditional 可以应用于类或方法上。使用时,需要指定一个或多个条件类,这些条件类必须实现 Condition 接口,并重写 matches()
@Configuration
public class SomeConfiguration {

    @Bean
    @Conditional(OnWebApplicationCondition.class)
    public SomeBean someBeanForWeb() {
        return new SomeBean();
    }

    @Bean
    @Conditional(OnNotWebApplicationCondition.class)
    public SomeBean someBeanForNotWeb() {
        return new SomeBean();
    }
}




 





 




56. @Lazy作用

用于指定一个 Bean 在启动时不是立即创建,而是在首次被使用(即第一次被请求)时才创建。这个注解可以应用于类(类级别的 @Lazy 会使得整个类作为懒加载的 Bean),或在配置方法上

  • 一般循环依赖问题,可以使用这个注解
@Lazy
@Component
public class SomeHeavyBean {
    // 这个 Bean 在启动时不会创建,直到第一次被请求
}
@Configuration
public class SomeConfig {

    @Lazy
    @Bean
    public SomeBean someBean() {
        // 这个 Bean 在启动时不会创建,直到第一次被请求
        return new SomeBean();
    }
}

57. @PropertySource作用

用于指定一个或多个属性文件,这些文件包含了配置属性,可以在应用程序中被注入和使用

  • @PropertySource 注解可以应用于配置类上,以指示 Spring 加载一个属性文件。通常与 @Configuration 注解结合使用
  • 通过使用 @PropertySource,开发者可以将配置信息与代码分离,便于在不同环境之间切换配置而无需修改代码
@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig {
    // 配置类内容
}

58. @EventListener作用

用于标记一个方法,使其作为事件监听器,能够监听并处理 Spring 应用程序中发布的事件

  • 通过使用 @EventListener,开发者可以构建基于事件的通信机制,实现应用程序组件之间的解耦
  • handleCustomEvent 方法被标记为监听 CustomEvent 类型的事件。当任何地方通过 Spring 的事件发布机制发布了 CustomEvent 事件时,这个方法就会被调用
@Component
public class EventListenerExample {

    @EventListener
    public void handleCustomEvent(CustomEvent event) {
        // 处理 CustomEvent 事件
        System.out.println("ooxx火了" + event);
    }
}



 





59. Spring、MVC联系区别

1. 联系

  • 共同基础:MVC 建立在 Spring 框架的基础之上,共享 Spring 的基础设施。eg:依赖注入和 AOP
  • 无缝集成:MVC 可以无缝集成到任何 Spring 应用程序中,利用 Spring 提供的各种企业级服务
  • 共生发展:MVC 经常与 Spring 框架的其他模块一起使用,以提供完整的应用程序开发体验

2. 区别

  • 功能范围:Spring 是一个全面的企业级应用程序开发框架,而 MVC 主要关注 Web 应用程序的 MVC 架构
  • 应用场景:Spring 可以用于开发任何类型的 Java 应用程序,而 MVC 特别适用于开发 Web 应用程序和服务
  • 组件关注点:Spring 框架提供了一系列广泛的组件。eg:数据访问、消息传递等,而 MVC 更多地关注于 HTTP 请求的接收、处理和响应

60. WebFlux、MVC区别

Spring WebFlux 是 Spring 框架的一个模块,它用于构建响应式应用程序。它基于 Reactor 库,一个用于构建异步、事件驱动和非阻塞应用程序的库。Spring WebFlux 是完全异步和非阻塞的,这使得它非常适合构建能够扩展到大量用户和高并发的应用程序

  1. 编程模型:Spring WebFlux 基于响应式编程模型;而 MVC 基于传统的同步请求-响应模型
  2. 性能和扩展性:WebFlux 提供了更好的并发性能和扩展性,适合大量并发连接;MVC 则适用于传统的 Web 应用程序
  3. I/O 处理:WebFlux 使用非阻塞 I/O;而 MVC 在单线程中处理阻塞 I/O
  4. 函数式编程:WebFlux 支持函数式编程;而 MVC 主要使用命令式编程
  5. 适用场景:WebFlux 更适合构建能够处理大量并发用户和长连接的应用程序。eg:实时通信系统;MVC 适用于传统的 Web 表单和 CRUD 应用程序

61. MVC核心组件

  1. DispatcherServlet(前端控制器)
    • 是 MVC 的中心调度器,负责将请求路由到相应的处理器
    • 作为 MVC 模式中的控制器,作用是将请求转发给相应的处理器进行处理
  2. HandlerMapping(处理器映射器)
    • 负责将请求映射到对应的控制器和方法上
    • HandlerMapping 通过分析请求 URL 和其他参数,确定调用哪个处理器来处理请求
  3. Controller(控制器)
    • 是 MVC 模式中的 C 部分,用于处理用户的输入和系统事件
    • 在 MVC 中,控制器通常通过注解来标识。eg:@Controller@RestController
  4. ViewResolver(视图解析器)
    • 用于将控制器返回的逻辑视图名解析为具体的视图实现
    • 负责渲染视图,并结合模型数据生成最终的响应内容
  5. ModelAndView(模型和视图)
    • 持有视图信息和模型数据,用于在控制器和视图之间传递数据
    • 控制器填充模型数据后,返回 ModelAndView 对象,它会被视图解析器用来渲染视图
  6. HandlerAdapter(处理器适配器)
    • 用于将控制器中的方法调用适配到统一的处理程序
    • 每个不同的控制器类型都需要一个相应的 HandlerAdapter 来处理

62. Restful风格接口流程

  1. 客户端发送 HTTP 请求:用户或其他客户端通过 HTTP 请求(GET、POST、PUT、DELETE)访问服务端资源
  2. 请求到达 DispatcherServletHttpServletRequest 被传递到 MVC 的 DispatcherServlet
  3. URL 路由:DispatcherServlet 根据请求的 URL 和配置的映射找到相应的 Controller 和处理方法
  4. Controller 处理请求:请求被映射到一个 Controller 的特定处理方法
    1. 数据转换:可能会接收来自请求的数据(JSON 或 XML),并将其转换为 Java 对象
    2. 业务逻辑处理:调用业务逻辑层(Service Layer),执行实际的业务操作
  5. 生成响应:业务逻辑处理完成后,Controller 会生成响应。响应可能是一个资源的表示(JSON 或 XML),或是一个状态码
  6. 数据转换和消息体编写:Controller 方法使用 @ResponseBody@RestController 注解,将返回值转换为客户端请求的格式,并写入 HTTP 响应体
  7. 视图解析(可选):对于某些请求,如 GET 请求获取资源表示,Controller 可能会返回一个视图名称,而不是直接写入响应体
  8. 拦截器执行:如果配置了拦截器(Interceptor),可能会在请求处理前后执行。eg:日志记录、权限验证等
  9. 返回 HTTP 响应:最终,DispatcherServlet 将返回一个包含状态码、响应头和消息体的 HTTP 响应
  10. 客户端接收响应:客户端接收到服务端的响应,并根据状态码和响应体进行相应的处理

63. 如何定义一个Controller

Controller 是一个用于处理用户请求和返回响应的组件。作为模型-视图-控制器(MVC)设计模式中的控制器部分,负责接收用户的输入,调用业务逻辑,并返回响应结果

  • @Controller 用于标记传统 MVC 控制器,它们返回视图和模型数据
  • @RestController@Controller@ResponseBody 的组合,用于标记 RESTful 控制器,它们直接返回响应体

1. 参数接受

  • @RequestParam:来接收查询参数或表单数据
  • @PathVariable:提取 URL 路径中的变量
  • @RequestBody:接收请求正文中的 JSON 或 XML 数据
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MyController {

    @GetMapping("/greeting")
    public ModelAndView greeting(@RequestParam(name = "name", defaultValue = "World") String name) {
        ModelAndView modelAndView = new ModelAndView("greeting");
        modelAndView.addObject("message", "Hello, " + name + "!");
        return modelAndView;
    }

    @GetMapping("/user/{id}")
    public ModelAndView getUserById(@PathVariable("id") Long id) {
        // 调用业务逻辑获取用户信息
        ModelAndView modelAndView = new ModelAndView("userView");
        modelAndView.addObject("user", "User with ID " + id);
        return modelAndView;
    }
}

64. MVC中如何处理表单提交

  1. 定义数据模型
    • 在 Controller 中定义一个 JavaBean 或 POJO 类,其属性与表单字段相对应
  2. 接收表单数据:
    • 使用 @ModelAttribute 注解来接收表单数据,并将其绑定到数据模型的实例上
  3. 验证数据(可选)
    • 使用 Spring 的验证框架,如 @Valid 注解和 javax.validation 规范,来验证用户输入
  4. 处理业务逻辑
    • 在 Controller 中调用 Service 层的方法,根据用户输入执行相应的业务逻辑
  5. 返回响应
    • 根据业务逻辑处理结果,Controller 可以重定向到新页面,或返回到同一个表单页面(如果需要显示错误信息)
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class FormController {

    @GetMapping("/form")
    public ModelAndView showForm() {
        ModelAndView modelAndView = new ModelAndView("form", "command", new User());
        return modelAndView;
    }

    @PostMapping("/form")
    public ModelAndView processForm(@ModelAttribute("command") User user) {
        // 可以在这里添加对 user 的处理逻辑
        // 例如,保存 user 到数据库

        ModelAndView modelAndView = new ModelAndView("result");
        modelAndView.addObject("message", "Form submitted successfully!");
        return modelAndView;
    }
}

// User 类对应表单数据
class User {
    private String username;
    private String email;
    // getters and setters
}

65. MVC视图解析器作用

  1. 解析视图名称
    • 将 Controller 返回的逻辑视图名称解析为具体的视图实现
  2. 定位视图模板
    • 根据解析后的视图名称,视图解析器负责定位实际的视图模板。eg:JSP、HTML 或其他类型的模板文件
  3. 支持多种视图技术
    • Spring MVC 提供了多种内置的视图解析器,支持不同的视图技术。eg:JSP、FreeMarker、Thymeleaf 等
  4. 配置视图前缀和后缀
    • 允许配置视图的前缀和后缀,这样在 Controller 中只需要返回视图名称,会自动添加前缀和后缀
  5. 链式视图解析
    • 可以配置多个视图解析器,它们将按照顺序尝试解析视图,直到其中一个解析器成功返回视图
  6. 自定义视图解析逻辑
    • 开发者可以自定义视图解析器,以适应特定的视图解析逻辑或集成第三方视图技术
  7. 渲染视图
    • 一旦确定了视图的具体实现,视图解析器会负责渲染视图,将模型数据填充到视图模板中,并生成最终的响应内容
  8. 提高控制器的解耦
    • 通过使用视图解析器,Controller 不需要关心视图的具体实现细节,只需返回逻辑视图名称,这提高了控制器的解耦和可维护性
  9. 配置和管理视图解析器
    • 在 Spring MVC 的配置中,可以定义和配置一个或多个视图解析器,以满足应用程序的需求

66. MVC的拦截器

在 Spring MVC 中,拦截器(Interceptor)是一种用于在请求处理流程中的特定点执行任务的技术,类似于 Servlet 中的过滤器(Filter)

  1. 实现 HandlerInterceptor 接口
    • 定义一个类,实现 org.springframework.web.servlet.HandlerInterceptor 接口
  2. 创建拦截方法:
    • 实现 preHandle(),在控制器调用之前执行。如果返回 false,则中断请求处理
    • 实现 postHandle(),在控制器调用之后执行,但在视图渲染之前
    • 实现 afterCompletion(),在请求完成后执行,用于清理资源
  3. 注册拦截器:
    • 在 MVC 的配置类中注册拦截器,可以使用 WebMvcConfigurer 接口的 addInterceptors()
  4. 配置拦截器的路径匹配
    • 通过 addPathPatterns() 指定拦截器应该应用于哪些请求路径
  5. 配置拦截器的排除路径
    • 使用 excludePathPatterns() 指定哪些请求路径应该被拦截器忽略
  6. 配置拦截器的顺序
    • 使用 order() 设置拦截器的顺序,数字越小,优先级越高
  7. 启用 MVC 配置
    • 确保配置类上使用了@EnableWebMvc 注解,以启用 Web MVC 配置
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Component
public class MyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 在控制器调用之前执行
        System.out.println("Before controller...");
        return true; // 返回 true 继续流程,返回 false 则中断请求
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        // 在控制器调用之后,视图渲染之前执行
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 请求完成后执行
    }
}





 


















  • 配置类注册,像登录、注册请求一般都会放行,还要注意像 Knife4j 之类的地址
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor())
                .addPathPatterns("/**") 		// 拦截所有请求
                .excludePathPatterns("/ignore") // 排除路径
                .order(1); 						// 设置拦截器顺序
    }
}




 









67. MVC的数据绑定

在 Spring MVC 中,数据绑定是指将客户端发送的请求参数(如表单数据)绑定到控制器处理方法的参数上的过程。这是通过自动将请求参数映射到 Java 对象来实现的,从而简化了数据的处理

  1. 定义数据模型
    • 创建一个 JavaBean 或 POJO 类,其属性与请求参数相对应
  2. 使用 @ModelAttribute 注解
  3. 自动绑定请求参数
    • MVC 会根据请求参数的名称自动将其绑定到数据模型的属性上
  4. 自定义绑定过程
    • 如果需要自定义绑定过程,实现 org.springframework.web.bind.WebDataBinder 接口
  5. 使用 WebDataBinderFactory 定制绑定
    • 通过实现 WebDataBinderFactory 接口,可以创建自定义的 WebDataBinder 并注册到控制器中
  6. 处理复杂类型的绑定
    • 对于复杂类型或嵌套对象,MVC 可以递归地绑定请求参数
  7. 使用 @InitBinder 注解初始化 WebDataBinder
    • 用于注册自定义属性编辑器或自定义数据绑定规则
  8. 处理数据校验
    • 结合 @Valid 注解和 JSR-303/JSR-380 验证标准,可以在数据绑定后进行校验
  9. 使用 ResponseEntityModelAndView 响应
    • 根据数据绑定和校验的结果,控制器可以返回 ResponseEntityModelAndView
  10. 配置数据绑定的属性
    • 在 MVC 的配置文件中,可以设置 webBindingInitializer 属性来定制数据绑定的行为

一般都用 @RequestBody 实体类接受,或 SpringMVC 自动映射属性名

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class DataBindingController {

    @GetMapping("/submit")
    public ModelAndView handleFormSubmission(@ModelAttribute("user") User user) {
        // 使用 user 对象进行业务处理
        ModelAndView modelAndView = new ModelAndView("successView");
        return modelAndView;
    }
}

class User {
    private String username;
    private String email;
    // 标准 getter 和 setter 方法
}

68. MVC的国际化支持

  1. 定义资源文件
    • 创建属性文件(.properties.yml),用于存储不同语言的文本信息
      • messages.properties 用于存储默认语言的消息
      • messages_zh_CN.properties 用于存储中文(中国)的消息
  2. 配置消息源
    • 在 MVC 配置中定义 MessageSource 接口的实现,通常使用 ReloadableResourceBundleMessageSource 来加载资源文件
  3. 设置国际化解析器
    • 使用 LocaleResolver 接口来解析用户的区域设置。MVC 提供了多种实现,如 AcceptHeaderLocaleResolverCookieLocaleResolver
  4. 配置视图解析器
    • 以支持国际化,确保视图名称可以根据区域设置来解析
  5. 使用消息标签
    • 在 JSP 或其他视图模板中使用 spring:message 标签来显示国际化的消息
  6. 在控制器中使用区域设置
    • 在控制器中可以通过 LocaleResolver 获取当前请求的区域设置,并据此进行逻辑处理
  7. 设置默认区域设置
    • 如果请求中没有明确指定区域设置,可以使用 LocaleResolver 设置一个默认的区域设置
  8. 更改区域设置
    • 允许用户在应用程序中更改其区域设置,通常通过 URL 参数、表单或 cookie 来实现
  9. 使用 @ResponseEntity 进行国际化响应
    • 在控制器方法中使用 @ResponseEntity 来自定义响应,包括国际化的消息
  10. 集成第三方库
  • 可以使用如 Thymeleaf 这样的模板引擎,它与 Spring 的国际化支持紧密集成,提供了额外的国际化功能
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

@Configuration
public class WebConfig {

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver slr = new SessionLocaleResolver();
        slr.setDefaultLocale(Locale.US); // 设置默认区域设置
        return slr;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
        lci.setParamName("lang"); // 设置区域设置更改的 URL 参数名称
        return lci;
    }

    // 配置消息源
    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:messages");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }
}













 






 







 
 



  • JSP 使用国际化
<spring:message code="greeting" />

69. MVC的异常处理

  1. 使用 @ExceptionHandler 注解
    • 捕获和处理特定的异常
  2. 定义异常处理方法
    • 创建方法来处理捕获的异常,这些方法可以返回视图名称、ModelAndViewResponseEntity
  3. 返回错误信息
    • 在异常处理方法中,可以设置模型属性来传递错误信息,并将其添加到模型中,以便在视图中显示
  4. 使用 @ControllerAdvice 类,应用全局异常处理
    • @ControllerAdvice 类中定义 @ExceptionHandler 方法,来捕获所有控制器中抛出的特定类型的异常

一般都会自定义业务异常用来捕获项目业务异常

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.http.HttpStatus;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(SpecificException.class)
    public ModelAndView handleSpecificException(SpecificException ex) {
        ModelAndView modelAndView = new ModelAndView("error");
        modelAndView.addObject("message", ex.getMessage());
        return modelAndView;
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public void handleAllExceptions(Exception ex) {
        // 可以记录日志或进行其他错误处理操作
    }
}

@ResponseStatus(value = HttpStatus.BAD_REQUEST)
class SpecificException extends RuntimeException {
    // 自定义异常类
}








 













 



70. MVC的RESTful风格实现

  1. 使用 @RestController 注解
    • 这表明该控制器的所有方法返回的数据将直接写入 HTTP 响应正文,而不是返回一个视图
  2. 定义请求映射
    • 使用 @RequestMapping 或其特定 HTTP 方法的快捷注解(@GetMapping@PostMapping等)来映射 URL 到控制器方法
  3. 返回响应体
    • 控制器方法返回的对象将被转换为 JSON 或 XML 格式的响应体,MVC 使用消息转换器(HttpMessageConverter)来实现这一转换
  4. 使用 @RequestBody 注解
    • 参数上使用 @RequestBody 注解来接收请求正文中的数据,并将其转换为 Java 对象
  5. 使用 @PathVariable 注解
    • 从 URL 中提取变量并将其注入到控制器方法的参数中,这有助于实现资源的标识
  6. 使用 @RequestParam 注解
    • 从请求的查询参数中获取数据,并将其绑定到控制器方法的参数上
  7. 使用 @ModelAttribute 注解
    • 用于将请求参数绑定到一个模型对象上,该对象可以是表单提交的数据或 URL 中的查询参数
  8. 状态码和 HTTP 头
    • 控制器方法可以返回 ResponseEntity 对象,这允许你自定义 HTTP 状态码和响应头
  9. 异常处理
    • 使用 @ExceptionHandler 注解来处理控制器中抛出的异常,并返回合适的 HTTP 状态码
  10. 使用 Spring Data REST
    • 对于基于 Spring Data 的存储库,Spring Data REST 提供了自动配置的 RESTful 支持,可以简化数据访问层的 CRUD 操作

使用 RESTFUL 风格会更加清晰规范

import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable("id") Long id) {
        User user = userService.findById(id);
        if (user != null) {
            return ResponseEntity.ok(user);
        } else {
            return ResponseEntity.notFound().build();
        }
    }

    @PostMapping
    public ResponseEntity<Void> createUser(@RequestBody User user) {
        userService.save(user);
        return ResponseEntity.ok().build();
    }
}

71. MVC的数据验证

  1. 使用 JSR-303/JSR-380 注解
    • 在数据模型的字段上使用 JSR-303/JSR-380 规范提供的注解,eg:@NotNull, @Size, @Valid
  2. 添加 Bean Validation API 依赖
    • 项目中包含了 Bean Validation API 的依赖,Spring 使用这个 API 来进行数据验证
  3. 实现 Validator 接口
    • 如果需要自定义验证逻辑,可以创建一个类实现 Validator 接口或 ConstraintValidator 接口
  4. 在控制器中使用 @Valid 注解
    • 以启用自动验证,一般数据验证直接用条件语句判断即可,可以使用工具类,没必要使用注解,人工判断更为方便
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import javax.validation.Valid;

@Controller
@RequestMapping("/users")
public class UserController {

    @PostMapping("/create")
    public String createUser(@Valid @ModelAttribute("user") User user, BindingResult result) {
        if (result.hasErrors()) {
            // 处理验证错误
            return "userForm";
        }
        // 保存用户并重定向
        userService.save(user);
        return "redirect:/users/" + user.getId();
    }
}

@Validated
class User {
    @NotNull(message = "Username is required")
    private String username;
    // ...
}









 










 





72. MVC的ModelAndView

在 MVC 中,ModelAndView 是一个重要的类,用于将模型数据和视图名称结合起来,返回给视图渲染器进行视图的渲染

import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public ModelAndView getUser(@PathVariable("id") Long id) {
        ModelAndView modelAndView = new ModelAndView("userView");
        User user = userService.findById(id);
        modelAndView.addObject("user", user);
        if (user != null) {
            modelAndView.setViewName("userDetails");
        } else {
            modelAndView.setViewName("userNotFound");
            modelAndView.setStatus(HttpStatus.NOT_FOUND);
        }
        return modelAndView;
    }
}

73. JPA、Hibernate区别

JPA 相当于是一种规范接口,Hibernate 是 JPA 规范的一个流行实现,两者的概念以及区别:

  1. JPA:是 Java 持久化的规范,定义了 Java 对象和数据库表之间映射的标准方式,并提供了一套持久化对象的 API
  2. Hibernate:实现 JPA 规定的所有功能外,它还提供了许多超越 JPA 规范的特性。eg:高级缓存、自然ID等