二三事

“みんなみんな大好き!”

0%

Spring 框架学习研究札记

文前提示:如果移动端访问时未显示侧栏,可点击左侧按钮以查看侧栏目录。

这篇文档仅仅是我自己的笔记。比如,我会整理 Spring 框架常见的容器相关注解,但不会对容器注解本身做进一步解释,也不会解释 IoC 的概念,只起目录与导航的作用——因为这些内容的详情要么在官方文档中可以很轻松地查到,要么十分基础。但对于 Bean 的生命周期这类比较重要的关键点,我将重点分析并记录。

Spring Boot 3.3.3

Spring Framework 6.1.6

JDK 17

Spring Core & AOP

IoC & DI

控制反转(Inversion of Control)是一种设计原则,指对象的创建、销毁与依赖关系不再由用户负责,而是将对象的生命周期交由容器统一管理。

依赖注入(Dependency Injection)是控制反转的最常见实现方式。

Spring Bean 接口与注解

  • @Configuration

  • @ComponentScan@Import

  • @Bean

  • @Component

    • @Controller@RestController),@Service@Repository
  • @Scope

  • @Lazy

  • 接口 FactoryBean

  • @Conditional、接口 Conditional 及一系列派生注解

    • @Profile

    Spring Boot 如此强大,最主要的原因就是其在底层广泛而灵活地使用了 @Conditional。如果说最初基于 XML 配置的 Spring 设计哲学是配置优于代码,那么后来基于注解的 Spring 为我们大幅减少甚至几乎完全消灭了冗长繁琐的 XML 配置,而 Spring Boot 在这基础上更进一步,通过让约定优于配置,让一个刚入门的开发者也能够轻松上手。

  • @Autowired@Resource

    在源码中,@Autowired 实际上是由 AutowiredAnnotationBeanPostProcessor 实现的,这是一个专门用于处理 @Autowired 的一种特殊的 BeanPostProcessor,在每个 Bean 或组件被创建后调用其特殊的 postProcessProperties 方法或 postProcessBeforeInitialization 方法,通过反射分析目标 Bean 的全部注解、字段、方法、构造器等等,然后对具有 @Autowired 注解修饰的对象尝试在容器中进行匹配,如果匹配成功则注入依赖。

    @Autowired@Resource 均能实现自动的依赖注入,但 @Autowired 是 Spring 框架的实现,而 @Resource 是 Java 标准规范 JSR-250 所定义的。如果不存在其他注解,例如 @Qualifier,则 @Autowired 默认按类型匹配,类型不唯一时尝试按名称匹配。@Resource 则默认按名称匹配。

  • @Qualifier@Primary

  • 接口 ...Aware

  • @Value

  • @PropertySource

  • 接口 InitializingBeanDisposableBean

  • @PreConstruct@PreDestory

  • 接口 BeanPostProcessor

各注解的作用与具体用法请查询 Spring 官方文档

Spring Bean 生命周期

Bean 生命周期:

  1. 初始化容器自身,加载配置类、解析元数据,随后初始化、创建 Bean;
  2. 创建阶段,在创建的过程中容器调用被初始化 Bean 的构造器,在这一阶段中待被依赖对象进入运行阶段后,被 @Autowired@Resource 修饰的对象由容器为其进行依赖注入(这里可能触发递归初始化);
  3. 初始化阶段及其前置、后置拦截处理,优先初始化 BeanPostProcessor,接着是其他 Bean 与组件:
    • 一个配置类中若显式注册了被 @Bean 修饰、返回类型为 BeanPostProcessor 的方法,且该方法应返回一个重写了 postProcessBeforeInitialization 方法的 BeanPostProcessor 类的一个实例(注册至容器),则后续创建的所有 Bean 与组件均会在这一步调用 postProcessBeforeInitialization 方法,而且 BeanPostProcessor 方法自身虽然被 @Bean 修饰但会被容器特殊对待,不会被其他 BeanPostProcessor 处理;
    • 初始化阶段,流程为:
      1. 若返回类具有被 @PostConstruct 修饰的方法,则调用该方法;
      2. 若返回类实现了接口 InitializingBeanafterPropertiesSet 方法,则调用该方法;
      3. 调用 @Bean 注解的属性 initMethod 所指定的方法,该方法可以在其他类中;
    • 一个配置类中若显式注册了被 @Bean 修饰、返回类型为 BeanPostProcessor 的方法,且该方法应返回一个实现了 postProcessAfterInitialization 方法的 BeanPostProcessor 类的一个实例(注册至容器),则后续创建的所有 Bean 与组件均会在这一步调用 postProcessAfterInitialization 方法,而且 BeanPostProcessor 方法自身虽然被 @Bean 修饰但会被容器特殊对待,不会被其他 BeanPostProcessor 处理。
  4. 运行阶段;
  5. 销毁阶段:
    • 若返回类具有被 @PreDestory 修饰的方法,则调用该方法;
    • 若返回类实现了接口 DisposableBeandestroy 方法,则调用该方法;
    • 调用 @Bean 注解的属性 destroyMethod 所指定的方法,该方法可以在其他类中;
    • 销毁容器自身。

