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;
}
2. 如何解决循环依赖
关键就是提前暴露未完全创建完毕的 Bean
- 在 Spring 中,只有同时满足以下两点可解决:
- 依赖的 Bean 必须都是单例
- 依赖注入的方式,必须不全是构造器注入,且 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 也完整了,且它们是相互依赖的
2. 为什么不能全是构造器注入
无法提前暴露未完全创建完毕的 Bean
- 在 Spring 中创建 Bean 分三步:
- 实例化(createBeanInstance)
- 属性注入(populateBean)
- 初始化(initializeBean)
如果全是构造器注入,比如A(B b)
,那表明在 new 的时候,就需要得到 B,此时需要 new B ,但是 B 也是要在构造的时候注入 A ,即B(A a)
,这时候 B 需要在一个 map 中找到不完整的 A ,发现找不到
3. 一个set,一个构造器注入?
- 假设 A 是通过 set 注入 B,B 通过构造函数注入 A,此时是成功的
- 假设 A 是通过构造器注入 B,B 通过 set 注入 A,此时是失败的
但是 Spring 容器是按照字母序创建 Bean 的,A 的创建永远排在 B 前面
4. 解决循环依赖全流程
明确了 Spring 创建 Bean 的三步骤之后,我们再来看看它为单例搞的三个 map:
- 一级缓存,singletonObjects,存储所有已创建完毕的单例 Bean (完整的 Bean)
- 二级缓存,earlySingletonObjects,存储所有仅完成实例化,但还未进行属性注入和初始化的 Bean
- 三级缓存,singletonFactories,存储能建立这个 Bean 的一个工厂,通过工厂能获取这个 Bean,延迟化 Bean 的生成,工厂生成的 Bean 会塞入二级缓存
这三个 map 是如何配合的呢?
- 首先,获取单例 Bean 的时候会通过 BeanName 先去 singletonObjects(一级缓存) 查找完整的 Bean,如果找到则直接返回,否则进行步骤 2
- 看对应的 Bean 是否在创建中,如果不在直接返回找不到,如果是,则会去 earlySingletonObjects (二级缓存)查找 Bean,如果找到则返回,否则进行步骤 3
- 去 singletonFactories (三级缓存)通过 BeanName 查找到对应的工厂,如果存着工厂则通过工厂创建 Bean ,并且放置到 earlySingletonObjects 中
- 如果三个缓存都没找到,则返回 null
如果查询发现 Bean 还未创建,到第二步就直接返回 null,不会继续查二级和三级缓存。返回 null 之后,说明这个 Bean 还未创建,标记这个 Bean 正在创建中,然后再调用 createBean 来创建 Bean,而实际创建是调用方法 doCreateBean
- 实例化
- 属性注入
- 初始化
实例化 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 来操作的
还扩展了很多其他功能:
- 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 扫描到注解自动加进来的
- 然后通过一个对象
CglibMethodInvocation
将这个集合封装起来,紧接着调用这个对象的proceed()
,可看到集合chain
被传入了
开始递归套娃,CglibMethodInvocation#proceed
核心调用逻辑:
currentInterceptorIndex
变量,通过递归,每次新增这索引值,来得到下一个interceptor
,并且每次都传入当前对象并调用interceptor#invoke
,这样就实现了拦截链的调用
MethodBeforeAdviceInterceptor
前置拦截举例invoke()
的实现是先执行切入的前置逻辑,然后再继续调用CglibMethodInvocation#proceed
(也就是mi.proceed
),进行下一个 interceptor 的调用
1. 总结
- Spring 根据
@Before, @After, @AfterReturning, @AfterThrowing
这些注解,往集合里面加入对应的 Spring 提供的MethodInterceptor
实现 - 然后通过一个对象 CglibMethodInvocation 将这个集合封装起来,紧接着调用这个对象的
proceed()
,具体是利用currentInterceptorIndex
下标,利用递归顺序地执行集合里面的MethodInterceptor
,这样就完成了拦截链的调用
第一个索引位置的 ExposeInvocationInterceptor
- 这个
Interceptor
作为第一个被调用,实际上就是将创建的CglibMethodInvocation
这个对象存入threadlocal
中,方便后面Interceptor
调用时能得到这个对象,进行一些调用
18. AOP和AspectJ区别
- Spring AOP 是动态代理
- 运行时织入。只支持方法级别的织入
- AspectJ 是静态代理
- 编译时织入。编译时就准备完毕,在调用时没有额外的织入开销,性能更好些。支持字段、方法、构造函数等,它更加强大,当然也更加复杂
19. Bean的生命周期
文字描述:
- 实例化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
返回给用户 - 常用的视图有
jsp
、freemarker
、velocity
等
22. MVC父子容器是什么
官网图:
- 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
- 模板方法:
JdbcTemplate
、RestTemplate
- 代理模式:AOP
- 单例
- 责任链模式:拦截器
- 观察者模式:Spring 里的监听器
- 适配器模式:SpringMVC 提到的
HandlerAdapter
24. Spring事务隔离级别
- DEFAULT:就是使用数据库定义的隔离级别
- 读未提交(READ UNCOMMITED)简称 RU:最宽松的限制,即一个事务的修改还未提交,另一个事务就能看到修改的结果,会产生脏读现象
- 读已提交(READ COMMITED)简称 RC:即一个事务只能读到另一个事务已经提交的修改,所以在一个事务里面的多次查询可能会得到不同的结果,因为第一次查询的时候,另一个事务还未提交,所以看不到修改的结果,第二次查询的时候,另一个事务提交了,因此读到了修改后的结果,所以两次查询结果不一致,称之为不可重复读
- 可重复读(REPEATABLE READ)简称 RR:它比 RC 更严格,即一个事务开始的时候读不到,那之后也读不到,也就是一个事务内的多次读结果是一致的,但是有幻读情况,即第一次读拿到了四行数据,第二次读拿到了五行数据,因为有新插入的行,不过 InnoDB 利用 MVCC 解决了大部分幻读的情况,利用 update 当前读再 select 的幻读无法解决
- 串行化(SERIALIZABLE):最严格的模式,即这个隔离级别的读写访问会把需要遍历到的记录上锁,这样另一个事务要访问对应的记录时候就被阻塞了,需要等待上一个事务提交之后,才能继续执行,所以叫串行
- 在 InnoDB 中,非自动提交时,会隐式地将所有普通的
SELECT
语句转换为SELECT... LOCK IN SHARE MODE
来满足这个隔离级别
- 在 InnoDB 中,非自动提交时,会隐式地将所有普通的
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相关术语
- 切面(Aspect):其实就是定义了一个 java 类,里面包含了通知(advice)和切点(pointcut),定义了在何处以及何时执行通知,将切面的一些东西模块化了
- 通知(Advice)
- 前置通知(Before advice):在目标方法执行前执行
- 后置通知(After returning advice):在目标方法成功执行后执行
- 后置异常通知(After throwing advice):在目标方法抛出异常后执行
- 后置最终通知(After (finally) advice):无论目标方法如何结束(正常返回或抛出异常),都会执行
- 环绕通知(Around advice):在目标方法执行前后都执行,并且可以控制目标方法的执行过程,环绕通知可以用作日志打印或者权限校验
- 切点(Pointcut):切点是一个表达式,用于定义在哪些连接点上执行通知,简单理解就是通过这个表达式可以找到想要织入的哪些方法
- 连接点(Join point):连接点是程序执行过程中可以应用切面的点,例如方法的调用、方法的执行、异常的抛出等,可以拿到切入方法名等诸多属性
30. Spring通知类型
- 前置通知(Before advice):在目标方法执行之前执行的通知。它不会影响目标方法的执行流程,但可以在方法执行前执行一些逻辑,eg:验证参数
- 后置通知(After returning advice):在目标方法成功执行并返回结果后执行的通知。它可以访问目标方法的返回值,但无法修改返回值。通常用于记录日志或清理资源等操作
- 后置异常通知(After throwing advice):在目标方法抛出异常后执行的通知。它可以访问目标方法抛出的异常,并且可以根据异常类型进行相应的处理,eg:记录异常信息或执行异常处理逻辑
- 后置最终通知(After (finally) advice):无论目标方法如何结束(正常返回或抛出异常),都会执行的通知。它类似于Java中的finally块,在方法结束时执行一些清理工作,eg:释放资源或关闭连接
- 环绕通知(Around advice):环绕通知是最强大的一种通知类型,它可以在目标方法执行前后都执行,并且可以控制目标方法的执行过程。它需要负责调用目标方法,并且可以决定是否继续执行目标方法以及是否返回自定义的返回值
1. 不同版本执行顺序
Spring4版本
- 正常情况
- 环绕之前通知
- 前置通知Before
- 被增强的方法
- 环绕之后通知
- After最终通知
- AfterReturning 后置通知
- 异常情况
- 环绕之前通知
- 前置通知Before
- 被增强的方法
- After最终通知
- AfterThrowing 异常通知
Spring5版本
- 正常情况
- 环绕之前通知
- 前置通知Before
- 被增强的方法
- 环绕之后通知
- AfterReturning后置通知
- After最终通知
- 异常情况
- 环绕之前通知
- 前置通知Before
- 被增强的方法
- AfterThrowing异常通知
- After最终通知
31. IOC容器初始化过程
- 加载配置文件或配置类。IoC 容器首先需要加载应用程序的配置信息,这些配置信息可以是 XML 配置文件、Java 配置类或注解配置等方式
- 创建和配置 BeanFactory 或 ApplicationContext
- 加载 Bean 定义。通过 BeanDefinitionReader 读取和解析 Bean 定义,得到 BeanDefinition 注册到 BeanDefinitionRegistry 中
- 创建 Bean 实例。通过构造方法或者工厂方法实例化 Bean
- 处理 Bean 的依赖注入。对 Bean 的属性完成注入,这个过程会根据配置信息中的依赖关系,对 Bean 进行依赖注入
- BeanPostProcessor 处理。这些处理器会在 Bean 初始化生命周期中加入定义的处理逻辑,postProcessBeforeInitialization 和 postProcessAfterInitialization 分别在 Bean 初始化前后被调用
- 调用初始化方法。eg:实现 InitializingBean 的 bean 会被调用 afterPropertiesSet 方法
32. Bean注入容器方式
- 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>
- 基于注解方式
- 使用
@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(); } }
- 使用
- 自动注入方式
- 利用
@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. 事务失效
五种常见导致失效:
- rollbackFor 没设置对:比如默认没有任何设置,则方法内抛出 IOException 则不会回滚,需要配置
@Transactional(rollbackFor = Exception.class)
- 异常被捕获了:比如 catch 了异常打了 log,这样事务无法正常获取到错误,因此不会回滚
- 同一个类中方法调用:因为事务是基于动态代理实现的,同类的方法调用不会走代理方法,因此事务自然就失效了
- @Transactional:应用在非 public 修饰的方法上,Spring 事务管理器判断非公共方法则不应用事务
- 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 容器启动的一般过程:
- 加载配置文件
- 首先会读取配置文件(eg:XML 配置文件、Java Config 类等),包括配置数据库连接、事务管理、AOP 配置等
- 实例化容器
- 根据配置文件中的信息创建容器 ApplicationContext,在容器启动阶段实例化 BeanFactory,并加载容器中的 BeanDefinitions
- 解析 BeanDefinitions
- 解析配置文件中的 BeanDefinitions,即声明的 Bean 元数据,包括 Bean 的作用域、依赖关系等信息
- 实例化 Bean
- 根据 BeanDefinitions 实例化 Bean 对象,将其放入容器管理
- 注入依赖
- 将 Bean 之间的依赖关系进行注入,包括构造函数注入、属性注入等
- 处理 Bean 生命周期初始化方法
- Spring 调用 Bean 初始化方法(如果有定义的话),对 Bean 进行初始化
- Bean 实现了
InitializingBean
接口,Spring 会调用其afterPropertiesSet()
- 处理 BeanPostProcessors
- 容器定义了很多 BeanPostProcessor,处理其中的自定义逻辑。eg:postProcessBeforeInitialization 会在 Bean 初始化前调用, postProcessAfterInitialization 则在之后调用
- 代理切面处理
- 根据配置注册 AOP 切面,生成代理对象,将切面织入到目标对象中
- 发布事件
- 可能会在启动过程中发布一些事件。eg:容器启动事件
- 完成启动
- 当所有 Bean 初始化完毕、依赖注入完成、AOP 配置生效等都准备就绪时,Spring 容器启动完成
39. 单例Bean并发安全问题
Spring 的单例 Bean 默认是非线程安全的,但只要我们避免在多个 bean 之间共享一些数据,就不会发生并发问题
原因:
- 单例 Bean 的生命周期:Spring 容器在初始化时会创建并管理单例 Bean,这些 Bean 在整个应用程序生命周期内只会被创建一次,并且被多个线程共享使用
- 多线程访问:如果单例 Bean 中包含共享的可变状态(实例变量),多个线程同时访问并修改这些共享状态时,可能会导致并发安全问题。eg:数据不一致、脏读、死锁等
40. Bean如何保证并发安全
大致分三种方式:
- 无状态Bean:如果单例 Bean 不包含可变状态,或仅包含只读状态,通常不会有并发安全问题。eg:所有方法都是无状态的纯函数,或使用的是本地变量
- 线程安全设计:确保单例 Bean 中的方法是线程安全的。可以通过同步方法、使用线程安全的数据结构(
ConcurrentHashMap
)、或使用java.util.concurrent
包中的其他并发工具类 @Scope("prototype")
:对于需要状态的 Bean,可以将其作用域设置为原型(prototype),这样每次请求该 Bean 时,Spring 容器都会返回一个新的实例
41. @Primary作用
主要作用:解决 Bean 注入时的歧义问题,当一个接口或父类有多个实现时,Spring 无法确定该注入哪个具体的 Bean,此时可以使用 @Primary
来指明首选的 Bean
- 多实现类
- 注入优先级
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 是完全异步和非阻塞的,这使得它非常适合构建能够扩展到大量用户和高并发的应用程序
- 编程模型:Spring WebFlux 基于响应式编程模型;而 MVC 基于传统的同步请求-响应模型
- 性能和扩展性:WebFlux 提供了更好的并发性能和扩展性,适合大量并发连接;MVC 则适用于传统的 Web 应用程序
- I/O 处理:WebFlux 使用非阻塞 I/O;而 MVC 在单线程中处理阻塞 I/O
- 函数式编程:WebFlux 支持函数式编程;而 MVC 主要使用命令式编程
- 适用场景:WebFlux 更适合构建能够处理大量并发用户和长连接的应用程序。eg:实时通信系统;MVC 适用于传统的 Web 表单和 CRUD 应用程序
61. MVC核心组件
- DispatcherServlet(前端控制器)
- 是 MVC 的中心调度器,负责将请求路由到相应的处理器
- 作为 MVC 模式中的控制器,作用是将请求转发给相应的处理器进行处理
- HandlerMapping(处理器映射器)
- 负责将请求映射到对应的控制器和方法上
- HandlerMapping 通过分析请求 URL 和其他参数,确定调用哪个处理器来处理请求
- Controller(控制器)
- 是 MVC 模式中的 C 部分,用于处理用户的输入和系统事件
- 在 MVC 中,控制器通常通过注解来标识。eg:
@Controller
或@RestController
- ViewResolver(视图解析器)
- 用于将控制器返回的逻辑视图名解析为具体的视图实现
- 负责渲染视图,并结合模型数据生成最终的响应内容
- ModelAndView(模型和视图)
- 持有视图信息和模型数据,用于在控制器和视图之间传递数据
- 控制器填充模型数据后,返回
ModelAndView
对象,它会被视图解析器用来渲染视图
- HandlerAdapter(处理器适配器)
- 用于将控制器中的方法调用适配到统一的处理程序
- 每个不同的控制器类型都需要一个相应的
HandlerAdapter
来处理
62. Restful风格接口流程
- 客户端发送 HTTP 请求:用户或其他客户端通过 HTTP 请求(GET、POST、PUT、DELETE)访问服务端资源
- 请求到达
DispatcherServlet
:HttpServletRequest
被传递到 MVC 的DispatcherServlet
- URL 路由:
DispatcherServlet
根据请求的 URL 和配置的映射找到相应的 Controller 和处理方法 - Controller 处理请求:请求被映射到一个 Controller 的特定处理方法
- 数据转换:可能会接收来自请求的数据(JSON 或 XML),并将其转换为 Java 对象
- 业务逻辑处理:调用业务逻辑层(Service Layer),执行实际的业务操作
- 生成响应:业务逻辑处理完成后,Controller 会生成响应。响应可能是一个资源的表示(JSON 或 XML),或是一个状态码
- 数据转换和消息体编写:Controller 方法使用
@ResponseBody
或@RestController
注解,将返回值转换为客户端请求的格式,并写入 HTTP 响应体 - 视图解析(可选):对于某些请求,如 GET 请求获取资源表示,Controller 可能会返回一个视图名称,而不是直接写入响应体
- 拦截器执行:如果配置了拦截器(Interceptor),可能会在请求处理前后执行。eg:日志记录、权限验证等
- 返回 HTTP 响应:最终,
DispatcherServlet
将返回一个包含状态码、响应头和消息体的 HTTP 响应 - 客户端接收响应:客户端接收到服务端的响应,并根据状态码和响应体进行相应的处理
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中如何处理表单提交
- 定义数据模型
- 在 Controller 中定义一个 JavaBean 或 POJO 类,其属性与表单字段相对应
- 接收表单数据:
- 使用
@ModelAttribute
注解来接收表单数据,并将其绑定到数据模型的实例上
- 使用
- 验证数据(可选)
- 使用 Spring 的验证框架,如
@Valid
注解和javax.validation
规范,来验证用户输入
- 使用 Spring 的验证框架,如
- 处理业务逻辑
- 在 Controller 中调用 Service 层的方法,根据用户输入执行相应的业务逻辑
- 返回响应
- 根据业务逻辑处理结果,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视图解析器作用
- 解析视图名称
- 将 Controller 返回的逻辑视图名称解析为具体的视图实现
- 定位视图模板
- 根据解析后的视图名称,视图解析器负责定位实际的视图模板。eg:JSP、HTML 或其他类型的模板文件
- 支持多种视图技术
- Spring MVC 提供了多种内置的视图解析器,支持不同的视图技术。eg:JSP、FreeMarker、Thymeleaf 等
- 配置视图前缀和后缀
- 允许配置视图的前缀和后缀,这样在 Controller 中只需要返回视图名称,会自动添加前缀和后缀
- 链式视图解析
- 可以配置多个视图解析器,它们将按照顺序尝试解析视图,直到其中一个解析器成功返回视图
- 自定义视图解析逻辑
- 开发者可以自定义视图解析器,以适应特定的视图解析逻辑或集成第三方视图技术
- 渲染视图
- 一旦确定了视图的具体实现,视图解析器会负责渲染视图,将模型数据填充到视图模板中,并生成最终的响应内容
- 提高控制器的解耦
- 通过使用视图解析器,Controller 不需要关心视图的具体实现细节,只需返回逻辑视图名称,这提高了控制器的解耦和可维护性
- 配置和管理视图解析器
- 在 Spring MVC 的配置中,可以定义和配置一个或多个视图解析器,以满足应用程序的需求
66. MVC的拦截器
在 Spring MVC 中,拦截器(Interceptor)是一种用于在请求处理流程中的特定点执行任务的技术,类似于 Servlet 中的过滤器(Filter)
- 实现 HandlerInterceptor 接口
- 定义一个类,实现
org.springframework.web.servlet.HandlerInterceptor
接口
- 定义一个类,实现
- 创建拦截方法:
- 实现
preHandle()
,在控制器调用之前执行。如果返回 false,则中断请求处理 - 实现
postHandle()
,在控制器调用之后执行,但在视图渲染之前 - 实现
afterCompletion()
,在请求完成后执行,用于清理资源
- 实现
- 注册拦截器:
- 在 MVC 的配置类中注册拦截器,可以使用 WebMvcConfigurer 接口的
addInterceptors()
- 在 MVC 的配置类中注册拦截器,可以使用 WebMvcConfigurer 接口的
- 配置拦截器的路径匹配
- 通过
addPathPatterns()
指定拦截器应该应用于哪些请求路径
- 通过
- 配置拦截器的排除路径
- 使用
excludePathPatterns()
指定哪些请求路径应该被拦截器忽略
- 使用
- 配置拦截器的顺序
- 使用
order()
设置拦截器的顺序,数字越小,优先级越高
- 使用
- 启用 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 对象来实现的,从而简化了数据的处理
- 定义数据模型
- 创建一个 JavaBean 或 POJO 类,其属性与请求参数相对应
- 使用
@ModelAttribute
注解 - 自动绑定请求参数
- MVC 会根据请求参数的名称自动将其绑定到数据模型的属性上
- 自定义绑定过程
- 如果需要自定义绑定过程,实现
org.springframework.web.bind.WebDataBinder
接口
- 如果需要自定义绑定过程,实现
- 使用
WebDataBinderFactory
定制绑定- 通过实现
WebDataBinderFactory
接口,可以创建自定义的WebDataBinder
并注册到控制器中
- 通过实现
- 处理复杂类型的绑定
- 对于复杂类型或嵌套对象,MVC 可以递归地绑定请求参数
- 使用
@InitBinder
注解初始化WebDataBinder
- 用于注册自定义属性编辑器或自定义数据绑定规则
- 处理数据校验
- 结合
@Valid
注解和 JSR-303/JSR-380 验证标准,可以在数据绑定后进行校验
- 结合
- 使用
ResponseEntity
或ModelAndView
响应- 根据数据绑定和校验的结果,控制器可以返回
ResponseEntity
或ModelAndView
- 根据数据绑定和校验的结果,控制器可以返回
- 配置数据绑定的属性
- 在 MVC 的配置文件中,可以设置
webBindingInitializer
属性来定制数据绑定的行为
- 在 MVC 的配置文件中,可以设置
一般都用 @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的国际化支持
- 定义资源文件
- 创建属性文件(
.properties
或.yml
),用于存储不同语言的文本信息messages.properties
用于存储默认语言的消息messages_zh_CN.properties
用于存储中文(中国)的消息
- 创建属性文件(
- 配置消息源
- 在 MVC 配置中定义
MessageSource
接口的实现,通常使用ReloadableResourceBundleMessageSource
来加载资源文件
- 在 MVC 配置中定义
- 设置国际化解析器
- 使用
LocaleResolver
接口来解析用户的区域设置。MVC 提供了多种实现,如AcceptHeaderLocaleResolver
或CookieLocaleResolver
- 使用
- 配置视图解析器
- 以支持国际化,确保视图名称可以根据区域设置来解析
- 使用消息标签
- 在 JSP 或其他视图模板中使用
spring:message
标签来显示国际化的消息
- 在 JSP 或其他视图模板中使用
- 在控制器中使用区域设置
- 在控制器中可以通过
LocaleResolver
获取当前请求的区域设置,并据此进行逻辑处理
- 在控制器中可以通过
- 设置默认区域设置
- 如果请求中没有明确指定区域设置,可以使用
LocaleResolver
设置一个默认的区域设置
- 如果请求中没有明确指定区域设置,可以使用
- 更改区域设置
- 允许用户在应用程序中更改其区域设置,通常通过 URL 参数、表单或 cookie 来实现
- 使用
@ResponseEntity
进行国际化响应- 在控制器方法中使用
@ResponseEntity
来自定义响应,包括国际化的消息
- 在控制器方法中使用
- 集成第三方库
- 可以使用如 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的异常处理
- 使用
@ExceptionHandler
注解- 捕获和处理特定的异常
- 定义异常处理方法
- 创建方法来处理捕获的异常,这些方法可以返回视图名称、
ModelAndView
或ResponseEntity
- 创建方法来处理捕获的异常,这些方法可以返回视图名称、
- 返回错误信息
- 在异常处理方法中,可以设置模型属性来传递错误信息,并将其添加到模型中,以便在视图中显示
- 使用
@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风格实现
- 使用
@RestController
注解- 这表明该控制器的所有方法返回的数据将直接写入 HTTP 响应正文,而不是返回一个视图
- 定义请求映射
- 使用
@RequestMapping
或其特定 HTTP 方法的快捷注解(@GetMapping
、@PostMapping
等)来映射 URL 到控制器方法
- 使用
- 返回响应体
- 控制器方法返回的对象将被转换为 JSON 或 XML 格式的响应体,MVC 使用消息转换器(
HttpMessageConverter
)来实现这一转换
- 控制器方法返回的对象将被转换为 JSON 或 XML 格式的响应体,MVC 使用消息转换器(
- 使用
@RequestBody
注解- 参数上使用
@RequestBody
注解来接收请求正文中的数据,并将其转换为 Java 对象
- 参数上使用
- 使用
@PathVariable
注解- 从 URL 中提取变量并将其注入到控制器方法的参数中,这有助于实现资源的标识
- 使用
@RequestParam
注解- 从请求的查询参数中获取数据,并将其绑定到控制器方法的参数上
- 使用
@ModelAttribute
注解- 用于将请求参数绑定到一个模型对象上,该对象可以是表单提交的数据或 URL 中的查询参数
- 状态码和 HTTP 头
- 控制器方法可以返回
ResponseEntity
对象,这允许你自定义 HTTP 状态码和响应头
- 控制器方法可以返回
- 异常处理
- 使用
@ExceptionHandler
注解来处理控制器中抛出的异常,并返回合适的 HTTP 状态码
- 使用
- 使用 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的数据验证
- 使用 JSR-303/JSR-380 注解
- 在数据模型的字段上使用 JSR-303/JSR-380 规范提供的注解,eg:
@NotNull, @Size, @Valid
等
- 在数据模型的字段上使用 JSR-303/JSR-380 规范提供的注解,eg:
- 添加 Bean Validation API 依赖
- 项目中包含了 Bean Validation API 的依赖,Spring 使用这个 API 来进行数据验证
- 实现 Validator 接口
- 如果需要自定义验证逻辑,可以创建一个类实现
Validator
接口或ConstraintValidator
接口
- 如果需要自定义验证逻辑,可以创建一个类实现
- 在控制器中使用
@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 规范的一个流行实现,两者的概念以及区别:
- JPA:是 Java 持久化的规范,定义了 Java 对象和数据库表之间映射的标准方式,并提供了一套持久化对象的 API
- Hibernate:实现 JPA 规定的所有功能外,它还提供了许多超越 JPA 规范的特性。eg:高级缓存、自然ID等