JAVAWeb-Spring高级知识学习
Spring高级知识点
参考文章:https://mofan212.github.io/posts/Spring-Forty-Nine-Lectures-Container-And-Bean/
Spring容器
以 SpringBoot 的启动类为例:
1 |
|
其中,run()
方法存在返回值,返回 ConfigurableApplicationContext 容器
1 | ConfigurableApplicationContext context = SpringApplication.run(Application.class, args); |
ConfigurableApplicationContext 接口继承了 ApplicationContext 接口,而 ApplicationContext 接口又间接地继承了 BeanFactory 接口,除此之外还继承了其他很多接口,相当于对 BeanFactory 进行了拓展。
BeanFactory
- 是 ApplicationContext 的父接口
- 是 Spring 的核心容器,主要的 ApplicationContext 实现 组合 了它的功能,也就是说,BeanFactory 是 ApplicationContext 中的一个成员变量。
常用的 context.getBean(“xxx”) 方法,其实是调用了 BeanFactory 的 getBean() 方法。
其他方法:
1 | preInstantiateSingletons():预先初始化单例对象 |
基于它的子接口:
- ListableBeanFactory:提供获取 Bean 集合的能力,比如一个接口可能有多个实现,通过该接口下的方法就能获取某种类型的所有 Bean;
- HierarchicalBeanFactory:Hierarchical 意为“层次化”,通常表示一种具有层级结构的概念或组织方式,这种层次化结构可以通过父子关系来表示对象之间的关联,比如树、图、文件系统、组织架构等。根据该接口下的方法可知,能够获取到父容器,说明 BeanFactory 有父子容器概念;
- AutowireCapableBeanFactory:提供了创建 Bean、自动装配 Bean、属性填充、Bean 初始化、依赖注入等能力,比如 @Autowired 注解的底层实现就依赖于该接口的 resolveDependency() 方法;
- ConfigurableBeanFactory:该接口并未直接继承至 BeanFactory,而是继承了 HierarchicalBeanFactory。
BeanFactory不会:
- 主动调用 BeanFactory 后置处理器;
- 主动添加 Bean 后置处理器;
- 主动初始化单例对象;
- 解析 ${} 和 #{}
DefaultListableBeanFactory
- DefaultListableBeanFactory 实现了 BeanFactory 接口,它能管理 Spring 中所有的 Bean,当然也包含 Spring 容器中的那些单例对象。
- DefaultListableBeanFactory 还继承了 DefaultSingletonBeanRegistry 类,这个类就是用来管理 Spring 容器中的单例对象。
- 通过 DefaultListableBeanFactory#registerBeanDefinition 可以注册bean到容器中
BeanFactoryPostProcessor
BeanFactory后置处理器,典型的有
1 | org.springframework.context.annotation.internalConfigurationAnnotationProcessor |
比如:internalConfigurationAnnotationProcessor就是用来处理 @Configuration 和 @Bean 注解的,将配置类中定义的 Bean 信息补充到 BeanFactory 中。
BeanPostProcessor
Bean后置处理器,典型的有
1 | org.springframework.context.annotation.internalAutowiredAnnotationProcessor |
前者用于解析 @Autowired 注解,后者用于解析 @Resource 注解,它们都有一个共同的类型 BeanPostProcessor。
1 | ConfigurationClassPostProcessor.class |
用于解析 @ComponentScan @Bean @Import @ImportResource
1 | MapperScannerConfigurer.class |
用于解析 @MapperScan
DefaultSingletonBeanRegistry
用来管理 Spring 容器中的单例对象。
1 | private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); |
Map 的 key 就是 Bean 的名字,而 value 是对应的 Bean,即单例对象。
BeanDefinition
BeanDefinition 也是一个接口,它封装了 Bean 的定义,Spring 根据 Bean 的定义,就能创建出符合要求的 Bean。
读取 BeanDefinition 可以通过下列两种类完成:
- BeanDefinitionReader
- ClassPathBeanDefinitionScanner
BeanDefinitionReader
该接口中对 loadBeanDefinitions() 方法进行了多种重载,支持传入一个或多个 Resource 对象、资源位置来加载 BeanDefinition。
它有一系列相关实现,比如:
- XmlBeanDefinitionReader:通过读取 XML 文件来加载;
- PropertiesBeanDefinitionReader:通过读取 properties 文件来加载,此类已经被 @Deprecated 注解标记;
除此之外,还有一个 AnnotatedBeanDefinitionReader,尽管它并不是 BeanDefinition 的子类,但它们俩长得很像,根据其类注释可知:它能够通过编程的方式对 Bean 进行注册,是 ClassPathBeanDefinitionScanner 的替代方案,能读取通过注解定义的 Bean。
ClassPathBeanDefinitionScanner
通过扫描指定包路径下的 @Component 及其派生注解来注册 Bean,是 @ComponentScan 注解的底层实现。
比如 MyBatis 通过继承 ClassPathBeanDefinitionScanner 实现通过 @MapperScan 注解来扫描指定包下的 Mapper 接口。
BeanDefinitionRegistry
AnnotatedBeanDefinitionReader 和 ClassPathBeanDefinitionScanner 中都有一个 BeanDefinitionRegistry 类型的成员变量,它是一个接口,提供了 BeanDefinition 的增加、删除和查找功能。
ApplicationContext
ApplicationContext 除了继承 BeanFactory 外,还继承了:
- MessageSource:使其具备处理国际化资源的能力
- ResourcePatternResolver:使其具备使用通配符进行资源匹配的能力
- EnvironmentCapable:使其具备读取 Spring 环境信息、配置文件信息的能力
- ApplicationEventPublisher:使其具备发布事件的能力
- ListableBeanFactory:提供了获取某种类型的 Bean 集合的能力
- HierarchicalBeanFactory:提供了获取父容器的能力
虽然 ApplicationContext 继承了很多接口,但这些能力的实现是通过一种委派(Delegate)的方式实现的,这种方式也被叫做委派模式。
委派模式:实现获取资源的方式并不是由实现类自身完成,而是交给其内部的一个成员变量完成,这样的方式就是委派(这和对象适配器模式很相似)。在日常编码遇到这样的实现逻辑时,类名可以以 Delegate 结尾。
ApplicationContext的相关实现
- ClassPathXmlApplicationContext:基于 classpath 下的 xml 格式的配置文件来创建Bean
- FileSystemXmlApplicationContext:基于磁盘路径下 xml 格式的配置文件来创建Bean
本质都是利用 XmlBeanDefinitionReader#loadBeanDefinitions 加载Bean
- AnnotationConfigApplicationContext:基于 Java 配置类来创建
- AnnotationConfigServletWebServerApplicationContext:基于 Java 配置类来创建,用于 web 环境
ConfigurableApplicationContext
ApplicationContext 有一个子接口 ConfigurableApplicationContext,从类名就可以看出,它提供了对 ApplicationContext 进行配置的能力,浏览其内部方法可知,提供了诸如设置父容器、设置 Environment 等能力。
AbstractApplicationContext
ApplicationContext 有一个非常重要的抽象实现 AbstractApplicationContext,其他具体实现都会继承这个抽象实现,在其内部通过委派的方式实现了一些接口的能力,除此之外还有一个与 Spring Bean 的生命周期息息相关的方法:refresh()。
Bean
生命周期
初始化和销毁 Bean 的实现有三种:
- 依赖于后置处理器提供的拓展功能
- 相关接口的功能
- 使用 @Bean 注解中的属性进行指定
当同时存在以上三种方式时,它们的执行顺序也将按照上述顺序进行执行。
通过实现以下 BeanPostProcessor 接口,可以增强 Bean
- InstantiationAwareBeanPostProcessor
- DestructionAwareBeanPostProcessor
1 |
|
设计模式
为什么实现了 BeanPostProcessor 接口后就能够在 Bean 生命周期的各个阶段进行拓展呢?
因为使用了模板方法设计模式。
1 | static class MyBeanFactory { |
@ConfigurationProperties 注解
使用 @ConfigurationProperties 可以指定配置信息的前缀,使得配置信息的读取更加简单。
AutowiredAnnotationBeanPostProcessor
用于解析 @Autowired 和 @Value 注解
AutowiredAnnotationBeanPostProcessor#postProcessProperties()方法
1 |
|
其中的 findAutowiringMetadata() 用于查找指定的 bean 对象中哪些地方使用了 @Autowired、@Value 等与注入相关的注解,并将这些信息封装在 InjectionMetadata 对象中,之后调用其 inject() 方法利用反射完成注入。
InjectionMetadata 对象中有一个名为 injectedElements 的集合类型成员变量,根据上图所示,injectedElements 存储了被相关注解标记的成员变量、方法的信息,因为 Bean1 中的 bean3 成员变量、setBean2() 和 setHome() 方法恰好被 @Autowired 注解标记。
Scope
Scope 用于指定 Bean 的作用范围,有如下五个取值:
- singleton:单例(默认值)。容器启动时创建(未设置延迟),容器关闭时销毁
- prototype:多例。每次使用时创建,不会自动销毁,需要调用 DefaultListableBeanFactory#destroyBean() 进行销毁
- request:作用于 Web 应用的请求范围。每次请求用到此 Bean 时创建,请求结束时销毁
- session:作用于 Web 应用的会话范围。每个会话用到此 Bean 时创建,会话结束时销毁
- application:作用于 Web 应用的 ServletContext。Web 容器用到此 Bean 时创建,容器关闭时销毁
application 的作用范围是 ServletContext,要想 application scope 发生变化可以重启程序。
Aware 接口
Aware 接口用于注入一些与容器相关的信息,比如:
- BeanNameAware 注入 Bean 的名字
- BeanFactoryAware 注入 BeanFactory 容器
- ApplicationContextAware 注入 ApplicationContext 容器
- EmbeddedValueResolverAware 解析 ${}
- Aware 接口提供了一种 内置 的注入手段,可以注入 BeanFactory、ApplicationContext;
- InitializingBean 接口提供了一种 内置 的初始化手段;
- 内置的注入和初始化不受拓展功能的影响,总会被执行,因此 Spring 框架内部的类总是使用这些接口。
Aware 相关接口
BeanNameAware, ApplicationContextAware
1 | /** |
InitializingBean
1 |
|
当同时实现 Aware 接口和 InitializingBean 接口时,会先执行 Aware 接口。
BeanFactoryAware 、ApplicationContextAware 和 EmbeddedValueResolverAware 三个接口的功能可以使用 @Autowired 注解实现,InitializingBean 接口的功能也可以使用 @PostConstruct 注解实现,为什么还要使用接口呢?
为何有这些接口
@Autowired 和 @PostConstruct 注解的解析需要使用 Bean 后置处理器,属于拓展功能,而这些接口属于内置功能,不加任何拓展 Spring 就能识别。在某些情况下,拓展功能会失效,而内容功能不会失效。
对于 context.refresh(); 方法来说,它主要按照以下顺序干了三件事:
- 执行 BeanFactory 后置处理器;
- 添加 Bean 后置处理器;
- 创建和初始化单例对象。
失效场景
当 Java 配置类中定义了BeanFactoryPostProcessor 时,如果要创建配置类中的 BeanFactoryPostProcessor 就必须 提前 创建和初始化 Java 配置类。
在创建和初始化 Java 配置类时,由于 BeanPostProcessor 还未准备好,无法解析配置类中的 @Autowired 等注解,导致 @Autowired 等注解失效:
具体场景参考博客。
如果一个单例对象的成员变量是多例,怎么办才能在getBean的时候,获取的成员变量是多例的
eg:
1 | // class1 |
原因:对于单例对象来说,依赖注入仅发生了一次,后续不会再注入其他的 f1,因此 e 始终使用的是第一次注入的 f1
解决:
- 可以使用 @Lazy 注解,因为 @Lazy 生成的是代理对象,虽然代理对象依旧是同一个,但每次使用代理对象中的方法时,会由代理对象创建新的目标对象
- 其他推荐方式(Object工程、Context容器)参考博客
AOP
前置基础
- aop的实现方式(除jdk、cglib外的实现方式)
- aop源码、原理
以上参考博客(9-13):
https://mofan212.github.io/posts/Spring-Forty-Nine-Lectures-AOP/
Pointcut(切点)
在 Spring 中,切点通过接口 org.springframework.aop.Pointcut 来表示
1 | public interface Pointcut { |
Pointcut 接口有很多实现类,比如:
- AnnotationMatchingPointcut:通过注解进行匹配
- AspectJExpressionPointcut:通过 AspectJ 表达式进行匹配(本节的选择)
- StaticMethodMatcherPointcut:通过注解进行匹配(视频中匹配 @Transactional 时使用)
无论是 AspectJExpressionPointcut 还是 StaticMethodMatcherPointcut,它们都实现了 MethodMatcher 接口,用来执行方法的匹配。
AspectJExpressionPointcut
判断编写的 AspectJ 表达式是否与某一方法匹配可以使用其 matches() 方法。
StaticMethodMatcherPointcut
@Transactional 是 Spring 中使用频率非常高的注解,那它底层是通过 AspectJExpressionPointcut 与 @annotation() 切点表达式相结合对目标方法进行匹配的吗?
答案是否定的。@Transactional 注解除了可以作用在方法上,还可以作用在类(或接口)上。
在底层 @Transactional 注解的匹配使用到了 StaticMethodMatcherPointcut
Advice(通知)
MethodInterceptor:这个接口实现的通知属于环绕通知。
Aspect(切面)
DefaultPointcutAdvisor:创建这种切面时,传递一个节点和通知。
/*
多个切面:
aspect =
通知 1 (advice) + 切点 1(pointcut)
通知 2 (advice) + 切点 2(pointcut)
通知 3 (advice) + 切点 3(pointcut)
…
advisor = 更细粒度的切面,包含一个通知和切点
*/
ProxyFactory
Spring 是根据什么信息来选择不同的动态代理实现呢?
ProxyFactory 的父类 ProxyConfig 中有个名为 proxyTargetClass 的布尔类型成员变量:
- 当 proxyTargetClass == false,并且目标对象所在类实现了接口时,将选择 JDK 动态代理;
- 当 proxyTargetClass == false,但目标对象所在类未实现接口时,将选择 CGLib 动态代理;
- 当 proxyTargetClass == true,总是选择 CGLib 动态代理。
1 | // 设置实现的接口 |
ProxyFactory 是用来创建代理的核心实现,使用 AopProxyFactory 选择具体的代理实现:
- JdkDynamicAopProxy
- ObjenesisCglibAopProxy
AopProxyFactory 根据 proxyTargetClass 等设置选择 AopProxy 实现,AopProxy 通过 getProxy() 方法创建代理对象。
上述类图中的类与接口都实现了 Advised 接口,能够获得关联的切面集合与目标(实际上是从 ProxyFactory 中获取的)。
调用代理方法时,会借助 ProxyFactory 统一将通知转换为环绕通知 MethodInterceptor。。
AnnotationAwareAspectJAutoProxyCreator
Bean 后置处理器。尽管它的名称中没有 BeanPostProcessor 的字样,但它确实是实现了 BeanPostProcessor 接口的。
AnnotationAwareAspectJAutoProxyCreator 有两个主要作用:
- 找到容器中所有的切面,针对高级切面,将其转换为低级切面;
- 根据切面信息,利用 ProxyFactory 创建代理对象。
AnnotationAwareAspectJAutoProxyCreator 实现了 BeanPostProcessor,可以在 Bean 生命周期中的一些阶段对 Bean 进行拓展。AnnotationAwareAspectJAutoProxyCreator 可以在 Bean 进行 依赖注入之前、Bean 初始化之后 对 Bean 进行拓展。
重点介绍 AnnotationAwareAspectJAutoProxyCreator 中的两个方法:
- findEligibleAdvisors
- wrapIfNecessary
findEligibleAdvisors
位于父类 AbstractAdvisorAutoProxyCreator 中,用于找到符合条件的切面类。低级切面直接添加,高级切面转换为低级切面再添加。
findEligibleAdvisors() 方法接收两个参数:
- beanClass:配合切面使用的目标类 Class 信息
- beanName:当前被代理的 Bean 的名称
wrapIfNecessary
wrapIfNecessary() 方法内部调用了 findEligibleAdvisors() 方法,若 findEligibleAdvisors() 方法返回的集合不为空,则表示需要创建代理对象。
如果需要创建对象,wrapIfNecessary() 方法返回的是代理对象,否则仍然是原对象。
wrapIfNecessary() 方法接收三个参数:
- bean:原始 Bean 实例
- beanName:Bean 的名称
- cacheKey:用于元数据访问的缓存 key
@Order
根据上述打印的信息可知,低级切面相比于高级切面先一步被执行,这个执行顺序是可以被控制的。
针对高级切面来说,可以在类上使用 @Order
注解
在高级切面中,@Order 只有放在类上才生效,放在方法上不会生效。比如高级切面中有多个前置通知,这些前置通知对应的方法上使用 @Order 注解是无法生效的。
针对低级切面,需要设置 advisor 的 order 值,而不是向高级切面那样使用 @Order 注解,使用 @Order 注解设置在 advisor3() 方法上是无用的。
代理对象创建时机
使用 AnnotationAwareAspectJAutoProxyCreator Bean 后置处理器创建代理对象的时机有以下两个选择:
- Bean 的依赖注入之前
- Bean 初始化完成之后
代理对象的创建时机:
- 无循环依赖时,在 Bean 初始化阶段之后创建;
- 有循环依赖时,在 Bean 实例化后、依赖注入之前创建,并将代理对象暂存于二级缓存。
Bean 的依赖注入阶段和初始化阶段不应该被增强,仍应被施加于原始对象。
高级切面转低级切面
调用 AnnotationAwareAspectJAutoProxyCreator 对象的 findEligibleAdvisors() 方法时,获取能配合目标 Class 使用的切面,最终返回 Advisor 列表。在搜索过程中,如果遇到高级切面,则会将其转换成低级切面。
以解析 @Before 注解为例:
1 | public static void main(String[] args) throws Throwable { |
@Before 标记的前置通知会被转换成原始的 AspectJMethodBeforeAdvice 形式,该对象包含了以下信息:
- 通知对应的方法信息
- 切点信息
- 通知对象如何创建,本例公用一个 Aspect 对象
注解 | 对应的原始通知类 |
---|---|
@Before | AspectJMethodBeforeAdvice |
@AfterReturning | AspectJAfterReturningAdvice |
@AfterThrowing | AspectJAfterThrowingAdvice |
@After | AspectJAfterAdvice |
@Around | AspectJAroundAdvice |
静态通知调用
详情见参考博客
动态通知调用
详情见参考博客
MVC
RequestMappingHandlerMapping
HandlerMapping,即处理器映射器,用于建立请求路径与控制器方法的映射关系。
RequestMappingHandlerMapping 是 HandlerMapping 的一种实现,根据类名可知,它是通过 @RequestMapping 注解来实现路径映射。
当 Spring 容器中没有 HandlerMapping 的实现时,尽管 DispatcherServlet 在初始化时会添加一些默认的实现,但这些实现不会交由 Spring 管理,而是作为 DispatcherServlet 的成员变量。
RequestMappingHandlerAdapter
RequestMappingHandlerAdapter 实现了 HandlerAdapter 接口,HandlerAdapter 用于执行控制器方法,而 RequestMapping 表明 RequestMappingHandlerAdapter 用于执行被 @RequestMapping 注解标记的控制器方法。
实现控制器方法的调用很简单,但如何将请求参数与方法参数相绑定的呢?
显然是需要解析 @RequestParam 注解。
Spring 支持许多种类的控制器方法参数,不同种类的参数使用不同的解析器,使用 RequestMappingHandlerAdapter 的 getArgumentResolvers() 方法获取所有参数解析器。
Spring 也支持许多种类的控制器方法返回值类型,使用 RequestMappingHandlerAdapter 的 getReturnValueHandlers() 方法获取所有返回值处理器。
参数解析器
@RequestParam
@RequestParam 注解的解析需要使用到 RequestParamMethodArgumentResolver 参数解析器。构造时需要两个参数:
- beanFactory:Bean 工厂对象。需要解析 ${} 时,就需要指定 Bean 工厂对象
- useDefaultResolution:布尔类型参数。为 false 表示只解析添加了 @RequestParam 注解的参数,为 true 针对未添加 @RequestParam 注解的参数也使用该参数解析器进行解析。
RequestParamMethodArgumentResolver 利用 resolveArgument() 方法完成参数的解析,该方法需要传递四个参数:
- parameter:参数对象
- mavContainer:ModelAndView 容器,用来存储中间的 Model 结果
- webRequest:由 ServletWebRequest 封装后的请求对象
- binderFactory:数据绑定工厂,用于完成对象绑定和类型转换,比如将字符串类型的 18 转换成整型
@PathVariable
@PathVariable 注解的解析需要使用到 PathVariableMethodArgumentResolver 参数解析器。构造时无需传入任何参数。
使用该解析器需要一个 Map 集合,该 Map 集合是 @RequestMapping 注解上指定的路径和实际 URL 路径进行匹配后,得到的路径上的参数与实际路径上的值的关系(获取这个 Map 并将其设置给 request 作用域由 HandlerMapping 完成)。
@RequestHeader
@RequestHeader 注解的解析需要使用到 RequestHeaderMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。
@CookieValue
@CookieValue 注解的解析需要使用到 ServletCookieValueMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。
@Value
@Value 注解的解析需要使用到 ExpressionValueMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。
HttpServletRequest
HttpServletRequest 类型的参数的解析需要使用到 ServletRequestMethodArgumentResolver 参数解析器。构造时无需传入任何参数。
ServletRequestMethodArgumentResolver 参数解析器不仅可以解析 HttpServletRequest 类型的参数,还支持许多其他类型的参数,其支持的参数类型可在 supportsParameter() 方法中看到:
@ModelAttribute
@ModelAttribute 注解的解析需要使用到 ServletModelAttributeMethodProcessor 参数解析器。构造时需要传入一个布尔类型的值。为 false 时,表示 @ModelAttribute 不是不必须的,即是必须的。
针对 @ModelAttribute(“abc”) User user1 和 User user2 两种参数来说,尽管后者没有使用 @ModelAttribute 注解,但它们使用的是同一种解析器。
添加两个 ServletModelAttributeMethodProcessor 参数解析器,先解析带 @ModelAttribute 注解的参数,再解析不带 @ModelAttribute 注解的参数。
通过 ServletModelAttributeMethodProcessor 解析得到的数据还会被存入 ModelAndViewContainer 中。存储的数据结构是一个 Map,其 key 为 @ModelAttribute 注解指定的 value 值,在未显式指定的情况下,默认为对象类型的首字母小写对应的字符串。
@RequestBody User user3 参数也被 ServletModelAttributeMethodProcessor 解析了,如果想使其数据通过 JSON 数据转换而来,则需要使用另一个参数解析器。
@RequestBody
@RequestBody 注解的解析需要使用到 RequestResponseBodyMethodProcessor 参数解析器。构造时需要传入一个消息转换器列表。
1 | 先添加解析 注解的解析器,再添加解析 注解的解析器,最后添加解析省略了 注解的解析器。如果更换最后两个解析器的顺序,那么 User user3 将会被 ServletModelAttributeMethodProcessor 解析,而不是 RequestResponseBodyMethodProcessor。 |
获取参数名
DefaultParameterNameDiscoverer
1 | public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer { |
需要获取参数名称的原因:(java编译的时候,如果不加-par-parameters 即不是【javac -parameters .\Bean2.java】编译的,则不会保留参数名称)
在项目的 src 目录外创建一个 Bean2.java 文件,使其不会被 IDEA 自动编译
1 | package indi.mofan.a22; |
将命令行切换到 Bean2.java 文件所在目录的位置,执行 javac .\Bean2.java 命令手动编译 Bean2.java。查看 Bean2.class 文件的内容
1 | package indi.mofan.a22; |
编译生成的 class 文件中的 foo() 方法的参数名称不再是 name 和 age,也就是说直接使用 javac 命令进行编译得到的字节码文件不会保存方法的参数名称。