Component 生命周期:

  1. 初始化容器自身,加载配置类、解析元数据,随后初始化、创建组件;
  2. 创建阶段,在创建的过程中容器调用被初始化组件的构造器,在这一阶段中待被依赖对象进入运行阶段后,被 @Autowired@Resource 修饰的对象由容器为其进行依赖注入(这里可能触发递归初始化);
  3. 初始化阶段及其前置、后置拦截处理,优先初始化 BeanPostProcessor,接着是其他 Bean 与组件:
    • 一个组件若实现了接口 BeanPostProcessor 的方法 postProcessBeforeInitialization(注册至容器),则后续创建的所有 Bean 与组件均会在这一步调用 postProcessBeforeInitialization 方法;
    • 初始化阶段,流程为:
      1. 调用组件中被 @PostConstruct 修饰的方法;
      2. 调用被组件实现的接口 InitializingBeanafterPropertiesSet 方法;
    • 一个组件若实现了接口 BeanPostProcessor 的方法 postProcessAfterInitialization(注册至容器),则后续创建的所有 Bean 与组件均会在这一步调用 postProcessAfterInitialization 方法。
  4. 运行阶段;
  5. 销毁阶段:
    • 调用组件中被 @PreDestory 修饰的方法;
    • 调用被组件实现的接口 DisposableBeandestroy 方法;
    • 销毁容器自身。

Bean 与组件(Component)其实没有区别,均为容器中的对象,上文中做区分只是为区别注解 @Bean@Component 的使用方式。下文不再区分 Bean 与组件。

Spring Bean 默认均为单实例(Singleton),即在容器初始化完成后便存在于容器中,全局唯一,任何请求获得的都是同一个 Spring Bean。如果不考虑懒加载 @Lazy 的 Bean,则容器初始化完成后单实例 Bean 就已经存在于容器中了。反之,通过指定 @Scope("prototype") 可以让 Spring Bean 成为原型(Prototype),只有当请求者向容器请求 Bean 时容器才会通过工厂方法创建一个新对象并返回。

三级缓存模型

Spring 三级缓存的主要设计目的是为了解决(如果没有三级缓存)两个单实例 Spring Bean 一旦存在循环依赖则将造成无限递归调用的问题。实际上,如果不考虑 AOP,则只需要两级缓存并使用二级缓存存储早期引用即可,设计第三级缓存是为了支持 AOP 的动态代理特性,通过第三级缓存以延迟代理的生成,避免直接注入目标对象。

注意,Spring 默认不允许循环引用(自 Spring Boot 2.6+ 开始默认禁止),需要在配置文件中明确 spring.main.allow-circular-references=true 以允许循环依赖的存在。

Spring 的三级缓存,实际上是三个 ConcurrentHashMap,分别为

  1. 一级缓存 ConcurrentHashMap<String, Object> singletonObjects

    负责存储初始化完毕的单例 Spring Bean

  2. 二级缓存 ConcurrentHashMap<String, Object> earlySingletonObjects

    负责临时存放已实例化但未初始化的 Spring Bean,供依赖方使用

  3. 三级缓存 ConcurrentHashMap<String, ObjectFactory<?>> singletonFactories

    存储 Spring Bean 的工厂对象,用于生成早期引用

假设有两个需要初始化的单实例对象 A、B 且 A、B 间存在循环依赖,则在三级缓存机制下 Spring 将按下述流程初始化 A:

  1. Spring 启动并开始实例化 A

    • Spring 检测到 A 是单例 Bean,开始初始化。
    • 在实例化 A 后(调用构造方法),将 A 的原始对象(未填充属性)放入三级缓存。
  2. 发现 A 依赖 B,尝试注入 B

    • Spring 解析 A 的依赖,发现需要注入 B。
    • 检查一级缓存中是否存在 B,若不存在则开始初始化 B。
  3. 实例化 B 并发现其依赖 A

    • 实例化 B 后,将 B 的原始对象放入三级缓存。

    • 解析 B 的依赖时,发现需要注入 A,此时:

      • 检查一级缓存:不存在 A。

      • 检查二级缓存:不存在 A。

      • 对当前 Bean A 的名称加同步锁synchronized ("bean_name_of_A")),然后:

        • 再次检查二级缓存,通过双重检查锁避免并发时重复生成。

        • 从三级缓存获取 A 的 ObjectFactory,调用 getObject() 生成早期引用(可能是代理,也可能是原始对象)。

        • 将生成的早期引用放入二级缓存,并立即从三级缓存移除 A 的 ObjectFactory

      • 释放同步锁。

  4. 将 A 的早期引用注入 B

    • B 成功获得 A 的早期引用,继续完成 B 的初始化(填充其他属性、执行 @PostConstruct 等)。
    • 初始化完成后,将完全初始化的 B 放入一级缓存,如果二级缓存与三级缓存中存在 B,则移除二级缓存与三级缓存中的 B。
  5. 返回并完成 A 的初始化

    • 此时 A 的依赖 B 已存在于一级缓存中,Spring 将 B 注入 A。
    • 继续完成 A 的初始化(填充其他属性、执行 @PostConstruct)。
    • 将 A 放入一级缓存,如果二级缓存中存在 A,则在二级缓存中移除 A。

上述描述的流程可以在 Spring 源码中得到验证。


在施加同步锁逻辑的源码中有一个有趣的细节——双检查锁机制。类 org.springframework.beans.factory.support.DefaultSingletonBeanRegistrygetSingleton 方法正是产生单实例 Bean 逻辑的关键方法,反编译结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized(this.singletonObjects) {
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}

return singletonObject;
}

将以上反编译源码以更接近自然语言的 Python 伪代码改写,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def getSingleton(beanName, allowEarlyReference):
# 第一次检查:常规单例缓存
bean = cacheFirst.get(beanName)
# isCreating() 检查 Bean 是否处于生命周期的创建阶段
if bean is None and isCreating(beanName):
# 第二次检查:早期单例缓存
bean = cacheSecond.get(beanName)
if bean is None and allowEarlyReference:
with synchronized(cacheFirst):
# 第三次检查:同步块内再次检查常规缓存
bean = cacheFirst.get(beanName)
if bean is None:
# 第四次检查:同步块内再次检查早期缓存
bean = cacheSecond.get(beanName)
if bean is None:
# 从工厂获取实例(第三级缓存)
factory = cacheThird.get(beanName)
if factory is not None:
bean = factory.create()
cacheSecond.put(beanName, bean)
cacheThird.remove(beanName)
return bean

不难看出,由于三级缓存的存在,双检查锁机制实际上在加锁的前后一共进行了四重检查,分别为两次常规检查与两次早期检查。通过双检查锁机制,保证了并发系统只会有唯一一个线程负责创建 Bean,避免重复。

IoC 容器初始化流程

十分推荐:Spring详解(五)——Spring IOC容器的初始化过程

AOP

面向切面编程(AOP,Aspect Oriented Programming)是面向对象编程(OOP,Object Oriented Programming)的补充,以解决横切关注点代码重复冗余的问题,例如日志系统与事务系统。

动态代理

面向切面编程的根基是动态代理(Dynamic Proxy)——有别于硬编码与静态代理。动态代理在底层则是依靠类似拦截器的机制(通常是回调)实现的,接口的动态代理由 JDK 利用反射在运行时提供原生支持(利用方法 Proxy.newProxyInstanceInvocationHandler.invoke),类的动态代理由 CGLIB 通过字节码在运行时提供支持。

在 Spring 中并不需要用户自己实现代理,用户只需要定义切面(Aspect)的通知方法(Advice)。对象方法在纵向执行自身的业务逻辑时,将在不同的阶段暴露出若干连接点(Join Point),例如方法调用前、方法返回后、抛出异常时与方法结束后。切面在通知方法中定义具体的增强逻辑,利用注解为通知方法绑定我们所感兴趣的一部分连接点,即切入点(Pointcut),同时通过切入点表达式(Pointcut Expression)匹配目标方法,进而确定 AOP 框架需要在哪些连接点处进行拦截。随后,AOP 框架将在运行时(动态代理)或编译时(字节码增强)将切面中定义的增强逻辑织入(Weave)到目标方法中(对 Spring AOP 而言,主要是运行时的动态代理),最终实现横切关注点(Cross-Cutting Concerns)与核心业务逻辑的解耦。

在 Spring AOP 的底层实现中,容器将为被切面切入的组件创建动态代理对象,代理对象中保存了由底层的 AOP 代理工厂 ProxyFactory 自动生成的增强器调用链——增强器链。增强器(Advisor)是 Spring 进一步封装通知方法的产物,增强器中除了增强逻辑以外还包含了通知方法何时何处生效的信息。增强器链(Advisor Chain)则是用以存放增强器的集合。目标方法被调用时,将按顺序执行增强器链中的增强器所蕴含的通知方法。

Spring AOP 切面注解

切面类通过注解 @Aspect 显式标记。由于切面类也应是容器中的一个组件,所以注解 @Aspect 一般需要配合注解 @Component 使用。

切面类具有若干通知方法,这些通知方法中定义了具体需要被织入的逻辑。除环绕切面 @Around 外,通知方法要么无形参,要么只能有 JoinPoint 类型的形参,要么允许其他类型形参的存在但其他类型的形参必须被注入或绑定(例如 @AfterReturning 通过属性 returning 将返回值注入至同名形参中)。JoinPoint 类型的形参封装了当前通知方法被调用时关于调用的全部信息。返回值、通知方法名称通常是无关紧要的。

通知方法的通知类型注解决定通知方法何时生效,切面表达式决定通知方法何处生效。

常见的通知类型注解:

  • @Before
  • @AfterReturning
  • @AfterThrowing
  • @After

修饰空方法的通知类型注解,仅用作抽取切面表达式:

  • @Pointcut,其他注解通过 value = "空方法名称()" 复用切面表达式

环绕通知注解:

  • @Around

切面表达式以字符串的形式作为通知类型注解的属性而发挥作用,切面表达式的匹配类型包括

  1. execution
  2. within
  3. this
  4. target
  5. args
  6. @args
  7. @annotation
  8. bean

切面表达式的不同匹配类型间可以通过 Java 的逻辑运算符连接,例如 &&||。以匹配类型 execution 为例,execution 表达式的格式为

1
2
3
4
5
execution(
[modifier-pattern]
return-type-pattern
[method-signature] / name-pattern (param-pattern)
[throws-pattern])

例如,一个完整的切面类的通知方法的注解为 @Before("execution(public long com.arth.picloud.service.impl.UserServiceImpl.userRegister(String, String, String)) throws BackendException"),常简写为 @Before("execution(long userRegister(String, String, String))")

切面表达式支持通配符 * 与任意参数 ..,例如 @Before("execution(* *(*, String, ..))")。如果将方法签名仅使用一个通配符 * 替代很可能将匹配到意料之外的底层方法从而导致错误,常见的用法是对指定包名下的所有方法使用通配符,例如 @Before("execution(* com.arth.picloud.service.impl.UserServiceImpl.*(..))")


多个切面默认按名称字典序决定调用顺序。配合 @Aspect 使用注解 @Order(n) 指定优先级,n 越小优先级越高。即使是多个切面,Spring AOP 也只会生成一个完整的增强器调用链。


环绕切面 @Around 是 Spring AOP 中最为灵活与强大的一种通知类型。相比于其他通知类型仅起到感知的作用,它允许我们在目标方法执行前后完全控制其行为(不单单是执行前或执行后,这是“环绕”最直接的体现),甚至可以阻止方法的执行或修改方法返回值,因此有必要单独在此整理环绕切面的内容。实际上,环绕通知在底层也是通过动态代理实现的。

被环绕通知注解 @Around 修饰的通知方法必须有 ProceedingJoinPoint 类型的形参(可继续执行目标方法的切点),而且返回值类型应当是 Object。记 ProceedingJoinPoint 类型的形参变量名为 pjppjp 有如下常用的方法:

  • pjp.getArgs(),返回目标方法的参数数组,返回类型为 Object[]
  • pjp.proceed(args),继续执行目标方法,返回目标方法的返回值

环绕通知方法的返回值将被视为动态代理的返回值。

Spring 事务管理

相比编程式的事务管理,Spring 框架更倾向于让开发者使用声明式的事务管理。

Spring 事务注解

使用注解 @EnableTransactionManagement 显式启用基于注解的自动化事务管理,为需要事务功能的类或方法添加注解 @Transactional 即可使用自动化的事务管理功能。

注解 @Transactional 的常用属性:

  • timeout,属性值类型为 int,默认 -1(不启用),计时范围为从事务获取数据库连接开始至最后一次数据库操作结束或触发回滚时。
  • readOnly,属性值类型为 boolean,默认 false,置为 true 以提示框架与数据库应进行只读优化。例如,MyBatis 将对只读操作跳过缓存刷新逻辑,MySQL 将减少锁开销且不会为只读操作分配事务 ID。因此,建议对简单单次 SELECT 以外的查询操作均显式添加注解 @Transactional(readOnly=true)

Spring 默认对非受检异常(Unchecked Exception,不可预期的程序错误,范围为 RuntimeExceptionError 及二者的子类)触发回滚,对受检异常(Checked Exception,可预期的业务逻辑错误,包括 IOException 等编译时已声明或处理过的异常)不触发回滚但仍提交事务。

  • rollbackFor,属性值类型为 Throwable 及其子类的字节码对象数组,出现指定异常时触发回滚。
    • rollbackForClassName,属性值类型为 String 数组,应填入全类名字符串,用法同上。
  • noRollbackFor,属性值类型为 Throwable 及其子类的字节码对象数组,出现指定异常时不触发回滚。
    • noRollbackForClassName,属性值类型为 String 数组,应填入全类名字符串,用法同上。

隔离级别注解

Spring 支持为每个事务指定隔离级别,只需要为 isolation 属性赋值相应 Isolation 的枚举类即可。一共有五个可选的枚举类:

  • Isolation.DEFAULT
  • Isolation.READ_UNCOMMITTED
  • Isolation.READ_COMMITTED
  • Isolation.REPEATABLE_READ
  • Isolation.SERIALIZABLE

传播行为注解

Spring 还支持为每个事务指定传播行为,通过为 propagation 属性赋值相应 Propagation 的枚举类以指定。

Propagation 枚举类 传播行为解释
Propagation.REQUIRED 需要当前事务,若当前存在事务则并入,若当前无事务则新建事务。

此为默认的事务传播行为。
Propagation.SUPPORTS 支持当前事务,若当前存在事务则并入,若当前无事务则无事务地执行。
Propagation.MANDATORY 强制支持当前事务,若当前存在事务则并入,若当前无事务则抛出异常。
Propagation.REQUIRES_NEW 新建事务并执行,若当前存在事务则挂起事务,优先执行新事务。
Propagation.NOT_SUPPORTED 无事务地执行,若当前存在事务则挂起事务,优先无事务执行。
Propagation.NEVER 强制无事务地执行,若当前存在事务则抛出异常。
Propagation.NESTED 如果当前存在事务则嵌套执行事务,若无事务则新建事务。

嵌套事务的特点是在外层事务的上下文中执行并通过保存点实现部分回滚。若外层事务不存在,则行为与 Propagation.REQUIRED 相同。该传播行为特性要求数据库的存储引擎支持保存点,否则任何情况下与 Propagation.REQUIRED 完全相同。

在传播机制下,

  • 事务合并时,外层事务除超时时间以外的设置将覆盖内层事务的设置,即使外层事务的部分设置为未显式声明的默认设置。针对超时时间设置,Spring 将取外层事务与内层事务的超时时间的最小值作为合并后事务的超时时间;
  • 新建事务时,内层事务的设置将完全独立生效。

Spring 事务的原理

具体如何实现回滚由各数据库与缓存负责,Spring 框架仅定义抽象的逻辑,例如何时开启一个事务、何时触发回滚。Spring 依靠事务管理器 PlatformTransactionManager 与事务拦截器 TransactionInterceptor 实现回滚——这两个对象也隶属于容器,本质上均为 Spring Bean。

PlatformTransactionManager 是 Spring 事务抽象的核心顶层接口,不同的数据访问技术有该接口的不同实现。例如 JDBC 默认使用实现类 DataSourceTransactionManager 作为事务管理器的实现,其中定义了 commitgetTransactionrollback 三个方法。也就是说,Spring 通过事务管理器定义事务的行为。

TransactionInterceptor 是一个编程式实现的切面,能够感知方法的调用时机,能够控制事务创建、提交与回滚的时机。具体而言,切面类 TransactionInterceptor 继承自父类 TransactionAspectSupport 的方法 completeTransactionAfterThrowing(txInfo, ex)commitTransactionAfterReturning(txInfo),前者负责感知回滚时机并触发回滚,后者负责感知事务提交时机并触发提交。

Spring MVC

Spring MVC 是 Spring 框架的 Web 模块,负责处理 HTTP 请求与响应。Spring MVC 基于 Servlet API,运行在 Servlet 容器上,Spring Boot 默认集成了轻量级开源 Web 服务器、Servlet 容器 Tomcat,使 Spring MVC 开发更加简便。

## 基本请求与响应

  • @Controller

  • @ResponseBody

    为控制层组件或组件内的方法添加注解 @ResponseBody 后,Spring MVC 将把返回值自动转换为响应体,否则视为传统的视图路径——在如今广泛采取前后端分离的开发模式下,这种用法已经十分少见了。

    @RequestMapping 的属性 produces 中可进一步指定响应体的类型,Spring Boot 默认为 JSON。

  • @RequestBody

  • @ResController

  • @RequestMapping

    • @GetMapping@PostMapping@PutMapping ...
  • @RequestParam

    除基本用法外(Key-Value 数据与 JSON 数据),形参类型 MultipartFileMultipartFile[] 往往需要结合 @RequestParam 使用。

  • @RequestHeader

  • @CookieValue

  • @PathVariable

  • @JsonFormat(Jackson,序列化与反序列化,伴随潜在的格式转换) / @DateTimeFormat(Spring,转换与绑定)

    二者均可以通过特殊的配置类实现全局的默认日期格式处理方案

  • 后端声明跨域访问规则注解 @CrossOrigin

  • 方法的形参类型 MultipartFile

  • 方法的形参类型 HttpEntity<T>

  • 传统 Servlet API 类型均可作为方法形参并传入值, 例如 HttpServletRequestHttpServletResponseHttpSession 等。

  • 响应文件下载的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @RestController
    public class FileDownloadController {

    @GetMapping("/download")
    public ResponseEntity<Resource> downloadFile() {
    try {
    String fileName = "心做し.flac";
    File file = Paths.get(fileName).toAbsolutePath().toFile();
    String encodedName = UriUtils.encode(fileName, UTF_8);

    if (!file.exists()) return ResponseEntity.notFound().build();

    return ResponseEntity.ok()
    .contentType(MediaType.APPLICATION_OCTET_STREAM)
    .contentLength(file.length())
    .header(HttpHeaders.ACCEPT_RANGES, "bytes")
    .header(HttpHeaders.CONTENT_DISPOSITION,
    "attachment; filename*=UTF-8''" + encodedName)
    .body(new FileSystemResource(file));
    } catch (Exception e) {
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
    }
    }
  • 如果存在服务端渲染的需求,Spring Boot 推荐使用 Thymeleaf 作为 Spring MVC 的模板引擎,尽管在前后端分离的设计模式下该需求已较为罕见(直接或间接通过 ModelAndView 由后端动态生成前端内容)。

各注解、参数与模板的作用与具体用法请查询 Spring 官方文档。Spring MVC 对数据类型的支持是十分广泛的,但使用频繁的只有极小一部分。

拦截器、异常处理与数据校验

拦截器

拦截器与动态代理和 Spring AOP 均没有关系,拦截器的逻辑硬编码在 MVC 中,专门为 Controller 的请求处理流程提供拦截点。拦截器用于在 MVC 体系下取代 Servlet API 的 Filter,更加契合 Spring 框架。

一个完整的拦截器定义如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class ThisIsAHandlerInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 在 Controller 执行前调用,返回 true 则放行请求,false 则中断请求
return condition;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 在 Controller 执行完毕但视图未渲染时调用,若启用该方法常用于修改 ModelAndView,使用频率极低
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 在整个请求完成后、视图已渲染时调用,无论 Controller 是否抛出异常,一般用于资源清理与日志记录
}
}

拦截器的执行顺序如下所示:

HandlerInterceptor执行顺序图示

定义拦截器后,要使拦截器生效,还需要对拦截器进行配置。一个基本的拦截器配置如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
class ThisIsAMvcConfiguration implements WebMvcConfigurer {

@Resource
private HandlerInterceptor thisIsAHandlerInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(ThisIsAHandlerInterceptor)
.addPathPatterns("/**"); // 路由 "/**" 拦截所有请求
}
}

异常处理

相较于经典的 try-catch 与 throw 的编程式异常处理,Spring MVC 更推荐声明式异常处理。

  • @ExceptionHandler(Class<? extends Exception>)@ExceptionHandler(Class<? extends Exception>[])

    修饰一个 Controller 的方法,若该 Controller 未被 @ControllerAdvice 修饰,则该方法将捕获并处理 Controller 中业务方法所抛出的特定异常。

  • @ControllerAdvice

    修饰一个类,使该类成为 Spring MVC 全局异常处理器组件。任何 Controller 所抛出的特定异常均将被全局异常处理器中 @ExceptionHandler 所修饰的特定方法处理。精确异常处理优先。

    • @ResControllerAdvice

如果抛出的异常没有被任何异常处理器处理,Spring Boot 将自适应地处理该异常,按照客户端类型返回相应格式的错误信息。


数据校验

数据校验注解应当修饰 POJO 的字段以通知 Spring MVC 在合适的时机进行相应的数据校验。更准确地说,此处 POJO 扮演的是 DTO 或 VO 的角色。

  • @Null@NotNull
  • @NotEmpty
  • @NotBlank
  • @AssertTrue@AssertFalse
  • @Max@Min@DecimalMax@DecimalMin
  • @Size
  • @Digits
  • @Email
  • @Future@Past
  • @Pattern(正则表达式校验)

POJO 作为 Controller 方法的参数时,使用注解 @Valid@Validated 以通知 Spring MVC 针对该方法的 POJO 接收参数应当按照给定的规则进行数据校验。

Java 标准 JSR-303 规定了注解 @Valid,使用注解 @Valid 后可以在 Controller 方法的参数中添加类型为 BindingResult 的形参,该参数封装了 @Valid 数据校验的结果。更常见的做法是使用全局异常处理器捕获并统一处理参数校验异常 org.springframework.web.bind.MethodArgumentNotValidException,该异常能够通过 getBindingResult() 方法获得相应的 BindingResult(而不是在 Controller 的方法的形参中显式给定 BindingResult),进而更高效地获取并处理错误信息。

@Validated 是 Spring 提供的扩展注解,在 @Valid 的基础上额外支持分组校验。

只要遵从规范,我们可以轻松地实现一个可用的自定义数据校验注解。不过,数据校验的逻辑实际上由校验器负责执行,因此要自定义数据校验注解,需要实现相应的自定义校验器。

以下为一个标准的自定义数据校验注解示例,除了校验器不同以外,同其他数据校验注解没有任何本质上的不:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Documented
@Constraint(validateBy = {ExtensionValidator.class})
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckExtension {

String message() default "Not Support Extension";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

}

以下是相应的自定义校验器:

1
2
3
4
5
6
7
public class ExtensionValidator implements ConstraintValidator<CheckExtension, String> {

@Override
public boolean isValid(String value, ConstraintValidatorContext conetxt) {
return value != null && Pattern.matches(".*\\.(mp3|flac|m4a)$", value);
}
}

DispatcherServlet

待补充……

Spring MVC 流程

待补充……

Spring Boot

嗯……如果 Spring Framework 是一堆未组装的汽车零件,比如发动机和变速器,那 Spring Boot 大概就是一辆装配了自动驾驶系统、开箱即用的电动汽车。自动配置(简化 XML 配置、约定大于配置)、内嵌 Tomcat、starter 场景启动器等等特性,大幅提升开发效率。

待补充……

这里有一份实现了(且只实现了)用户模块功能的 Spring Boot 项目源码,同时包含了用户模块测试用的 Vue 前端。如有需要,可以在此下载