1. 容器接口
以 SpringBoot 的启动类为例:
1 2 3 4 5 6 7 @Slf4j @SpringBootApplication public class A01Application { public static void main (String[] args) { SpringApplication.run(A01Application.class, args); } }
其中的 run()
方法是有返回值的:
1 ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args);
在 IDEA 中使用快捷键 Ctrl + Alt + U
查看 ConfigurableApplicationContext
类的类图:
ConfigurableApplicationContext
接口继承了 ApplicationContext
接口,而 ApplicationContext
接口又间接地继承了 BeanFactory
接口,除此之外还继承了其他很多接口,相当于对 BeanFactory
进行了拓展。
1.1 什么是 BeanFactory
它是 ApplicationContext
的父接口
它才是 Spring 的核心容器,主要的 ApplicationContext
实现 组合 了它的功能,也就是说,BeanFactory
是 ApplicationContext
中的一个成员变量。
常用的context.getBean("xxx")
方法,其实是调用了 BeanFactory
的 getBean()
方法。
1.2 BeanFactory 能做什么
进入BeanFactory
接口,在 IDEA 中使用快捷键Ctrl+F12
查看这个接口中的所有方法定义:
通过这些方法定义可知,BeanFactory
表面上只有getBean()
方法,实际上控制反转、基本的依赖注入、乃至 Bean 的生命周期的各种功能,都由它的实现类提供。
查看 DefaultListableBeanFactory
类的类图:
DefaultListableBeanFactory
实现了 BeanFactory
接口,它能管理 Spring 中所有的 Bean,当然也包含 Spring 容器中的那些单例对象。
DefaultListableBeanFactory
还继承了 DefaultSingletonBeanRegistry
类,这个类就是用来管理 Spring 容器中的单例对象。
在 IDEA 提供的类图中选中 DefaultSingletonBeanRegistry
,然后按下F4
进入这个类。它有一个Map
类型的成员变量singletonObjects
:
1 private final Map<String, Object> singletonObjects = new ConcurrentHashMap <>(256 );
Map
的 key 就是 Bean 的名字,而 value 是对应的 Bean,即单例对象。
创建两个类
1 2 3 4 5 6 7 8 @Component public class ComponentA {} @Component public class ComponentB {}
查看singletonObjects
中是否存在两个 bean 的信息。
1 2 3 4 5 6 7 8 9 10 11 12 @SpringBootApplication public class A01Application { public static void main (String[] args) throws Exception { ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args); Field singletonObjects = DefaultSingletonBeanRegistry.class.getDeclaredField("singletonObjects" ); singletonObjects.setAccessible(true ); Map<String, Object> map = (Map<String, Object>) singletonObjects.get(context.getBeanFactory()); map.entrySet().stream() .filter(entry -> entry.getKey().startsWith("component" )) .forEach(entry -> System.out.println(entry.getKey() + " ==> " + entry.getValue())); } }
运行 main()方法后,控制台打印出:
componentA ==> com.itheima.a01.ComponentA@10c72a6f
componentB ==> com.itheima.a01.ComponentB@70e94ecb
1.3 ApplicationContext 的功能
回顾ApplicationContext
的类图:
它的拓展功能主要体现在继承的 4 个父接口上
MessageSource:使其具备处理国际化资源的能力
ResourcePatternResolver:使其具备使用通配符进行资源匹配的能力
EnvironmentCapable:使其具备读取 Spring 环境信息、配置文件信息的能力
ApplicationEventPublisher:使其具备发布事件的能力
MessageSource
读取一个 Key 翻译后的结果
在 SpringBoot 项目的 resources 目录下创建 messages.properties、messages_en.properties、messages_zh_CN.properties 三个国际化文件
测试 MessageSource
接口中 getMessage()
方法的使用:
1 2 3 4 5 public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args); System.out.println(context.getMessage("thanks" , null , Locale.ENGLISH)); System.out.println(context.getMessage("thanks" , null , Locale.SIMPLIFIED_CHINESE)); }
运行 main() 方法后,控制台打印出:
Thank you
谢谢
ResourcePatternResolver
1 2 3 4 5 6 7 8 9 10 @SpringBootApplication public class A01Application { public static void main (String[] args) throws Exception { ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args); Resource[] resource = context.getResources("classpath*:META-INF/spring.factories" ); for (Resource r : resource) { System.out.println(r); } } }
运行 main() 方法后,控制台打印出:
URL [jar:file:/C:/Users/Administrator/.m2/repository/org/springframework/boot/spring-boot/2.5.5/spring-boot-2.5.5.jar!/META-INF/spring.factories]
URL [jar:file:/C:/Users/Administrator/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.5.5/spring-boot-autoconfigure-2.5.5.jar!/META-INF/spring.factories]
URL [jar:file:/C:/Users/Administrator/.m2/repository/org/springframework/spring-beans/5.3.10/spring-beans-5.3.10.jar!/META-INF/spring.factories]
EnvironmentCapable
1 2 3 4 5 6 7 8 9 10 @SpringBootApplication public class A01Application { public static void main (String[] args) throws Exception { ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args); System.out.println(context.getEnvironment().getProperty("java_home" )); System.out.println(context.getEnvironment().getProperty("server.port" )); } }
运行 main() 方法后,控制台打印出:
C:\Program Files\Java\jdk1.8.0_60
8080
ApplicationEventPublisher
定义事件类 RegisteredEvent
:
1 2 3 4 5 6 public class RegisteredEvent extends ApplicationEvent { public RegisteredEvent (Object source) { super (source); } }
将 ComponentA
作为发送事件的 Bean:
1 2 3 4 5 6 7 8 9 10 11 @Component @Slf4j public class ComponentA { @Autowired private ApplicationEventPublisher context; public void register () { log.info("ComponentA 发布注册事件" ); context.publishEvent(new RegisteredEvent (this )); } }
将 ComponentB
作为事件监听器:
1 2 3 4 5 6 7 8 9 @Component @Slf4j public class ComponentB { @EventListener public void hander (RegisteredEvent event) { log.info("{}" ,event); log.info("ComponentB接收到事件,发送短信" ); } }
在 main() 方法中使用ComponentA
发送事件:
1 2 3 4 public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args); context.getBean(ComponentA.class).register(); }
运行 main()方法后,控制台打印出:
com.itheima.a01.ComponentA : ComponentA 发布注册事件
com.itheima.a01.ComponentB : com.itheima.a01.RegisteredEvent[source=com.itheima.a01.ComponentA@33425811]
com.itheima.a01.ComponentB : ComponentB接收到事件,发送短信
2. 容器实现
2.1 BeanFactory 的实现
现有如下测试类
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 26 27 28 29 30 31 32 33 34 35 36 37 public class TestBeanFactory { @Configuration static class Config { @Bean public Bean1 bean1 () { return new Bean1 (); } @Bean public Bean2 bean2 () { return new Bean2 (); } } @Slf4j static class Bean1 { public Bean1 () { log.debug("构造 Bean1()" ); } @Autowired private Bean2 bean2; public Bean2 getBean2 () { return bean2; } } @Slf4j static class Bean2 { public Bean2 () { log.debug("构造 Bean2()" ); } } }
使用DefaultListableBeanFactory
作为beanFactory
的实现类,来进行演示。
有了 Bean 工厂,还需要定义 Bean,之后再把定义的 Bean 注册到工厂即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void main (String[] args) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory (); AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder .genericBeanDefinition(Config.class) .setScope("singleton" ) .getBeanDefinition(); beanFactory.registerBeanDefinition("config" , beanDefinition); for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) { System.out.println(beanDefinitionName); } }
运行 main()方法后,控制台打印出:
config
现在 Bean 工厂中 有且仅有一个 名为 config
的 Bean。
解析配置类
但我们的配置类明明加了@Configuration
和 @Bean
两个注解,那么 Bean 工厂中应该还存在 bean1
和 bean2
啊,那为什么现在没有呢?
很明显是现在的 BeanFactory
缺少了解析 @Configuration
和 @Bean
两个注解的能力。
1 2 3 4 5 public static void main (String[] args) { AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory); Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println); }
运行 main()方法后,控制台打印出:
config
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
观察控制台输出,我们可以看到有一个名为:
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
的 Bean,根据其所含的 ConfigurationAnnotationProcessor
字样,可以知道这个 Bean 就是用来处理 @Configuration
和 @Bean
注解的,将配置类中定义的 Bean 信息补充到 BeanFactory
中。
但现在已经加入 Bean 工厂中了,为啥依旧没有 bean1
和 bean2
呢?
现在仅仅是将处理器添加到了 Bean 工厂,还没有使用处理器。
像 internalConfigurationAnnotationProcessor
这样的 Bean
,都有一个共同的类型,名为 BeanFactoryPostProcessor
,因此可以我们拿到所有的后置处理器,逐一执行。
1 2 3 4 5 6 7 8 beanFactory.getBeansOfType(BeanFactoryPostProcessor.class) .values() .stream() .forEach(beanFactoryPostProcessor -> beanFactoryPostProcessor.postProcessBeanFactory(beanFactory)); for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) { System.out.println(beanDefinitionName); }
运行 main()方法后,控制台打印出:
config
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
bean1
bean2
由此可见,beanFactory
的原始功能并没有那么丰富,它的一些拓展功能是由一些后置处理器来完成的。
依赖注入
bean1
和bean2
已经被补充到 bean 工厂中了,那我们可以使用吗?
1 2 System.out.println(beanFactory.getBean(Bean1.class).getBean2());
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'bean1'
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'config'
com.itheima.a02.TestBeanFactory$Bean1 - 构造 Bean1()
null
控制台输出为 null,bean2
没有注入到bean1
中
beanFactory
依赖注入的功能也没有…
我们刚才用到的是其中一种后置处理器,是针对beanFactory
的后置处理器,作用是补充beanDefination
,去解析这些@Bean
、@Configuration
,但如果要依赖注入的话,还有一些叫bean的后置处理器
,由它来去解析@Autowired
在先前添加到 BeanFactory 中的后置处理器里,有名为 internalAutowiredAnnotationProcessor
和 internalCommonAnnotationProcessor
的两个后置处理器。前者用于解析 @Autowired
注解,后者用于解析 @Resource
注解,这种我们称之为bean的后置处理器
,它们会针对 bean 生命周期的各个阶段,提供一些拓展功能,它们都有一个共同的类型BeanPostProcessor
,因此可以:
1 2 3 4 5 6 public static void main (String[] args) { System.out.println("---------------------------------------------" ); beanFactory.getBeansOfType(BeanPostProcessor.class).values().forEach(beanFactory::addBeanPostProcessor); System.out.println(beanFactory.getBean(Bean1.class).getBean2()); }
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'bean1'
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'config'
com.atguigu.demo.TestBeanFactory$Bean1 - 构造 Bean1()
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'bean2'
com.itheima.a02.TestBeanFactory$Bean2 - 构造 Bean2()
com.itheima.a02.TestBeanFactory$Bean2@31304f14
建立BeanPostProcessor
和BeanFactory
的关系后,bean2 被成功注入到 bean1 中了。
1 2 3 4 5 6 7 AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory); beanFactory.getBeansOfType(BeanPostProcessor.class) .values() .forEach(beanFactory::addBeanPostProcessor);
我们发现:刚开始 beanFactory 中都是一些BeanDefination
,并没有去实例化、初始化,仅仅是保存了 bean 的描述信息在 bean 工厂中,当我们第一次调用getBean()
时,才会去创建 bean,即延时创建。
那有没有什么办法预先就初始化好单例对象呢?
1 2 3 4 5 6 7 8 public static void main (String[] args) { beanFactory.preInstantiateSingletons(); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" ); beanFactory.getBeansOfType(BeanPostProcessor.class).values().forEach(beanFactory::addBeanPostProcessor); System.out.println(beanFactory.getBean(Bean1.class).getBean2()); }
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'config'
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'bean1'
com.itheima.a02.TestBeanFactory$Bean1 - 构造 Bean1()
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'bean2'
com.itheima.a02.TestBeanFactory$Bean2 - 构造 Bean2()
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
com.itheima.a02.TestBeanFactory$Bean2@76505305
目前可以知道,BeanFactory
不会 :
主动调用 BeanFactory
后置处理器;
主动添加 Bean 后置处理器
;
主动初始化单例对象;
解析 ${}
和 #{}
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 public class TestBeanFactory { @Configuration static class Config { @Bean public Bean1 bean1 () { return new Bean1 (); } @Bean public Bean2 bean2 () { return new Bean2 (); } @Bean public Bean3 bean3 () { return new Bean3 (); } @Bean public Bean4 bean4 () { return new Bean4 (); } } interface Inter { } @Slf4j static class Bean1 { public Bean1 () { log.debug("构造 Bean1()" ); } @Autowired private Bean2 bean2; @Autowired @Resource(name = "bean4") private Inter bean3; private Inter getInter () { return bean3; } public Bean2 getBean2 () { return bean2; } } @Slf4j static class Bean2 { public Bean2 () { log.debug("构造 Bean2()" ); } } @Slf4j static class Bean3 implements Inter { public Bean3 () { log.debug("构造 Bean3()" ); } } @Slf4j static class Bean4 implements Inter { public Bean4 () { log.debug("构造 Bean4()" ); } } }
向 Bean 工厂中添加了 bean3
和 bean4
,并且在 bean1
中注入 Inter
类型的 Bean。
现在 Bean 工厂中Inter
类型的 Bean 有两个,分别是 bean3
、bean4
,那么会注入哪一个呢?
如果只使用 @Autowired
,首先会按照类型注入,如果同类型的 Bean 有多个,再按照变量名称注入,如果再注入失败,就报错;如果只使用 @Resource
,首先会按照 name 进行注入,同名的 Bean 有多个,再按照类型注入,否则就报错。
那如果即使用 @Autowired
又使用 @Resource(name = "bean4")
呢?
1 2 3 public static void main (String[] args) { System.out.println(beanFactory.getBean(Bean1.class).getInter()); }
com.itheima.a02.TestBeanFactory$Bean3@175c2241
根据打印的结果可知,@Autowired
生效了,这是因为internalAutowiredAnnotationProcessor
排在 internalCommonAnnotationProcessor
之前。我们可以打印出它们的顺序:
1 2 3 4 5 6 beanFactory.getBeansOfType(BeanPostProcessor.class) .values() .forEach(beanPostProcessor -> { System.out.println(">>>>>>>>" + beanPostProcessor); beanFactory.addBeanPostProcessor(beanPostProcessor); });
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@2e005c4b
org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@4567f35d
然后我们改变它们的顺序
1 2 3 4 5 6 7 8 beanFactory.getBeansOfType(BeanPostProcessor.class) .values() .stream() .sorted(beanFactory.getDependencyComparator()) .forEach(beanPostProcessor -> { System.out.println(">>>>>>>>" + beanPostProcessor); beanFactory.addBeanPostProcessor(beanPostProcessor); });
org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@4f18837a
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@359f7cdf
com.itheima.a02.TestBeanFactory$Bean4@62379589
结果注入的是bean4
为什么使用beanFactory.getDependencyComparator()
后就改变了 BeanPostProcessor
的先后顺序呢?
我们在使用AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory)
工具类时,翻看源码可以发现
1 2 3 4 5 6 7 8 9 10 11 12 13 public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors (BeanDefinitionRegistry registry, @Nullable Object source) { DefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry); if (beanFactory != null ) { if (!(beanFactory.getDependencyComparator() instanceof AnnotationAwareOrderComparator)) { beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); } if (!(beanFactory.getAutowireCandidateResolver() instanceof ContextAnnotationAutowireCandidateResolver)) { beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver ()); } } }
比较器是根据后置处理器的getOrder()
返回值,从小到大进行排序。
AutowiredAnnotationBeanPostProcessor
设置的 order 是:
1 private int order = Ordered.LOWEST_PRECEDENCE - 2 ;
CommonAnnotationBeanPostProcessor
设置的 order 是:
1 2 3 public CommonAnnotationBeanPostProcessor () { setOrder(Ordered.LOWEST_PRECEDENCE - 3 ); }
因此当设置了AnnotationAwareOrderComparator
比较器后,CommonAnnotationBeanPostProcessor
排在更前面,@Resource
就先生效。
总结
beanFactory
可以通过 registerBeanDefinition
注册一个 bean definition
对象
我们平时使用的配置类、xml、组件扫描等方式都是生成 bean definition
对象注册到 beanFactory
当中
bean definition
描述了这个 bean 的创建蓝图:scope 是什么、用构造还是工厂创建、初始化销毁方法是什么,等等
beanFactory
需要手动调用 beanFactory 后处理器
对它做增强
例如通过解析 @Bean
、@ComponentScan
等注解,来补充一些 bean definition
beanFactory
需要手动添加 bean 后处理器,以便对后续 bean 的创建过程提供增强
例如 @Autowired
,@Resource
等注解的解析都是 bean 后处理器完成的
bean 后处理器
的添加顺序会对解析结果有影响,见上文中同时加 @Autowired
,@Resource
的例子
beanFactory
需要手动调用方法来初始化单例
beanFactory
需要额外设置才能解析 ${}
与 #{}
2.2 ApplicationContext 的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class A02Application { static class Bean1 { } static class Bean2 { private Bean1 bean1; public void setBean1 (Bean1 bean1) { this .bean1 = bean1; } public Bean1 getBean1 () { return bean1; } } }
ClassPathXmlApplicationContext
较为经典的容器,基于 classpath 下的 xml 格式的配置文件 来创建
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0" encoding="UTF-8" ?> <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 ="bean1" class ="com.itheima.a02.A02Application.Bean1" > </bean > <bean id ="bean2" class ="com.itheima.a02.A02Application.Bean2" > <property name ="bean1" ref ="bean1" > </property > </bean > </beans >
1 2 3 4 5 6 7 private static void testClassPathXmlApplicationContext () { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext ("bean1.xml" ); for (String beanDefinitionName : context.getBeanDefinitionNames()) { System.out.println(beanDefinitionName); } System.out.println(context.getBean(Bean2.class).getBean1()); }
bean1
bean2
com.itheima.a02.A02Application$Bean1@6f1de4c7
FileSystemXmlApplicationContext
基于磁盘路径下 xml 格式的配置文件来创建。
1 2 3 4 5 6 7 8 private static void testFileSystemXmlApplicationContext () { FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext ("D:\\EdgeDownload\\framwork\\demo\\src\\main\\resources\\bean1.xml" ); for (String beanDefinitionName : context.getBeanDefinitionNames()) { System.out.println(beanDefinitionName); } System.out.println(context.getBean(Bean2.class).getBean1()); }
bean1
bean2
com.itheima.a02.A02Application$Bean1@128d2484
ClassPathXmlApplicationContext
和 FileSystemXmlApplicationContext
都依赖于从 XML 文件中读取 Bean 的信息,而这都利用了 XmlBeanDefinitionReader
进行读取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main (String[] args) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory (); System.out.println("读取之前" ); for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) { System.out.println(beanDefinitionName); } System.out.println("读取之后" ); XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader (beanFactory); xmlBeanDefinitionReader.loadBeanDefinitions(new ClassPathResource ("bean1.xml" )); for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) { System.out.println(beanDefinitionName); } }
读取之前
读取之后
org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 2 bean definitions from class path resource [bean1.xml]
bean1
bean2
AnnotationConfigApplicationContext
基于 java 配置类来创建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private static void testAnnotationConfigApplicationContext () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (Config.class); for (String beanDefinitionName : context.getBeanDefinitionNames()) { System.out.println(beanDefinitionName); } System.out.println(context.getBean(Bean2.class).getBean1()); } @Configuration static class Config { @Bean public Bean1 bean1 () { return new Bean1 (); } @Bean public Bean2 bean2 (Bean1 bean1) { Bean2 bean2 = new Bean2 (); bean2.setBean1(bean1); return bean2; } }
1 2 3 4 public class A02Application { public static void main (String[] args) { testAnnotationConfigApplicationContext(); }
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
a02Application.Config
bean1
bean2
com.itheima.a02.A02Application$Bean1@7748410a
与前面两种基于 Xml 创建ApplicationContext
的方式相比,AnnotationConfigApplicationContext
自动帮我们添加了一些常用的后置处理器。
如果想在基于 Xml 的方式中也添加上这些 Bean,可以在 Xml 中进行配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd" > <bean id ="bean1" class ="com.itheima.a02.A02Application.Bean1" > </bean > <bean id ="bean2" class ="com.itheima.a02.A02Application.Bean2" > <property name ="bean1" ref ="bean1" > </property > </bean > <context:annotation-config /> </beans >
AnnotationConfigServletWebServerApplicationContext
基于 Java 配置类来创建,用于 web 环境。
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 26 27 28 29 30 31 @Configuration static class WebConfig { @Bean public ServletWebServerFactory servletWebServerFactory () { return new TomcatServletWebServerFactory (); } @Bean public DispatcherServlet dispatcherServlet () { return new DispatcherServlet (); } @Bean public DispatcherServletRegistrationBean registrationBean (DispatcherServlet dispatcherServlet) { return new DispatcherServletRegistrationBean (dispatcherServlet, "/" ); } @Bean("/hello") public Controller controller1 () { return (request, response) -> { response.getWriter().println("hello" ); return null ; }; } }
1 2 3 4 5 private static void testAnnotationConfigServletWebServerApplicationContext () { AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext (WebConfig.class); }
运行代码,在浏览器中访问 http://localhost:8080/hello 路径则会显示出 hello 字样
2.3 【补充】BeanFactory 接口体系
BeanFactory
其实就是 Spring IOC 容器,它本身是一个接口,提供了一系列获取 Bean 的方式。
基于它也有众多子接口:
ListableBeanFactory
:提供获取 Bean 集合的能力,比如一个接口可能有多个实现,通过该接口下的方法就能获取某种类型的所有 Bean;
HierarchicalBeanFactory
:Hierarchical
意为“层次化”,通常表示一种具有层级结构的概念或组织方式,这种层次化结构可以通过父子关系来表示对象之间的关联,比如树、图、文件系统、组织架构等。根据该接口下的方法可知,能够获取到父容器,说明 BeanFactory
有父子容器概念;
AutowireCapableBeanFactory
:提供了创建 Bean、自动装配 Bean、属性填充、Bean 初始化、依赖注入等能力,比如 @Autowired
注解的底层实现就依赖于该接口的 resolveDependency()
方法;
ConfigurableBeanFactory
:该接口并未直接继承至 BeanFactory
,而是继承了 HierarchicalBeanFactory
。Configurable
意为“可配置的”,就是说该接口用于对 BeanFactory
进行一些配置,比如设置类型转换器。
2.4 【补充】读取 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 的增加、删除和查找功能。
注册与获取 Bean
根据前面的补充,现在可以这样注册并获取 Bean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class DefaultListableBeanFactoryTest { static class MyBean { } public static void main (String[] args) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory (); AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader (beanFactory); reader.registerBean(MyBean.class); MyBean bean = beanFactory.getBean(MyBean.class); System.out.println(bean); } }
2.5 【补充】ApplicationContext 接口体系
1 2 3 public interface ApplicationContext extends EnvironmentCapable , ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver { }
ApplicationContext
接口继承了许多接口,可谓是接口的集大成者,其中:
EnvironmentCapable
:提供获取 Environment 的能力
ListableBeanFactory
:提供了获取某种类型的 Bean 集合的能力
HierarchicalBeanFactory
:提供了获取父容器的能力
MessageSource
:提供了对国际化信息进行处理的能力
ApplicationEventPublisher
:提供了事件发布能力
ResourcePatternResolver
:提供了通过通配符获取多个资源的能力
虽然 ApplicationContext
继承了很多接口,但这些能力的实现是通过一种委派(Delegate)的方式实现的,这种方式也被叫做委派模式,但它并不属于 GoF 的 23 种设计模式中的一种,是一种面向对象的设计模式。什么是委派呢?
1 2 3 4 5 6 7 8 9 10 11 12 public class MyApplicationContext implements ApplicationContext { private final ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver (); @Override public Resource[] getResources(String locationPattern) throws IOException { return resourcePatternResolver.getResources(locationPattern); } }
实现获取资源的方式并不是由实现类自身完成,而是交给其内部的一个成员变量完成,这样的方式就是委派(这和对象适配器模式很相似)。
在日常编码遇到这样的实现逻辑时,类名可以以 Delegate
结尾。
ConfigurableApplicationContext
ApplicationContext
有一个子接口 ConfigurableApplicationContext
,从类名就可以看出,它提供了对 ApplicationContext
进行配置的能力,浏览其内部方法可知,提供了诸如设置父容器、设置 Environment
等能力。
AbstractApplicationContext
ApplicationContext
有一个非常重要的抽象实现 AbstractApplicationContext
,其他具体实现都会继承这个抽象实现,在其内部通过委派的方式实现了一些接口的能力,除此之外还有一个与 Spring Bean 的生命周期息息相关的方法:refresh()
。
3. Bean 的生命周期
1 2 3 4 5 6 7 8 @SpringBootApplication public class A03Application { public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A03Application.class, args); context.close(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Slf4j @Component public class LifeCycleBean { public LifeCycleBean () { log.info("构造" ); } @Autowired public void autowire (@Value("${JAVA_HOME}") String home) { log.info("依赖注入: {}" , home); } @PostConstruct public void init () { log.info("初始化" ); } @PreDestroy public void destroy () { log.info("销毁" ); } }
运行主启动类,查看控制台的日志信息(只列举主要信息):
com.itheima.a03.LifeCycleBean : 构造
com.itheima.a03.LifeCycleBean : 依赖注入: C:\Program Files\Java\jdk1.8.0_60
com.itheima.a03.LifeCycleBean : 初始化
com.itheima.a03.LifeCycleBean : 销毁
除此之外,Spring 还提供了一些对 Bean 生命周期的各个阶段进行拓展的BeanPostProcessor
,比如
InstantiationAwareBeanPostProcessor
和 DestructionAwareBeanPostProcessor
。
实现这两个接口,并使用 @Component
注解标记实现类:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @Slf4j @Component public class MyBeanPostProcessor implements InstantiationAwareBeanPostProcessor , DestructionAwareBeanPostProcessor { @Override public void postProcessBeforeDestruction (Object o, String beanName) throws BeansException { if ("lifeCycleBean" .equals(beanName)) { log.info("<<<<<<<<<< 销毁执行之前,如 @PreDestroy" ); } } @Override public Object postProcessBeforeInstantiation (Class<?> beanClass, String beanName) throws BeansException { if ("lifeCycleBean" .equals(beanName)) { log.info("<<<<<<<<<< 实例化之前执行,这里返回的对象会替换掉原本的 bean" ); } return null ; } @Override public boolean postProcessAfterInstantiation (Object bean, String beanName) throws BeansException { if ("lifeCycleBean" .equals(beanName)) { log.info("<<<<<<<<<< 实例化之后执行,如果返回 false 会跳过依赖注入节点" ); } return true ; } @Override public PropertyValues postProcessProperties (PropertyValues pvs, Object bean, String beanName) throws BeansException { if ("lifeCycleBean" .equals(beanName)) { log.info("<<<<<<<<<< 依赖注入阶段执行,如 @Autowired、@Value、@Resource" ); } return pvs; } @Override public Object postProcessBeforeInitialization (Object bean, String beanName) throws BeansException { if ("lifeCycleBean" .equals(beanName)) { log.info("<<<<<<<<<< 初始化执行之前,这里返回的对象会替换掉原本的 bean,如 @PostConstruct、@ConfigurationProperties" ); } return bean; } @Override public Object postProcessAfterInitialization (Object bean, String beanName) throws BeansException { if ("lifeCycleBean" .equals(beanName)) { log.info("<<<<<<<<<< 初始化之后执行,这里返回的对象会替换掉原本的 bean,如代理增强" ); } return bean; } }
再运行主启动类,查看控制台的日志信息(只列举主要信息):
com.itheima.a03.MyBeanPostProcessor : <<<<<<<<<< 实例化之前执行,这里返回的对象会替换掉原本的 bean
com.itheima.a03.LifeCycleBean : 构造
com.itheima.a03.MyBeanPostProcessor : <<<<<<<<<< 实例化之后执行,如果返回 false 会跳过依赖注入节点
com.itheima.a03.MyBeanPostProcessor : <<<<<<<<<< 依赖注入阶段执行,如 @Autowired、@Value、@Resource
com.itheima.a03.LifeCycleBean : 依赖注入: C:\Program Files\Java\jdk1.8.0_60
com.itheima.a03.MyBeanPostProcessor : <<<<初始化执行之前,这里返回的对象会替换掉原本的 bean,如 @PostConstruct、@ConfigurationProperties
com.itheima.a03.LifeCycleBean : 初始化
com.itheima.a03.MyBeanPostProcessor : <<<<<<<<<< 初始化之后执行,这里返回的对象会替换掉原本的 bean,如代理增强
com.itheima.a03.MyBeanPostProcessor : <<<<<<<<<< 销毁执行之前,如 @PreDestroy
com.itheima.a03.LifeCycleBean : 销毁
为什么实现了 BeanPostProcessor
接口后就能够在 Bean 生命周期的各个阶段进行拓展呢?
这使用了模板方法
设计模式。
现有如下代码,模拟 BeanFactory
构造 Bean:
1 2 3 4 5 6 7 8 9 static class MyBeanFactory { public Object getBean () { Object bean = new Object (); System.out.println("构造 " + bean); System.out.println("依赖注入 " + bean); System.out.println("初始化 " + bean); return bean; } }
如果现在需要在依赖注入之后,初始化之前进行其他的操作,那首先能想到的就是直接改写 getBean()相关操作的代码,显然不是一种好方式。
可以定义一个接口:
1 2 3 interface BeanPostProcessor { void inject (Object bean) ; }
然后对 MyBeanFactory
进行修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static class MyBeanFactory { private List<BeanPostProcessor> processors = new ArrayList <>(); public void addProcessor (BeanPostProcessor processor) { processors.add(processor); } public Object getBean () { Object bean = new Object (); System.out.println("构造 " + bean); System.out.println("依赖注入 " + bean); for (BeanPostProcessor processor : processors) { processor.inject(bean); } System.out.println("初始化 " + bean); return bean; } }
之后如果需要拓展,调用 MyBeanFactory
实例的 addProcessor()
方法添加拓展逻辑即可:
1 2 3 4 5 6 public static void main (String[] args) { MyBeanFactory beanFactory = new MyBeanFactory (); beanFactory.addProcessor(bean -> System.out.println("解析 @Autowired" )); beanFactory.addProcessor(bean -> System.out.println("解析 @Resource" )); beanFactory.getBean(); }
构造 java.lang.Object@49097b5d
依赖注入 java.lang.Object@49097b5d
解析 @Autowired
解析 @Resource
初始化 java.lang.Object@49097b5d
4. Bean 的后置处理器
4.1 常见的 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 26 27 28 29 30 31 32 33 34 35 36 @Slf4j public class Bean1 { private Bean2 bean2; @Autowired public void setBean2 (Bean2 bean2) { log.info("@Autowired 生效: {}" , bean2); this .bean2 = bean2; } private Bean3 bean3; @Resource public void setBean3 (Bean3 bean3) { log.info("@Resource 生效: {}" , bean3); this .bean3 = bean3; } private String home; @Autowired public void setHome (@Value("${JAVA_HOME}") String home) { log.info("@Value 生效: {}" , home); this .home = home; } @PostConstruct public void init () { log.info("@PostConstruct 生效" ); } @PreDestroy public void destroy () { log.info("@PreDestroy 生效" ); } }
1 2 3 4 5 public class Bean2 {} public class Bean3 {}
Bean2
和 Bean3
很简单,而在 Bean1
中使用了多个注解以实现 Bean 注入和值注入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class A04Application { public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("bean1" , Bean1.class); context.registerBean("bean2" , Bean2.class); context.registerBean("bean3" , Bean3.class); context.refresh(); context.close(); } }
运行上述方法后,控制台中只打印了与 Spring 相关的日志信息,也就是说 Bean1
中使用的注解并没有生效。
向 GenericApplicationContext
添加一些 Bean 后置处理器,使得 Bean1
中使用的注解能够生效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("bean1" , Bean1.class); context.registerBean("bean2" , Bean2.class); context.registerBean("bean3" , Bean3.class); context.getDefaultListableBeanFactory().setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver ()); context.registerBean(AutowiredAnnotationBeanPostProcessor.class); context.registerBean(CommonAnnotationBeanPostProcessor.class); context.refresh(); context.close(); }
com.itheima.a04.Bean1 - @Resource 生效: com.atguigu.a04.Bean3@1649b0e6
com.itheima.a04.Bean1 - @Autowired 生效: com.atguigu.a04.Bean2@34f7cfd9
com.itheima.a04.Bean1 - @Value 生效: C:\Program Files\Java\jdk1.8.0_60
com.itheima.a04.Bean1 - @PostConstruct 生效
com.itheima.a04.Bean1 - @PreDestroy 生效
解析 @ConfigurationProperties
1 2 3 4 5 6 7 8 @Getter @Setter @ToString @ConfigurationProperties(prefix = "java") public class Bean4 { private String home; private String version; }
上述代码用于获取环境变量中 java.home 和 java.version 的信息。
对先前的 main()方法进行补充:
1 2 3 4 5 6 7 8 public static void main (String[] args) { context.registerBean("bean4" , Bean4.class); System.out.println(context.getBean(Bean4.class)); }
Bean4(home=null, version=null)
Bean4
成功添加到容器中,但值注入失败了,显然是缺少解析 @ConfigurationProperties
注解的后置处理器。
1 2 3 4 5 6 7 8 9 public static void main (String[] args) { context.registerBean("bean4" , Bean4.class); ConfigurationPropertiesBindingPostProcessor.register(context.getDefaultListableBeanFactory()); System.out.println(context.getBean(Bean4.class)); }
Bean4(home=C:\Users\Administrator.jdks\corretto-17.0.11, version=17.0.11)
4.2 AutowiredAnnotationBeanPostProcessor
AutowiredAnnotationBeanPostProcessor
可用于解析@Autowired
和@Value
注解,下面我们对它的运行过程进行深入分析。
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 public class DigInAutowired { public static void main (String[] args) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory (); beanFactory.registerSingleton("bean2" , new Bean2 ()); beanFactory.registerSingleton("bean3" , new Bean3 ()); beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver ()); beanFactory.addEmbeddedValueResolver(new StandardEnvironment ()::resolvePlaceholders); AutowiredAnnotationBeanPostProcessor processor = new AutowiredAnnotationBeanPostProcessor (); processor.setBeanFactory(beanFactory); Bean1 bean1 = new Bean1 (); System.out.println(bean1); processor.postProcessProperties(null , bean1, "bean1" ); System.out.println(bean1); } }
Bean1(bean2=null, bean3=null, home=null)
com.itheima.a04.Bean1 - @Autowired 生效: com.itheima.a04.Bean2@de3a06f
com.itheima.a04.Bean1 - @Value 生效: C:\Program Files\Java\jdk1.8.0_60
Bean1(bean2=com.itheima.a04.Bean2@de3a06f, bean3=null, home=C:\Program Files\Java\jdk1.8.0_60)
在未调用AutowiredAnnotationBeanPostProcessor#postProcessProperties()
方法时,bean1
中的bean2
、bean3
、home
都没有注入,而在调用之后,成功注入了bean2
和home
,由于bean3
是通过@Resource
标注,所以不会进行注入。
postProcessProperties()
源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override public PropertyValues postProcessProperties (PropertyValues pvs, Object bean, String beanName) { InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs); try { metadata.inject(bean, beanName, pvs); } catch (BeanCreationException ex) { throw ex; } catch (Throwable ex) { throw new BeanCreationException (beanName, "Injection of autowired dependencies failed" , ex); } return pvs; }
其中 findAutowiringMetadata()
用于查找哪些属性
或方法参数
上标注@Autowired
、@Value
,将这些信息封装到InjectionMetadata
中,然后调用inject()
反射完成注入。
findAutowiringMetadata()
是一个私有方法,利用反射进行调用并打断点查看InjectionMetadata
中的信息。
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 26 27 28 public class DigInAutowired { public static void main (String[] args) throws Exception { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory (); beanFactory.registerSingleton("bean2" , new Bean2 ()); beanFactory.registerSingleton("bean3" , new Bean3 ()); beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver ()); beanFactory.addEmbeddedValueResolver(new StandardEnvironment ()::resolvePlaceholders); AutowiredAnnotationBeanPostProcessor processor = new AutowiredAnnotationBeanPostProcessor (); processor.setBeanFactory(beanFactory); Bean1 bean1 = new Bean1 (); Method findAutowiringMetadata = AutowiredAnnotationBeanPostProcessor.class.getDeclaredMethod("findAutowiringMetadata" , String.class, Class.class, PropertyValues.class); findAutowiringMetadata.setAccessible(true ); InjectionMetadata metadata = (InjectionMetadata) findAutowiringMetadata.invoke(processor, "bean1" , Bean1.class, null ); System.out.println(metadata); } }
InjectionMetadata
中有一个名为injectedElements
的集合类型成员变量,其中存储了添加了@Autowired
或 @Value
的成员变量,方法参数信息
然后调用InjectionMetadata#inject()
注入
1 2 3 4 5 6 7 8 9 10 11 @SneakyThrows public static void main (String[] args) { InjectionMetadata metadata = (InjectionMetadata) method.invoke(postProcessor, "bean1" , Bean1.class, null ); metadata.inject(bean1, "bean1" , null ); System.out.println(bean1); }
com.itheima.a04.Bean1 - @Autowired 生效: com.itheima.a04.Bean2@1e4a7dd4
com.itheima.a04.Bean1 - @Value 生效: C:\Program Files\Java\jdk1.8.0_60
Bean1(bean2=com.itheima.a04.Bean2@1e4a7dd4, bean3=null, home=C:\Program Files\Java\jdk1.8.0_60)
既然InjectionMetadata
中保存了 @Value
、 @Autowired
注解的成员变量、方法参数信息,那么inject()
中又如何处理呢?
通过以下代码简单概括:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @SneakyThrows public static void main (String[] args) { Field bean3 = Bean1.class.getDeclaredField("bean3" ); DependencyDescriptor dd1 = new DependencyDescriptor (bean3, false ); Object o1 = beanFactory.doResolveDependency(dd1, null , null , null ); System.out.println(o1); Method setBean2 = Bean1.class.getDeclaredMethod("setBean2" , Bean2.class); DependencyDescriptor dd2 = new DependencyDescriptor (new MethodParameter (setBean2, 0 ), false ); Object o2 = beanFactory.doResolveDependency(dd2, null , null , null ); System.out.println(o2); Method setHome = Bean1.class.getDeclaredMethod("setHome" , String.class); DependencyDescriptor dd3 = new DependencyDescriptor (new MethodParameter (setHome, 0 ), true ); Object o3 = beanFactory.doResolveDependency(dd3, null , null , null ); System.out.println(o3); }
com.itheima.a04.Bean3@31206beb
com.itheima.a04.Bean2@50de0926
C:\Program Files\Java\jdk1.8.0_60
5. BeanFactory 后置处理器
5.1 常见的工厂后置处理器
引入所需依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 2.3.0</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.2.15</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency >
需要用到的类信息:
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 @Configuration @ComponentScan("com.itheima.a05.component") public class Config { @Bean public Bean1 bean1 () { return new Bean1 (); } @Bean public SqlSessionFactoryBean sqlSessionFactoryBean (DataSource dataSource) { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean (); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean; } @Bean(initMethod = "init") public DruidDataSource dataSource () { DruidDataSource dataSource = new DruidDataSource (); dataSource.setUrl("jdbc:mysql://localhost:3306/advanced_spring" ); dataSource.setName("root" ); dataSource.setPassword("123456" ); return dataSource; } }
1 2 3 4 5 6 @Slf4j public class Bean1 { public Bean1 () { System.out.println("我被 Spring 管理啦" ); } }
1 2 3 4 5 6 7 @Slf4j @Component public class Bean2 { public Bean2 () { log.info("我被 Spring 管理啦" ); } }
使用GenericApplicationContext
作为容器,向容器中注册config
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class A05Application { public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("config" , Config.class); context.refresh(); for (String name : context.getBeanDefinitionNames()) { System.out.println(name); } context.close(); } }
config
并没有打印出config
外的 Bean 信息,@Configuration
、@Bean
并没有被解析。
向容器中注册ConfigurationClassPostProcessor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class A05Application { public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("config" , Config.class); context.registerBean(ConfigurationClassPostProcessor.class); context.refresh(); for (String name : context.getBeanDefinitionNames()) { System.out.println(name); } context.close(); } }
com.itheima.a05.component.Bean2 - 我被 Spring 管理啦
com.itheima.a05.Bean1 - 我被 Spring 管理啦
com.alibaba.druid.pool.DruidDataSource - {dataSource-1,root} inited
config
org.springframework.context.annotation.ConfigurationClassPostProcessor
bean2
bean1
sqlSessionFactoryBean
dataSource
整合MyBatis
时,@Mapper
注解的解析,也需要特定的BeanFactory后置处理器
。
1 2 3 4 5 6 7 8 @Mapper public interface Mapper1 {} @Mapper public interface Mapper2 {}
向容器中注册MapperScannerConfigurer
1 2 3 4 5 6 7 8 9 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("config" , Config.class); context.registerBean(ConfigurationClassPostProcessor.class); context.registerBean(MapperScannerConfigurer.class, i -> i.getPropertyValues().add("basePackage" , "com.itheima.a05.mapper" )); }
mapper1
mapper2
除此之外,还有一些常用的后置处理器并没有在上述信息中体现。
5.2 工厂后置处理器模拟实现
移除容器中添加的 ConfigurationClassPostProcessor
和 MapperScannerConfigurer
两个后置处理器,编码模拟对应功能的实现。
组件扫描 @ComponentScan
在Bean2
所在包路径下,新增两个类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Slf4j @Controller public class Bean3 { public Bean3 () { log.info("我被 Spring 管理啦" ); } } @Slf4j public class Bean4 { public Bean4 () { log.info("我被 Spring 管理啦" ); } }
编写 ComponentScanPostProcessor
,实现 @ComponentScan
注解的解析
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public class ComponentScanPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory (ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { try { ComponentScan componentScan = AnnotationUtils.findAnnotation(Config.class, ComponentScan.class); if (componentScan != null ) { for (String packageName : componentScan.basePackages()) { System.out.println(packageName); CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory (); String path = "classpath*:" + packageName.replace("." , "/" ) + "/**/*.class" ; Resource[] resources = new PathMatchingResourcePatternResolver ().getResources(path); AnnotationBeanNameGenerator generator = new AnnotationBeanNameGenerator (); for (Resource resource : resources) { MetadataReader reader = factory.getMetadataReader(resource); AnnotationMetadata annotationMetadata = reader.getAnnotationMetadata(); if (annotationMetadata.hasAnnotation(Component.class.getName()) || annotationMetadata.hasMetaAnnotation(Component.class.getName())) { AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder .genericBeanDefinition(reader.getClassMetadata().getClassName()) .getBeanDefinition(); if (configurableListableBeanFactory instanceof DefaultListableBeanFactory beanFactory) { String beanName = generator.generateBeanName(beanDefinition, beanFactory); beanFactory.registerBeanDefinition(beanName, beanDefinition); } } } } } } catch (IOException e) { e.printStackTrace(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void main (String[] args) throws Exception { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("config" , Config.class); context.registerBean(ComponentScanPostProcessor.class); context.refresh(); for (String name : context.getBeanDefinitionNames()) { System.out.println(name); } context.close(); }
com.itheima.a05.component
com.itheima.a05.component.Bean2 - 我被 Spring 管理啦
com.itheima.a05.component.Bean3 - 我被 Spring 管理啦
config
com.itheima.a05.ComponentScanPostProcessor
bean2
bean3
@Bean 的解析
Config
类中新增一个方法作为对比项
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 26 27 28 29 30 @Configuration @ComponentScan("com.atguigu.a05.component") public class Config { public Bean2 bean2 () { return new Bean2 (); } @Bean public Bean1 bean1 () { return new Bean1 (); } @Bean public SqlSessionFactoryBean sqlSessionFactoryBean (DataSource dataSource) { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean (); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean; } @Bean(initMethod = "init") public DruidDataSource dataSource () { DruidDataSource dataSource = new DruidDataSource (); dataSource.setUrl("jdbc:mysql://localhost:3306/advanced_spring" ); dataSource.setName("root" ); dataSource.setPassword("123456" ); return dataSource; } }
编写 AtBeanPostProcessor
实现 @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 26 27 28 29 30 public class AtBeanPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory (ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { try { CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory (); MetadataReader reader = factory.getMetadataReader(new ClassPathResource ("com/itheima/a05/Config.class" )); Set<MethodMetadata> methods = reader.getAnnotationMetadata().getAnnotatedMethods(Bean.class.getName()); for (MethodMetadata method : methods) { String initMethod = method.getAnnotationAttributes(Bean.class.getName()).get("initMethod" ).toString(); System.out.println(method); BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(); builder.setFactoryMethodOnBean(method.getMethodName(), "config" ); builder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); AbstractBeanDefinition beanDefinition = builder.getBeanDefinition(); if (initMethod.length() > 0 ) { builder.setInitMethodName(initMethod); } if (configurableListableBeanFactory instanceof DefaultListableBeanFactory beanFactory) { beanFactory.registerBeanDefinition(method.getMethodName(), beanDefinition); } } } catch (IOException e) { e.printStackTrace(); } } }
而sqlSessionFactoryBean
需要一个DataSource
类型的参数,所以必须指定 自动装配模式
1 2 3 4 5 6 @Bean public SqlSessionFactoryBean sqlSessionFactoryBean (DataSource dataSource) { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean (); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean; }
1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean(AtBeanPostProcessor.class); context.refresh(); for (String name : context.getBeanDefinitionNames()) { System.out.println(name); } context.close(); }
com.itheima.a05.Config.bean1()
com.itheima.a05.Config.sqlSessionFactoryBean(javax.sql.DataSource)
com.itheima.a05.Config.dataSource()
com.itheima.a05.Bean1 - 我被 Spring 管理啦
com.alibaba.druid.pool.DruidDataSource - {dataSource-1,root} inited
config
com.itheima.a05.AtBeanPostProcessor
bean1
sqlSessionFactoryBean
dataSource
@Mapper 的解析
@Mapper
注解是标注在接口上的,Spring 并不能真的管理一个接口,最终管理的还是一个个的对象。
而这些接口依赖于 @MapperFactoryBean
将接口转换为对象。
在 Config 添加注册 Mapper1
和 Mapper2
的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Bean public MapperFactoryBean<Mapper1> mapper1 (SqlSessionFactory sqlSessionFactory) { MapperFactoryBean<Mapper1> factory = new MapperFactoryBean <>(Mapper1.class); factory.setSqlSessionFactory(sqlSessionFactory); return factory; } @Bean public MapperFactoryBean<Mapper2> mapper2 (SqlSessionFactory sqlSessionFactory) { MapperFactoryBean<Mapper2> factory = new MapperFactoryBean <>(Mapper2.class); factory.setSqlSessionFactory(sqlSessionFactory); return factory; }
运行后,容器中 包含 名为 mapper1
和mapper2
的 Bean
这种方式虽然可以完成 Mapper 接口的注册,但每次只能单个注册,不能批量注册。
移除 Config
类中的 mapper1()
和 mapper2()
方法,编写 MapperPostProcessor
实现@Mapper
注解的解析
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class MapperPostProcessor implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanFactory (ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { } @Override public void postProcessBeanDefinitionRegistry (BeanDefinitionRegistry beanFactory) throws BeansException { try { PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver (); Resource[] resources = resolver.getResources("classpath:com/atguigu/a05/mapper/**/*.class" ); AnnotationBeanNameGenerator generator = new AnnotationBeanNameGenerator (); CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory (); for (Resource resource : resources) { MetadataReader reader = factory.getMetadataReader(resource); ClassMetadata classMetadata = reader.getClassMetadata(); if (classMetadata.isInterface()) { AbstractBeanDefinition beanDefinition1 = BeanDefinitionBuilder.genericBeanDefinition(MapperFactoryBean.class) .addConstructorArgValue(classMetadata.getClassName()) .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE) .getBeanDefinition(); AbstractBeanDefinition beanDefinition2 = BeanDefinitionBuilder.genericBeanDefinition(classMetadata.getClassName()) .getBeanDefinition(); String beanName = generator.generateBeanName(beanDefinition2, beanFactory); beanFactory.registerBeanDefinition(beanName, beanDefinition1); } } } catch (IOException e) { e.printStackTrace(); } } }
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("config" , Config.class); context.registerBean(AtBeanPostProcessor.class); context.registerBean(MapperPostProcessor.class); context.refresh(); for (String name : context.getBeanDefinitionNames()) { System.out.println(name); } context.close(); }
com.itheima.a05.Config.bean1()
com.itheima.a05.Config.sqlSessionFactoryBean(javax.sql.DataSource)
com.itheima.a05.Config.dataSource()
22:24:18.940 [main] INFO com.alibaba.druid.pool.DruidDataSource - {dataSource-1,root} inited
22:24:19.042 [main] INFO com.itheima.a05.Bean1 - 我被 Spring 管理啦
config
com.itheima.a05.AtBeanPostProcessor
com.itheima.a05.MapperPostProcessor
mapper1
mapper2
bean1
sqlSessionFactoryBean
dataSource
容器中存在 mapper1
和 mapper2
两个 Bean。
5.3 【补充】注册创建完成的 Bean
如果要将 Bean 添加到 Spring 容器中,需要先根据配置文件或注解信息为每一个 Bean 生成一个 BeanDefinition
,然后将这些 BeanDefinition
添加到 BeanDefinitionRegistry
中,当创建 Bean 对象时,直接从 BeanDefinitionRegistry
中获取 BeanDefinition
来生成 Bean。
如果生成的 Bean 是单例的,Spring 会将它们保存到 SingletonBeanRegistry
中,后续需要时从这里面寻找,避免重复创建。
那么向 Spring 容器中添加单例 Bean 时,可以跳过注册 BeanDefinition
,直接向 SingletonBeanRegistry
中添加创建完成的 Bean。既然添加的是创建完成的 Bean,所以 这个 Bean 不会经过 Spring 的生命周期。
SingletonBeanRegistry
是一个接口,它有一个子接口名为 ConfigurableListableBeanFactory
,而这恰好是 BeanFactoryPostProcessor
接口中抽象方法的参数:
1 2 3 4 @FunctionalInterface public interface BeanFactoryPostProcessor { void postProcessBeanFactory (ConfigurableListableBeanFactory beanFactory) throws BeansException; }
尝试使用 BeanFactoryPostProcessor
注册创建完成的 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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @Slf4j public class TestBeanFactoryPostProcessor { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (); context.registerBean("bean2" , Bean2.class); context.registerBean(MyBeanFactoryPostProcessor.class); context.refresh(); Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println); System.out.println(">>>>>>>>>>>>>>>>>>" ); System.out.println(context.getBean(Bean1.class)); } static class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory (ConfigurableListableBeanFactory beanFactory) throws BeansException { Bean1 bean1 = new Bean1 (); bean1.setName("mofan" ); beanFactory.registerSingleton("bean1" , bean1); } } @Getter @ToString static class Bean1 { @Setter private String name; private Bean2 bean2; @Autowired private void setBean2 (Bean2 bean2) { log.debug("依赖注入 bean2" ); this .bean2 = bean2; } @PostConstruct public void init () { log.debug("初始化..." ); } } static class Bean2 { } }
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
bean2
testBeanFactoryPostProcessor.MyBeanFactoryPostProcessor
>>>>>>>>>>>>>>>>>>
TestBeanFactoryPostProcessor.Bean1(name=mofan, bean2=null)
BeanDefinition
的名称数组中不包含 bean1
,也没有输出任何与经过 Spring 生命周期相关的日志信息,容器中 bean1
里注入的 bean2
也是 null
。这表明通过这种方式注册的 Bean 不会注册 BeanDefinition
,也不会经过 Spring 生命周期。
6. Aware 接口
6.1 Aware 接口
Aware 接口用于注入一些与容器相关的信息,例如
BeanNameAware 注入 bean 的名字
BeanFactoryAware 注入 BeanFactory 容器
ApplicationContextAware 注入 ApplicationContext 容器
EmbeddedValueResolverAware ${}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class MyBean implements BeanNameAware , ApplicationContextAware { private static final Logger logger = LoggerFactory.getLogger(MyBean.class); @Override public void setBeanName (String name) { logger.debug("\n当前bean =>" + this + " \n名称 => " + name); } @Override public void setApplicationContext (ApplicationContext applicationContext) throws BeansException { logger.debug("\n当前bean =>" + this + " \nApplicationContext => " + applicationContext); } }
1 2 3 4 5 6 7 8 9 10 @Slf4j public class A06Application { public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("myBean" , MyBean.class); context.refresh(); context.close(); } }
当前bean =>com.itheima.a06.MyBean@1a3869f4
名称 => myBean
当前bean =>com.itheima.a06.MyBean@1a3869f4
ApplicationContext => org.springframework.context.support.GenericApplicationContext@75bd9247, started on Mon Dec 23 23:08:39 CST 2024
6.2 InitializingBean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class MyBean implements BeanNameAware , ApplicationContextAware,InitializingBean { private static final Logger logger = LoggerFactory.getLogger(MyBean.class); @Override public void setBeanName (String name) { logger.debug("\n当前bean =>" + this + " \n名称 => " + name); } @Override public void setApplicationContext (ApplicationContext applicationContext) throws BeansException { logger.debug("\n当前bean =>" + this + " \nApplicationContext => " + applicationContext); } @Override public void afterPropertiesSet () throws Exception { logger.debug("\n当前bean =>" + this + " 初始化" ); } }
当前bean =>com.itheima.a06.MyBean@63440df3
名称 => myBean
com.itheima.a06.MyBean - 当前bean =>com.itheima.a06.MyBean@63440df3
ApplicationContext => org.springframework.context.support.GenericApplicationContext@75bd9247, started on Mon Dec 23 23:10:58 CST 2024
com.itheima.a06.MyBean - 当前bean =>com.itheima.a06.MyBean@63440df3 初始化
BeanFactoryAware
、ApplicationContextAware
和 EmbeddedValueResolverAware
三个接口的功能可以使用 @Autowired
注解实现,InitializingBean
接口的功能也可以使用 @PostConstruct
注解实现,为什么还要使用接口呢?
@Autowired
和 @PostConstruct
注解的解析需要使用 Bean 后置处理器,属于拓展功能,而Aware接口
属于内置功能,不加任何拓展 Spring 就能识别。
在某些情况下,拓展功能会失效,而内置功能不会失效。
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 26 27 28 public class MyBean implements BeanNameAware , ApplicationContextAware,InitializingBean { private static final Logger logger = LoggerFactory.getLogger(MyBean.class); @Override public void setBeanName (String name) { logger.debug("\n当前bean =>" + this + " \n名称 => " + name); } @Override public void setApplicationContext (ApplicationContext applicationContext) throws BeansException { logger.debug("\n当前bean =>" + this + " \nApplicationContext => " + applicationContext); } @Override public void afterPropertiesSet () throws Exception { logger.debug("\n当前bean =>" + this + " 初始化" ); } @Autowired public void method (ApplicationContext applicationContext) { logger.debug("\n当前bean =>" + this + " \n使用@AutoWired注入,ApplicationContext => " + applicationContext); } @PostConstruct public void init () { logger.debug("\n当前bean =>" + this + " 使用@PostConstruct 初始化" ); } }
com.itheima.a06.MyBean -
当前bean =>com.itheima.a06.MyBean@63440df3
名称 => myBean
com.itheima.a06.MyBean -
当前bean =>com.itheima.a06.MyBean@63440df3
ApplicationContext => org.springframework.context.support.GenericApplicationContext@75bd9247, started on Mon Dec 23 23:23:43 CST 2024
com.itheima.a06.MyBean -
当前bean =>com.itheima.a06.MyBean@63440df3 初始化
运行 main()方法会发现使用的注解没有被成功解析,原因很简单,GenericApplicationContext
是一个干净的容器,其内部没有用于解析这些注解的bean后置处理器
。如果想要这些注解生效,则需要添加必要的 bean 后置处理器:
1 2 context.registerBean(AutowiredAnnotationBeanPostProcessor.class); context.registerBean(CommonAnnotationBeanPostProcessor.class);
6.3 失效的@Autowired 注解
在某些情况下,尽管容器中存在必要的后置处理器,但@Autowired
和 @PostConstruct
等注解也会失效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration public class MyConfig1 { private static final Logger logger = LoggerFactory.getLogger(MyConfig1.class); @Autowired private void setApplicationContext (ApplicationContext applicationContext) { logger.info("注入 ApplicationContext" ); } @PostConstruct public void init () { logger.info("执行 MyConfig1 的 init 方法" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("myConfig1" , MyConfig1.class); context.registerBean(AutowiredAnnotationBeanPostProcessor.class); context.registerBean(CommonAnnotationBeanPostProcessor.class); context.registerBean(ConfigurationClassPostProcessor.class); context.refresh(); context.close(); }
com.itheima.a06.MyConfig1 - 注入 ApplicationContext
com.itheima.a06.MyConfig1 - 执行 MyConfig1 的 init 方法
@Autowired
和 @PostConstruct
注解解析成功。
然后我们向Config1
中添加BeanFactoryPostProcessor
1 2 3 4 5 6 7 8 9 10 @Configuration public class MyConfig1 { private static final Logger logger = LoggerFactory.getLogger(MyConfig1.class); @Bean public BeanFactoryPostProcessor processor1 () { return beanFactory -> logger.info("执行 processor1" ); } }
在 Config1
中添加了一个被 @Bean
注解标记的 processor1()
方法,用于向容器中添加 BeanFactoryPostProcessor
。
再次运行
com.itheima.a06.MyConfig1 - 执行 processor1
用传统接口方式的注入和初始化依然成功,processor1()
方法成功生效,但@Autowired
和 @PostConstruct
的注入和初始化失败。
那是什么原因导致的呢?
配置类 @Autowired
注解失效分析
Java 配置类不包含 BeanFactoryPostProcessor
的情况
1 2 3 4 5 6 7 8 9 10 11 12 sequenceDiagram participant ac as ApplicationContext participant bfpp as BeanFactoryPostProcessor participant bpp as BeanPostProcessor participant config as Java 配置类 ac ->> bfpp : 1. 执行 BeanFactoryPostProcessor ac ->> bpp : 2. 注册 BeanPostProcessor ac ->> +config : 3. 创建和初始化 bpp ->> config : 3.1 依赖注入扩展(如 @Value 和 @Autowired) bpp ->> config : 3.2 初始化扩展(如 @PostConstruct) ac ->> config : 3.3 执行 Aware 及 InitializingBean config -->> -ac : 3.4 创建成功
可以使用 Spring 内置的接口,避免类似问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class MyConfig2 implements InitializingBean , ApplicationContextAware { private static final Logger logger = LoggerFactory.getLogger(MyConfig2.class); @Override @Autowired public void setApplicationContext (ApplicationContext applicationContext) { logger.info("注入 ApplicationContext" ); } @Override public void afterPropertiesSet () throws Exception { logger.info("初始化" ); } @Bean public BeanFactoryPostProcessor processor2 () { return beanFactory -> logger.info("执行 processor2" ); } }
1 2 3 4 5 6 7 8 9 10 11 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("myConfig2" , MyConfig2.class); context.registerBean(AutowiredAnnotationBeanPostProcessor.class); context.registerBean(CommonAnnotationBeanPostProcessor.class); context.registerBean(ConfigurationClassPostProcessor.class); context.refresh(); context.close(); }
com.itheima.a06.MyConfig2 - 注入 ApplicationContext
com.itheima.a06.MyConfig2 - 初始化
com.itheima.a06.MyConfig2 - 执行 processor2
总结
Aware 接口提供了一种内置的注入手段,可以注入BeanFactory
,ApplicationContext
InitializingBean
接口提供了一种内置的初始化手段
内置的注入和初始化不受扩展功能的影响,总会执行,因此 Spring 框架内部的类常用它们
7. 初始化与销毁
初始化和销毁 Bean 的实现有三种:
依赖于后置处理器提供的拓展功能
相关接口的功能
使用 @Bean 注解中的属性进行指定
三种初始化方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Slf4j public class Bean1 implements InitializingBean { @PostConstruct public void init () { log.info("@PostConstruct 初始化" ); } @Override public void afterPropertiesSet () throws Exception { log.info("InitializingBean 初始化" ); } public void init3 () { log.info("@Bean 初始化" ); } }
三种销毁方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Slf4j public class Bean2 implements DisposableBean { @PreDestroy public void destroy1 () { log.info("@PreDestroy 销毁" ); } @Override public void destroy () throws Exception { log.info("DisposableBean 销毁" ); } public void destroy3 () { log.info("@Bean 销毁" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @SpringBootApplication public class A07Application { public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A07Application.class, args); context.close(); } @Bean(initMethod = "init3") public Bean1 bean1 () { return new Bean1 (); } @Bean(destroyMethod = "destroy3") public Bean2 bean2 () { return new Bean2 (); } }
执行顺序
com.itheima.a07.Bean1 : @PostConstruct 初始化
com.itheima.a07.Bean1 : InitializingBean 初始化
com.itheima.a07.Bean1 : @Bean 初始化
com.itheima.a07.Bean2 : @PreDestroy 销毁
com.itheima.a07.Bean2 : DisposableBean 销毁
com.itheima.a07.Bean2 : @Bean 销毁
8. Scope
8.1 Scope 的类型与销毁
Scope 用于指定 Bean 的作用范围,有如下五个取值:
singleton
:单例(默认值)。容器启动时创建(未设置延迟),容器关闭时销毁
prototype
:多例。每次使用时创建,不会自动销毁,需要调用 DefaultListableBeanFactory#destroyBean()
进行销毁
request
:作用于 Web 应用的请求范围。每次请求用到此 Bean 时创建,请求结束时销毁
session
:作用于 Web 应用的会话范围。每个会话用到此 Bean 时创建,会话结束时销毁
application
:作用于 Web 应用的 ServletContext
。Web 容器用到此 Bean 时创建,容器关闭时销毁
前两个取值不再赘述,重点看下后三个取值。
1 2 3 4 5 6 7 8 9 @Slf4j @Component @Scope(WebApplicationContext.SCOPE_REQUEST) public class BeanForRequest { @PreDestroy public void destroy () { log.info("destroy" ); } }
1 2 3 4 5 6 7 8 9 @Slf4j @Component @Scope(WebApplicationContext.SCOPE_SESSION) public class BeanForSession { @PreDestroy public void destroy () { log.info("destroy" ); } }
1 2 3 4 5 6 7 8 9 @Slf4j @Component @Scope(WebApplicationContext.SCOPE_APPLICATION) public class BeanForApplication { @PreDestroy public void destroy () { log.info("destroy" ); } }
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 26 @RestController public class MyController { @Lazy @Autowired private BeanForRequest beanForRequest; @Lazy @Autowired private BeanForSession beanForSession; @Lazy @Autowired private BeanForApplication beanForApplication; @GetMapping(value = "/test", produces = "text/html") public String test (HttpServletRequest request, HttpSession session) { session.setMaxInactiveInterval(10 ); return "<ul>" + "<li>request scope: " + beanForRequest + "</li>" + "<li>session scope: " + beanForSession + "</li>" + "<li>application scope: " + beanForApplication + "</li>" + "</ul>" ; } }
主启动类
1 2 3 4 5 6 7 @SpringBootApplication public class A08Application { public static void main (String[] args) { SpringApplication.run(A08Application.class, args); } }
如果使用的 JDK 版本大于 8,需要要启动参数中添加如下信息避免报错:
1 --add -opens java.base /java.lang=ALL-UNNAMED
但更建议在 pom.xml 中添加以下配置,一劳永逸:
1 2 3 4 5 6 7 8 9 10 11 12 13 <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-surefire-plugin</artifactId > <configuration > <argLine > --add-opens java.base/java.lang=ALL-UNNAMED </argLine > </configuration > </plugin > </plugins > </build >
运行主启动类,在浏览器中访问 http://localhost:8080/test,页面上显示:
request scope: com.itheima.a08.BeanForRequest@2b6ecfd4
session scope: com.itheima.a08.BeanForSession@54ecf133
application scope:com.itheima.a08.BeanForApplication@29b1126e
刷新页面,页面上的信息变化为:
request scope: com.itheima.a08.BeanForRequest@3ef32346
session scope: com.itheima.a08.BeanForSession@54ecf133
application scope:com.itheima.a08.BeanForApplication@29b1126e
可以看到 request scope
发生了变化,session scope
和 application scope
没有变化。
换一个浏览器访问 http://localhost:8080/test,两个浏览器中的会话肯定不是同一个,此时 session scope
应该会发生变化:
request scope: com.itheima.a08.BeanForRequest@49575379
session scope: com.itheima.a08.BeanForSession@86a5942
application scope:com.itheima.a08.BeanForApplication@29b1126e
application
的作用范围是 ServletContext
,要想 application scope 发生变化可以重启程序。
当刷新页面后, request scope 的值发生变化,request 作用范围的 Bean 执行了销毁方法。
com.itheima.a08.BeanForRequest : 销毁
如果想看到 session 作用范围的 Bean 执行销毁方法,可以等 session 过期时在控制台上看到对应的信息。默认情况下,session 的过期时间是 30 分钟,为了更好地测试,可以在配置文件中添加:
1 2 server.servlet.session.timeout =10s
这个配置是全局的,如果只想针对某个请求进行配置,则可以:
1 2 3 4 5 6 7 @GetMapping(value = "/test", produces = "text/html") public String test (HttpServletRequest request, HttpSession session) { session.setMaxInactiveInterval(10 ); }
设置 session 过期时间为 10 秒后,并不表示不进行任何操作 10 秒后就能在控制台上看到执行销毁方法的信息,经过测试,大概会等 1 分钟,静静等待 1 分钟左右,控制台上显示:
com.itheima.a08.BeanForSession : 销毁
很遗憾没有办法看到application
作用范围的 Bean 执行销毁方法,因为 Spring 似乎并没有对 application
作用范围的 Bean 进行正确的销毁处理,因此在 Servlet 容器销毁时看不到 application
作用范围的 Bean 执行销毁方法。
8.2 Scope 失效分析
现有两个类
1 2 3 4 @Scope("prototype") @Component public class F1 {}
1 2 3 4 5 6 @Component @Getter public class E { @Autowired private F1 f1; }
执行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Slf4j @ComponentScan("com.atguigu.a09") public class A09Application { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (A09Application.class); E e = context.getBean(E.class); log.info("{}" , e.getF1()); log.info("{}" , e.getF1()); log.info("{}" , e.getF1()); context.close(); } }
F1
的 Scope
为 prototype
,向 E
中注入后,log.info("{}", e.getF1())
打印出的应该是不同的对象,可结果却相同
com.itheima.a09.A09Application - com.itheima.a09.F1@55183b20
com.itheima.a09.A09Application - com.itheima.a09.F1@55183b20
com.itheima.a09.A09Application - com.itheima.a09.F1@55183b20
获取到的f1
居然都是同一个,也就是说向单例对象中注入多例对象失败了。
对于单例对象来说,依赖注入仅发生了一次,后续不会再注入其他的 f1,因此 e 始终使用的是第一次注入的 f1
1 2 3 4 5 6 7 8 graph LR e1(e 创建) e2(e set 注入 f) f1(f 创建) e1-->f1-->e2
可以使用 @Lazy
生成代理对象,虽然代理对象依旧是同一个,但每次使用代理对象中的方法时,会由代理对象创建新的目标对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 graph LR e1(e 创建) e2(e set 注入 f 代理) f1(f 创建) f2(f 创建) f3(f 创建) e1-->e2 e2--使用 f 方法-->f1 e2--使用 f 方法-->f2 e2--使用 f 方法-->f3
方案一
1 2 3 4 5 6 7 @Component @Getter public class E { @Lazy @Autowired private F1 f1; }
执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Slf4j @ComponentScan("com.atguigu.a09") public class A09Application { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (A09Application.class); E e = context.getBean(E.class); log.info("{}" , e.getF1().getClass()); log.info("{}" , e.getF1()); log.info("{}" , e.getF1()); log.info("{}" , e.getF1()); context.close(); } }
com.itheima.a09.A09Application - class com.itheima.a09.F1EnhancerBySpringCGLIBEnhancerBySpringCGLIBf49b54ad
com.itheima.a09.A09Application - com.itheima.a09.F1@429bffaa
com.itheima.a09.A09Application - com.itheima.a09.F1@483f6d77
com.itheima.a09.A09Application - com.itheima.a09.F1@63a12c68
使用@Lazy
注解后,注入的是代理对象,每次获取到的f1
不再是同一个。
方案二
除了使用 @Lazy
注解外,可以使用 @Scope 注解的 proxyMode
属性指定代理模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) @Component public class F2 {} @Getter @Component public class E { @Autowired private F2 f2; }
执行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Slf4j @ComponentScan("com.atguigu.a09") public class A09Application { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (A09Application.class); E e = context.getBean(E.class); log.info("{}" , e.getF2().getClass()); log.info("{}" , e.getF2()); log.info("{}" , e.getF2()); log.info("{}" , e.getF2()); context.close(); } }
com.itheima.a09.A09Application - class com.itheima.a09.F2$$EnhancerBySpringCGLIB$$fbdf70d3
com.itheima.a09.A09Application - com.itheima.a09.F2@6d4d66d2
com.itheima.a09.A09Application - com.itheima.a09.F2@2a265ea9
com.itheima.a09.A09Application - com.itheima.a09.F2@11392934
方案三
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component @Scope(value = "prototype") public class F3 {} @Component public class E { @Autowired private ObjectFactory<F3> f3; public F3 getF3 () { return f3.getObject(); } }
执行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Slf4j @ComponentScan("com.atguigu.a09") public class A09Application { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (A09Application.class); E e = context.getBean(E.class); log.info("{}" , e.getF3().getClass()); log.info("{}" , e.getF3()); log.info("{}" , e.getF3()); log.info("{}" , e.getF3()); context.close(); } }
com.itheima.a09.A09Application - class com.itheima.a09.F3
com.itheima.a09.A09Application - com.itheima.a09.F3@25ce9dc4
com.itheima.a09.A09Application - com.itheima.a09.F3@17f62e33
com.itheima.a09.A09Application - com.itheima.a09.F3@27406a17
方案四
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component @Scope(value = "prototype") public class F4 {} @Component public class E { @Autowired private ApplicationContext applicationContext; public F4 getF4 () { return applicationContext.getBean(F4.class); } }
执行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Slf4j @ComponentScan("com.atguigu.a09") public class A09Application { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (A09Application.class); E e = context.getBean(E.class); log.info("{}" , e.getF4()); log.info("{}" , e.getF4()); context.close(); } }
com.itheima.a09.A09Application - com.itheima.a09.F4@54422e18
com.itheima.a09.A09Application - com.itheima.a09.F4@117159c0
如果对性能要求较高,则推荐使用后两种方式,前两种使用代理会有一定的性能损耗;如果不在乎那点性能损耗,则可以使用第一种方式,这种方式最简单。
四种解决方式虽然不同,但在理念上殊途同归,都是推迟了其他 Scope Bean 的获取,或者说按需加载。
9. AspectJ 编译器增强
新建一个 SpringBoot 项目,引入 Aop 相关依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency >
新建 Service 类
1 2 3 4 5 6 7 8 @Service public class MyService { private static final Logger log = LoggerFactory.getLogger(MyService.class); public void foo () { log.info("foo()" ); } }
新建切面类,当前切面类没有被 Spring 管理
1 2 3 4 5 6 7 8 9 @Aspect public class MyAspect { private static final Logger logger = LoggerFactory.getLogger(MyAspect.class); @Before("execution(* com.itheima.service.MyService.foo())") public void before () { logger.info("before()" ); } }
主启动类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @SpringBootApplication public class A10Application { private static final Logger log = LoggerFactory.getLogger(A10Application.class); public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A10Application.class, args); MyService service = context.getBean(MyService.class); log.info("service class: {}" , service.getClass()); service.foo(); context.close(); } }
com.itheima.A10Application : service class: class com.itheima.service.MyService
com.itheima.aop.MyAspect : before()
com.itheima.service.MyService : foo()
service.getClass()
打印出的是原始类的 Class 信息,而非代理类 Class 信息。
实际并没有通过代理进行增强,而是用 AspectJ 编译器进行增强,原理是 通过改写目标类源文件来增强
在 pom 中引入插件
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 26 27 28 <build > <plugins > <plugin > <groupId > org.codehaus.mojo</groupId > <artifactId > aspectj-maven-plugin</artifactId > <version > 1.11</version > <configuration > <complianceLevel > 1.8</complianceLevel > <source > 8</source > <target > 8</target > <showWeaveInfo > true</showWeaveInfo > <verbose > true</verbose > <Xlint > ignore</Xlint > <encoding > UTF-8</encoding > </configuration > <executions > <execution > <goals > <goal > compile</goal > <goal > test-compile</goal > </goals > </execution > </executions > </plugin > </plugins > </build >
使用 Maven 进行编译,查看编译后生成的 target
文件夹下的 MyService.class
文件
1 2 3 4 5 6 7 8 9 10 11 12 @Service public class MyService { private static final Logger log = LoggerFactory.getLogger(MyService.class); public MyService () { } public void foo () { MyAspect.aspectOf().before(); log.info("foo()" ); } }
编译后的代码中新增加了一行代码,MyAspect.aspectOf().before()
,编译时增强
既然如此,那么不依赖 Spring 容器也能实现方法的增强
1 2 3 4 5 6 7 8 9 public class A10Application { private static final Logger log = LoggerFactory.getLogger(A10Application.class); public static void main (String[] args) { MyService service = new MyService (); log.info("service class: {}" , service.getClass()); service.foo(); } }
com.itheima.A10Application : service class: class com.itheima.service.MyService
com.itheima.aop.MyAspect : before()
com.itheima.service.MyService : foo()
这种增强方式,可以突破一些代理的限制,代理本质上是通过方法重写来实现,如果目标是 static 的,那么代理是不能增强的
10. Agent 增强
Service 类
1 2 3 4 5 6 7 8 9 10 11 12 @Slf4j @Service public class MyService { final public void foo () { log.info("foo()" ); bar(); } public void bar () { log.info("bar()" ); } }
切面类
1 2 3 4 5 6 7 8 9 @Slf4j @Aspect public class MyAspect { @Before("execution(* com.itheima.service.MyService.*())") public void before () { log.info("before()" ); } }
主启动类
1 2 3 4 5 6 7 8 9 10 11 12 @Slf4j @SpringBootApplication public class A11Application { public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A11Application.class, args); MyService service = context.getBean(MyService.class); log.info("service class: {}" , service.getClass()); service.foo(); } }
com.itheima.A11Application : service class: class com.itheima.service.MyService
com.itheima.aop.MyAspect : before()
com.itheima.service.MyService : foo()
com.itheima.aop.MyAspect : before()
com.itheima.service.MyService : bar()
前提需要在resource
目录下新建 METE-INF
文件夹,并在 目录下新建 aop.xml
,其内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 <aspectj > <aspects > <aspect name ="com.itheima.aop.MyAspect" /> <weaver options ="-verbose -showWeaveInfo" > <include within ="com.itheima.service.MyService" /> <include within ="com.itheima.aop.MyAspect" /> </weaver > </aspects > </aspectj >
运行时添加 VM options
1 -javaagent:D:\environment\Maven\3.6.3-repository\.m2\repository\org\aspectj\aspectjweaver\1.9.7\aspectjweaver-1.9.7.jar
其中的 D:\environment\Maven\3.6.3-repository.m2 指本地 Maven 仓库地址,还需要确保本地仓库中存在 1.9.7 版本的 aspectjweaver,否则修改至对应版本。
从输出的内容可以看到 service.getClass()
打印出的信息也是原始类的 Class 信息,而非代理类的 Class 信息。因此不依赖 Spring 容器,直接 new
一个 MyService
实例并调用其 foo()
方法也能达到增强的目的。
如果查看 MyService
对应的 class 文件,会发现其内容并没有被修改,可以断定不是编译时增强,这里是在类加载时增强。
利用 Arthas
反编译类文件
可以借助阿里巴巴的 Arthas 来反编译加载的类文件,下载地址:下载 | arthas
在使用 Arthas 前,需要确保对应 Java 进程的存在,因此在上述代码中调用 service.foo()
方法后并没有关闭 Spring 容器。
解压下载的压缩包,进入 arthas-boot.jar
文件的同级目录,使用终端工具执行 java -jar .\arthas-boot.jar
运行之后会列举出存在的 Java 进程,找到需要连接的进程,之后输入目标进程对应的序号。当界面上成功显示 Arthas 的 Banner 时,证明连接成功:
输入 jad indi.mofan.service.MyService
表示需要反编译 MyService
:
可以看到 foo() 和 bar() 方法的第一行都被增加了一行代码,也就是这行代码对这两个方法实现了增强。
不仅如此,如果使用代理实现增强,被调用的 bar()
方法不会被成功增强,因为调用时默认使用了 this
关键词,表示调用的是原类中的方法,而不是代理类中的方法
11. 动态代理
11.1 JDK 动态代理
JDK 动态代理 只能 针对接口进行代理
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 26 27 28 public class JDKProxyDemo { interface Foo { void foo () ; } static class Target implements Foo { @Override public void foo () { System.out.println("Target foo" ); } } public static void main (String[] param) { Target target = new Target (); Foo proxy = (Foo) Proxy.newProxyInstance(JDKProxyDemo.class.getClassLoader(), new Class []{Foo.class}, (p, method, args) -> { System.out.println("before" ); Object result = method.invoke(target, args); System.out.println("after" ); return result; }); proxy.foo(); } }
before
Target foo
after
总结
代理对象和目标对象类似兄弟关系,都实现了相同接口,因此不能将代理对象强转成目标对象类型
代理类与目标类之间没有继承关系,因此目标类可以被final
修饰
11.2 CGLib 动态代理
CGLib 动态代理与 JDK 动态代理不一样,无需目标类实现某个特定的接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class CglibProxyDemo { static class Target { public void foo () { System.out.println("foo" ); } } public static void main (String[] param) { Target t = (Target) Enhancer.create(Target.class, (MethodInterceptor) (proxy, method, args, methodProxy) -> { System.out.println("before" ); Object result = methodProxy.invokeSuper(proxy, args); System.out.println("after" ); return result; }); t.foo(); } }
before
foo
after
调用目标方法的方式有三种
1 2 3 4 Object result = method.invoke(new Target (), args); Object result = methodProxy.invoke(new Target (), args); Object result = methodProxy.invokeSuper(proxy, args);
总结
与 JDK 动态代理相比,CGLib 动态代理无需实现接口
代理对象和目标对象是父子关系,也就是说代理类继承了目标类
由于代理类继承了目标类,因此目标类不能被 final 修饰,否则将报异常信息
代理类继承目标类后,通过重写目标类中的方法进行增强,因此方法不能被 final 修饰,否则将无法被增强,但不会抛出异常
12. JDK 动态代理原理
12.1 模拟 JDK 动态代理的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class A13 { interface Foo { void foo () ; } static class Target implements Foo { @Override public void foo () { System.out.println("Target.foo" ); } } public static void main (String[] args) { new $Proxy0 ().foo(); } }
代理类
1 2 3 4 5 6 7 8 9 public class $Proxy0 implements Foo { @Override public void foo () { System.out.println("before" ); new Target ().foo(); } }
before
Target.foo
代码看起来太简单了,但如果是 JDK 中的实现
功能增强 的代码实现会直接硬编码吗???
调用目标方法 一定要调用吗???存不存在满足条件才调用的场景???
也就是说,"功能增强"和"调用目标"这两部分的代码都是不确定的。
针对这种"不确定"的实现,可以提供一个抽象方法,等到用户具体使用时提供抽象的实现。
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 26 27 28 29 public class A13 { interface Foo { void foo () ; } interface InvocationHandler { void invoke () ; } static class Target implements Foo { @Override public void foo () { System.out.println("Target.foo" ); } } public static void main (String[] args) { Foo proxy = new $Proxy0 (new InvocationHandler () { @Override public void invoke () { System.out.println("before" ); new Target ().foo(); } }); proxy.foo(); } }
代理类
1 2 3 4 5 6 7 8 9 10 11 12 13 public class $Proxy0 implements Foo { private InvocationHandler h; public $Proxy0(InvocationHandler h) { this .h = h; } @Override public void foo () { h.invoke(); } }
before
Target.foo
多个抽象方法的接口
这样的实现依旧有问题,如果接口中提供了两个抽象方法呢?比如:
1 2 3 4 5 interface Foo { void foo () ; void bar () ; }
此时无论是目标类,还是代理类都要重写这个方法:
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 26 27 28 29 30 31 32 33 34 35 36 37 public class A13 { interface Foo { void foo () ; void bar () ; } interface InvocationHandler { void invoke () ; } static class Target implements Foo { @Override public void foo () { System.out.println("Target.foo" ); } @Override public void bar () { System.out.println("Target.bar" ); } } public static void main (String[] args) { Foo proxy = new $Proxy0 (new InvocationHandler () { @Override public void invoke () { System.out.println("before" ); new Target ().foo(); } }); proxy.foo(); proxy.bar(); } }
代理类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class $Proxy0 implements Foo { private InvocationHandler h; public $Proxy0(InvocationHandler h) { this .h = h; } @Override public void foo () { h.invoke(); } @Override public void bar () { h.invoke(); } }
before
Target.foo
before
Target.foo
proxy.foo()
和 proxy.bar()
输出的都是Target.foo
,因为实现 InvocationHandler
的 invoke()
方法时,依旧只调用了目标类的 foo()
方法,而不是 bar()
方法。
也就是说,在调用代理对象中的某个方法时,增强的应该是目标对象中对应的方法,希望在调用目标方法时能够动态编码。
那么可以在 invoke()
方法中添加两个入参,分别表示需要调用的目标方法和目标方法的参数:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 public class A13 { interface Foo { void foo () ; void bar () ; } interface InvocationHandler { void invoke (Method method, Object... args) throws Throwable; } static class Target implements Foo { @Override public void foo () { System.out.println("Target.foo" ); } @Override public void bar () { System.out.println("Target.bar" ); } } public static void main (String[] args) { Foo proxy = new $Proxy0 (new InvocationHandler () { @Override public void invoke (Method method, Object... args) throws Throwable { System.out.println("before" ); method.invoke(new Target (), args); } }); proxy.foo(); proxy.bar(); } }
代理类
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 26 27 28 public class $Proxy0 implements Foo { private InvocationHandler h; public $Proxy0(InvocationHandler h) { this .h = h; } @Override public void foo () { try { Method foo = Foo.class.getMethod("foo" ); h.invoke(foo, new Object [0 ]); } catch (Throwable e) { e.printStackTrace(); } } @Override public void bar () { try { Method bar = Foo.class.getMethod("bar" ); h.invoke(bar, new Object [0 ]); } catch (Throwable e) { e.printStackTrace(); } } }
before
Target.foo
before
Target.bar
有返回值的抽象方法
优化还在继续,如果抽象方法有返回值呢?比如:
1 2 3 4 5 interface Foo { void foo () ; int bar () ; }
实现了这个接口的目标类和代理类重写的方法都需要有具体的返回值:
1 2 3 4 5 6 7 8 9 10 11 12 static class Target implements Foo { @Override public void foo () { System.out.println("target foo" ); } @Override public int bar () { System.out.println("target bar" ); return 1 ; } }
目标类可以直接返回,那代理类返回什么?
InvocationHandler
的 invoke()
方法是对"功能增强"和"调用目标"的抽象,因此可以使 invoke()
方法也返回一个值,返回的值即为目标方法的返回值,这样就可以使得代理类中的方法有值可返。
1 2 3 interface InvocationHandler { Object invoke (Method method, Object[] params) throws Throwable; }
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 26 27 28 29 30 31 32 public class $Proxy0 implements Foo { private final InvocationHandler h; public $Proxy0(InvocationHandler h) { this .h = h; } @Override public void foo () { try { Method foo = Foo.class.getMethod("foo" ); h.invoke(foo, new Object [0 ]); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } @Override public int bar () { try { Method bar = Foo.class.getMethod("bar" ); return (int ) h.invoke(bar, new Object [0 ]); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } }
修改 main()方法,打印 bar()
方法的返回值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main (String[] args) { $Proxy0 proxy = new $Proxy0 (new InvocationHandler () { @Override public Object invoke (Method method, Object[] params) throws Throwable { System.out.println("before..." ); return method.invoke(new Target (), params); } }); proxy.foo(); System.out.println(proxy.bar()); }
before...
target foo
before...
target bar
1
在静态代码块里创建 Method 实例
每调用一次代理对象中的方法都会创建一个 Method 实例,这些实例是可以复用的,因此可以将这些实例的创建移动到静态代码块中:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class $Proxy0 implements Foo { private InvocationHandler h; static Method foo; static Method bar; static { try { foo = Foo.class.getMethod("foo" ); bar = Foo.class.getMethod("bar" ); } catch (NoSuchMethodException e) { throw new NoSuchMethodError (e.getMessage()); } } public $Proxy0(InvocationHandler h) { this .h = h; } @Override public void foo () { try { h.invoke(foo, new Object [0 ]); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } @Override public int bar () { try { Object result = h.invoke(bar, new Object [0 ]); return (int ) result; } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } }
同时对异常进行处理,运行时异常直接抛出,编译时异常转换为运行时异常抛出。
invoke()
方法增加代理对象作为参数
在 JDK 提供的 InvocationHandler
接口的 invoke()
方法还将代理对象作为方法的参数,以便用户根据实际情况使用。继续修改自定义的 InvocationHandler
接口:
1 2 3 interface InvocationHandler { Object invoke (Object proxy, Method method, Object[] params) throws Throwable; }
修改代理类中对 invoke()
方法的调用,第一个参数为当前类的实例,即 this
:
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 26 27 28 public class $Proxy0 implements Foo { @Override public void foo () { try { h.invoke(this , foo, new Object [0 ]); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } @Override public int bar () { try { return (int ) h.invoke(this , bar, new Object [0 ]); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } }
main()方法重写的 invoke()
方法也要增加 proxy
参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void main (String[] args) { $Proxy0 proxy = new $Proxy0 (new InvocationHandler () { @Override public Object invoke (Object proxy,Method method, Object[] params) throws Throwable { System.out.println("before..." ); return method.invoke(new Target (), params); } }); proxy.foo(); System.out.println(proxy.bar()); }
运行 main()方法,结果不发生变化。
对照 JDK
到目前为止,我们自定义的InvocationHandler
接口,和 JDK 提供的接口基本无异,注释自定义的InvocationHandler
接口,替换为 JDK 提供的。
在 JDK 提供的 InvocationHandler
接口的注释中有一句:See Also: Proxy
,在 Proxy 类的代码中有:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Proxy implements java .io.Serializable { protected InvocationHandler h; protected Proxy (InvocationHandler h) { Objects.requireNonNull(h); this .h = h; } }
Proxy
类中有一个 InvocationHandler
对象的成员变量。
因此还可以使代理类 $Proxy0
继承 Proxy
来进一步优化代码:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class $Proxy0 extends Proxy implements Foo { static Method foo; static Method bar; static { try { foo = Foo.class.getMethod("foo" ); bar = Foo.class.getMethod("bar" ); } catch (NoSuchMethodException e) { throw new NoSuchMethodError (e.getMessage()); } } public $Proxy0(InvocationHandler h) { super (h); } @Override public void foo () { try { h.invoke(this , foo, new Object [0 ]); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } @Override public int bar () { try { Object result = h.invoke(this , bar, new Object [0 ]); return (int ) result; } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } }
12.2 JDK 代理源码
JDK 动态代理生成的代理类是以字节码的形式存在的
利用 Arthas
反编译代理类字节码文件
要使用 Arthas 的反编译功能需要满足两个条件:
知道被反编译文件的全限定类名
程序不能中断,需要存在 Java 进程
为了满足这个条件,可以在控制台打印出生成的代理类的全限定类名,然后利用阻塞 IO 使程序不中断:
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 26 27 28 29 30 31 32 public class JDKProxyDemo { interface Foo { void foo () ; } static class Target implements Foo { @Override public void foo () { System.out.println("Target foo" ); } } public static void main (String[] param) throws Exception { Target target = new Target (); Foo proxy = (Foo) Proxy.newProxyInstance(JDKProxyDemo.class.getClassLoader(), new Class []{Foo.class}, (p, method, args) -> { System.out.println("before" ); Object result = method.invoke(target, args); System.out.println("after" ); return result; }); System.out.println(proxy.getClass()); proxy.foo(); System.in.read(); } }
class com.itheima.a12.$Proxy0
before
Target foo
after
运行 main()方法,在当前项目目录下生成 com.itheima.a12.$Proxy0.class 文件,查看其内容:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 package com.itheima.a12;import com.itheima.a12.JDKProxyDemo;import java.lang.invoke.MethodHandles;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.lang.reflect.UndeclaredThrowableException;final class $Proxy0 extends Proxy implements JDKProxyDemo .Foo { private static final Method m0; private static final Method m1; private static final Method m2; private static final Method m3; private static MethodHandles.Lookup proxyClassLookup (MethodHandles.Lookup lookup) throws IllegalAccessException { if (lookup.lookupClass() == Proxy.class && lookup.hasFullPrivilegeAccess()) { return MethodHandles.lookup(); } throw new IllegalAccessException (lookup.toString()); } public $Proxy0(InvocationHandler invocationHandler) { super (invocationHandler); } static { try { m0 = Class.forName("java.lang.Object" ).getMethod("hashCode" , new Class [0 ]); m1 = Class.forName("java.lang.Object" ).getMethod("equals" , Class.forName("java.lang.Object" )); m2 = Class.forName("java.lang.Object" ).getMethod("toString" , new Class [0 ]); m3 = Class.forName("com.atguigu.a12.JDKProxyDemo$Foo" ).getMethod("foo" , new Class [0 ]); return ; } catch (NoSuchMethodException noSuchMethodException) { throw new NoSuchMethodError (noSuchMethodException.getMessage()); } catch (ClassNotFoundException classNotFoundException) { throw new NoClassDefFoundError (classNotFoundException.getMessage()); } } public final boolean equals (Object object) { try { return (Boolean)this .h.invoke(this , m1, new Object []{object}); } catch (Error | RuntimeException throwable) { throw throwable; } catch (Throwable throwable) { throw new UndeclaredThrowableException (throwable); } } public final String toString () { try { return (String)this .h.invoke(this , m2, null ); } catch (Error | RuntimeException throwable) { throw throwable; } catch (Throwable throwable) { throw new UndeclaredThrowableException (throwable); } } public final int hashCode () { try { return (Integer)this .h.invoke(this , m0, null ); } catch (Error | RuntimeException throwable) { throw throwable; } catch (Throwable throwable) { throw new UndeclaredThrowableException (throwable); } } public final void foo () { try { this .h.invoke(this , m3, null ); return ; } catch (Error | RuntimeException throwable) { throw throwable; } catch (Throwable throwable) { throw new UndeclaredThrowableException (throwable); } } }
其内容与自定义的 $Proxy0
几乎无异,只不过 JDK 生成的代理类信息还生成 equals()
、toString()
和 hashCode()
三个方法对应的 Method
对象,并对它们也进行了相同的增强。
12.3 JDK 代理类字节码生成
JDK 在生成代理类时,没有经历源码、编译阶段,而是直接采用字节码,使用了 ASM 来完成。
ASM 的学习成本较高,在此不做过多介绍,本节将采用一直“曲线求国”的方式,使用 IDEA 的 Byte Code Analyzer
插件将 Java 源码转换成使用 ASM 编写的代码,建议在 Java8 环境下使用。
编写接口和代理类
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 26 27 28 29 public interface Foo { void foo () ; } public class $Proxy0 extends Proxy implements Foo { private static Method method; static { try { method = Foo.class.getMethod("foo" ); } catch (NoSuchMethodException e) { throw new NoSuchMethodError (e.getMessage()); } } public $Proxy0(InvocationHandler h) { super (h); } @Override public void foo () { try { this .h.invoke(this , method, null ); } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } }
编译后,右键 Analyze Byte Code
,查看 ASM,拷贝其内容,复制到$Proxy0Dump
中
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 package com.itheima;import org.springframework.asm.*;public class $Proxy0Dump implements Opcodes { public static byte [] dump() throws Exception { ClassWriter classWriter = new ClassWriter (0 ); FieldVisitor fieldVisitor; RecordComponentVisitor recordComponentVisitor; MethodVisitor methodVisitor; AnnotationVisitor annotationVisitor0; classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/itheima/$Proxy0" , null , "java/lang/reflect/Proxy" , new String []{"com/itheima/Foo" }); classWriter.visitSource("$Proxy0.java" , null ); { fieldVisitor = classWriter.visitField(ACC_PRIVATE | ACC_STATIC, "method" , "Ljava/lang/reflect/Method;" , null , null ); fieldVisitor.visitEnd(); } { methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>" , "(Ljava/lang/reflect/InvocationHandler;)V" , null , null ); methodVisitor.visitCode(); Label label0 = new Label (); methodVisitor.visitLabel(label0); methodVisitor.visitLineNumber(25 , label0); methodVisitor.visitVarInsn(ALOAD, 0 ); methodVisitor.visitVarInsn(ALOAD, 1 ); methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/reflect/Proxy" , "<init>" , "(Ljava/lang/reflect/InvocationHandler;)V" , false ); Label label1 = new Label (); methodVisitor.visitLabel(label1); methodVisitor.visitLineNumber(26 , label1); methodVisitor.visitInsn(RETURN); Label label2 = new Label (); methodVisitor.visitLabel(label2); methodVisitor.visitLocalVariable("this" , "Lcom/itheima/$Proxy0;" , null , label0, label2, 0 ); methodVisitor.visitLocalVariable("h" , "Ljava/lang/reflect/InvocationHandler;" , null , label0, label2, 1 ); methodVisitor.visitMaxs(2 , 2 ); methodVisitor.visitEnd(); } { methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "foo" , "()V" , null , null ); methodVisitor.visitCode(); Label label0 = new Label (); Label label1 = new Label (); Label label2 = new Label (); methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/lang/Throwable" ); methodVisitor.visitLabel(label0); methodVisitor.visitLineNumber(31 , label0); methodVisitor.visitVarInsn(ALOAD, 0 ); methodVisitor.visitFieldInsn(GETFIELD, "com/itheima/$Proxy0" , "h" , "Ljava/lang/reflect/InvocationHandler;" ); methodVisitor.visitVarInsn(ALOAD, 0 ); methodVisitor.visitFieldInsn(GETSTATIC, "com/itheima/$Proxy0" , "method" , "Ljava/lang/reflect/Method;" ); methodVisitor.visitInsn(ACONST_NULL); methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/lang/reflect/InvocationHandler" , "invoke" , "(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;" , true ); methodVisitor.visitInsn(POP); methodVisitor.visitLabel(label1); methodVisitor.visitLineNumber(34 , label1); Label label3 = new Label (); methodVisitor.visitJumpInsn(GOTO, label3); methodVisitor.visitLabel(label2); methodVisitor.visitLineNumber(32 , label2); methodVisitor.visitFrame(Opcodes.F_SAME1, 0 , null , 1 , new Object []{"java/lang/Throwable" }); methodVisitor.visitVarInsn(ASTORE, 1 ); Label label4 = new Label (); methodVisitor.visitLabel(label4); methodVisitor.visitLineNumber(33 , label4); methodVisitor.visitTypeInsn(NEW, "java/lang/reflect/UndeclaredThrowableException" ); methodVisitor.visitInsn(DUP); methodVisitor.visitVarInsn(ALOAD, 1 ); methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/reflect/UndeclaredThrowableException" , "<init>" , "(Ljava/lang/Throwable;)V" , false ); methodVisitor.visitInsn(ATHROW); methodVisitor.visitLabel(label3); methodVisitor.visitLineNumber(35 , label3); methodVisitor.visitFrame(Opcodes.F_SAME, 0 , null , 0 , null ); methodVisitor.visitInsn(RETURN); Label label5 = new Label (); methodVisitor.visitLabel(label5); methodVisitor.visitLocalVariable("e" , "Ljava/lang/Throwable;" , null , label4, label3, 1 ); methodVisitor.visitLocalVariable("this" , "Lcom/itheima/$Proxy0;" , null , label0, label5, 0 ); methodVisitor.visitMaxs(4 , 2 ); methodVisitor.visitEnd(); } { methodVisitor = classWriter.visitMethod(ACC_STATIC, "<clinit>" , "()V" , null , null ); methodVisitor.visitCode(); Label label0 = new Label (); Label label1 = new Label (); Label label2 = new Label (); methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/lang/NoSuchMethodException" ); methodVisitor.visitLabel(label0); methodVisitor.visitLineNumber(18 , label0); methodVisitor.visitLdcInsn(Type.getType("Lcom/itheima/Foo;" )); methodVisitor.visitLdcInsn("foo" ); methodVisitor.visitInsn(ICONST_0); methodVisitor.visitTypeInsn(ANEWARRAY, "java/lang/Class" ); methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class" , "getMethod" , "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;" , false ); methodVisitor.visitFieldInsn(PUTSTATIC, "com/itheima/$Proxy0" , "method" , "Ljava/lang/reflect/Method;" ); methodVisitor.visitLabel(label1); methodVisitor.visitLineNumber(21 , label1); Label label3 = new Label (); methodVisitor.visitJumpInsn(GOTO, label3); methodVisitor.visitLabel(label2); methodVisitor.visitLineNumber(19 , label2); methodVisitor.visitFrame(Opcodes.F_SAME1, 0 , null , 1 , new Object []{"java/lang/NoSuchMethodException" }); methodVisitor.visitVarInsn(ASTORE, 0 ); Label label4 = new Label (); methodVisitor.visitLabel(label4); methodVisitor.visitLineNumber(20 , label4); methodVisitor.visitTypeInsn(NEW, "java/lang/NoSuchMethodError" ); methodVisitor.visitInsn(DUP); methodVisitor.visitVarInsn(ALOAD, 0 ); methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/NoSuchMethodException" , "getMessage" , "()Ljava/lang/String;" , false ); methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/NoSuchMethodError" , "<init>" , "(Ljava/lang/String;)V" , false ); methodVisitor.visitInsn(ATHROW); methodVisitor.visitLabel(label3); methodVisitor.visitLineNumber(22 , label3); methodVisitor.visitFrame(Opcodes.F_SAME, 0 , null , 0 , null ); methodVisitor.visitInsn(RETURN); methodVisitor.visitLocalVariable("e" , "Ljava/lang/NoSuchMethodException;" , null , label4, label3, 0 ); methodVisitor.visitMaxs(3 , 1 ); methodVisitor.visitEnd(); } classWriter.visitEnd(); return classWriter.toByteArray(); } }
编写测试方法使用 $Proxy0Dump
生成 $Proxy0
的 class 文件:
1 2 3 4 5 6 7 8 9 public class TestProxy { public static void main (String[] args) throws Exception { byte [] dump = $Proxy0Dump.dump(); FileOutputStream os = new FileOutputStream ("$Proxy0.class" ); os.write(dump, 0 , dump.length); os.close(); } }
反编译后的内容与手动编写的 $Proxy0.java
文件的内容无异。
实际使用时并不需要使用 $Proxy0Dump
生成 $Proxy.class
文件,而是利用 ClassLoader 直接加载类信息:
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 26 27 public class TestProxy { public static void main (String[] args) throws Exception { byte [] dump = $Proxy0Dump.dump(); ClassLoader classLoader = new ClassLoader () { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { return super .defineClass(name, dump, 0 , dump.length); } }; Class<?> proxyClass = classLoader.loadClass("com.itheima.$Proxy0" ); Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class); Foo proxy = (Foo) constructor.newInstance(new InvocationHandler () { @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { System.out.println("before" ); System.out.println("调用目标" ); System.out.println("after" ); return null ; } }); proxy.foo(); } }
before
调用目标
after
12.4 JDK 反射优化
使用 JDK 的动态代理时,会使用反射调用方法:
1 Object result = method.invoke(target, params);
相比于正常调用方法,利用反射的性能要稍微低一些,JDK 对反射进行了优化
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 26 27 28 29 30 public class TestMethodProxy { public static void main (String[] args) throws Exception { Method foo = TestMethodProxy.class.getMethod("foo" , int .class); for (int i = 1 ; i <= 17 ; i++) { show(i, foo); foo.invoke(null , i); } System.in.read(); } private static void show (int i, Method foo) throws Exception { Method getMethodAccessor = Method.class.getDeclaredMethod("getMethodAccessor" ); getMethodAccessor.setAccessible(true ); Object invoke = getMethodAccessor.invoke(foo); if (invoke == null ) { System.out.println(i + ":" + null ); return ; } Field delegate = Class.forName("sun.reflect.DelegatingMethodAccessorImpl" ).getDeclaredField("delegate" ); delegate.setAccessible(true ); System.out.println(i + ": " + delegate.get(invoke)); } public static void foo (int i) { System.out.println(i + ": foo" ); } }
1: null
1: foo
2: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
2: foo
3: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
3: foo
4: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
4: foo
5: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
5: foo
6: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
6: foo
7: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
7: foo
8: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
8: foo
9: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
9: foo
10: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
10: foo
11: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
11: foo
12: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
12: foo
13: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
13: foo
14: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
14: foo
15: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
15: foo
16: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
16: foo
17: sun.reflect.GeneratedMethodAccessor2@5b2133b1
17: foo
从上述信息可知,第一次调用时没有使用 MethodAccessor
对象,从第二次到第十六次,使用了 NativeMethodAccessorImpl
对象,而在第十七次使用了 GeneratedMethodAccessor2
对象。
NativeMethodAccessorImpl
基于 Java 本地 API 实现,性能较低,第十七次调用换成 GeneratedMethodAccessor2
后,性能得到一定的提升。
使用 Arthas
反编译查看 GeneratedMethodAccessor2
类中的信息,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class GeneratedMethodAccessor2 extends MethodAccessorImpl { public Object invoke (Object object, Object[] objectArray) throws InvocationTargetException { try { TestMethodProxy.foo((int )c); return null ; } catch (Throwable throwable) { throw new InvocationTargetException (throwable); } catch (ClassCastException | NullPointerException runtimeException) { throw new IllegalArgumentException (super .toString()); } } }
反编译得到的代码中,不再是通过反射调用方法,而是直接正常调用方法,即:
1 TestMethodProxy.foo((int )c);
性能得到了提升,但这样的提升也是有一定代价的:为优化 一个 方法的反射调用,生成了一个 GeneratedMethodAccessor2
代理类。
13. CGLib 动态代理原理
13.1 CGLib 动态代理的模拟
目标类
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Target { public void save () { System.out.println("save()" ); } public void save (int i) { System.out.println("save(int)" ); } public void save (long i) { System.out.println("save(long)" ); } }
CGLib 动态代理生成的代理类:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public class Proxy extends Target { private MethodInterceptor methodInterceptor; public void setMethodInterceptor (MethodInterceptor methodInterceptor) { this .methodInterceptor = methodInterceptor; } static Method save0; static Method save1; static Method save2; static { try { save0 = Target.class.getMethod("save" ); save1 = Target.class.getMethod("save" , int .class); save2 = Target.class.getMethod("save" , long .class); } catch (NoSuchMethodException e) { throw new NoSuchMethodError (e.getMessage()); } } @Override public void save () { try { methodInterceptor.intercept(this , save0, new Object [0 ], null ); } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } @Override public void save (int i) { try { methodInterceptor.intercept(this , save1, new Object []{i}, null ); } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } @Override public void save (long i) { try { methodInterceptor.intercept(this , save2, new Object []{i}, null ); } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static void main (String[] args) { Target target = new Target (); Proxy proxy = new Proxy (); proxy.setMethodInterceptor(new MethodInterceptor () { @Override public Object intercept (Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println("before" ); return method.invoke(target, args); } }); proxy.save(); proxy.save(1 ); proxy.save(2L ); }
before
save()
before
save(int)
before
save(long)
13.2 MethodProxy
在上述 Proxy
类中,重写了父类中的方法,并在重写的方法中调用了 intercept()
方法,重写的这些方法相当于是带增强功能的方法。
在 JDK 的动态代理中,使用反射对方法进行调用,而在 CGLib 动态代理中,可以使用 intercept()
方法中 MethodProxy
类型的参数实现不经过反射来调用方法。
接收的 MethodProxy
类型的参数可以像 Method 类型的参数一样,在静态代码块中被实例化。
可以通过静态方法 MethodProxy.create()
来创建 MethodProxy
对象:
1 2 3 4 5 6 7 public static MethodProxy create (Class c1, Class c2, String desc, String name1, String name2) { MethodProxy proxy = new MethodProxy (); proxy.sig1 = new Signature (name1, desc); proxy.sig2 = new Signature (name2, desc); proxy.createInfo = new CreateInfo (c1, c2); return proxy; }
参数 c1
指目标类(或者说原始类)的 Class
对象;
参数 c2
指代理类的 Class
对象;
参数 desc
指方法描述符;
参数 name1
指带 增强 功能的方法名称;
参数 name2
指带 原始 功能的方法名称。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 public class Proxy extends Target { private MethodInterceptor methodInterceptor; public void setMethodInterceptor (MethodInterceptor methodInterceptor) { this .methodInterceptor = methodInterceptor; } static Method save0; static Method save1; static Method save2; static MethodProxy save0Proxy; static MethodProxy save1Proxy; static MethodProxy save2Proxy; static { try { save0 = Target.class.getMethod("save" ); save1 = Target.class.getMethod("save" , int .class); save2 = Target.class.getMethod("save" , long .class); save0Proxy = MethodProxy.create(Target.class, Proxy.class, "()V" , "save" , "saveSuper" ); save1Proxy = MethodProxy.create(Target.class, Proxy.class, "(I)V" , "save" , "saveSuper" ); save2Proxy = MethodProxy.create(Target.class, Proxy.class, "(J)V" , "save" , "saveSuper" ); } catch (NoSuchMethodException e) { throw new NoSuchMethodError (e.getMessage()); } } public void saveSuper () { super .save(); } public void saveSuper (int i) { super .save(i); } public void saveSuper (long i) { super .save(i); } @Override public void save () { try { methodInterceptor.intercept(this , save0, new Object [0 ], save0Proxy); } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } @Override public void save (int i) { try { methodInterceptor.intercept(this , save1, new Object []{i}, save1Proxy); } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } @Override public void save (long i) { try { methodInterceptor.intercept(this , save2, new Object []{i}, save2Proxy); } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class TestProxy { public static void main (String[] args) { Proxy proxy = new Proxy (); proxy.setMethodInterceptor(new MethodInterceptor () { @Override public Object intercept (Object p, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println("before" ); return methodProxy.invokeSuper(p, args); } }); proxy.save(); proxy.save(1 ); proxy.save(2L ); } }
14. MethodProxy 原理
调用 methodProxy.invoke()
方法时,会额外创建一个代理类,该代理类配合目标对象使用。
调用 methodProxy.invokeSuper()
方法时,也会额外创建一个代理类,该代理类配合代理对象使用。
当调用 MethodProxy
对象的 invoke()
方法或 invokeSuper()
方法时,就会生成这两个代理类,它们都继承至 FastClass
。
FastClass
是一个抽象类,其内部有多个抽象方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 public abstract class FastClass { public abstract int getIndex (String var1, Class[] var2) ; public abstract int getIndex (Class[] var1) ; public abstract Object invoke (int var1, Object var2, Object[] var3) throws InvocationTargetException; public abstract Object newInstance (int var1, Object[] var2) throws InvocationTargetException; public abstract int getIndex (Signature signature) ; public abstract int getMaxIndex () ; }
重点关注 invoke()
方法与 getIndex(Signature signature)
方法。
模拟生成的与目标类相关的代理类
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public class TargetFastClass { static Signature s0 = new Signature ("save" , "()V" ); static Signature s1 = new Signature ("save" , "(I)V" ); static Signature s2 = new Signature ("save" , "(J)V" ); public int getIndex (Signature signature) { if (s0.equals(signature)) { return 0 ; } else if (s1.equals(signature)) { return 1 ; } else if (s2.equals(signature)) { return 2 ; } return -1 ; } public Object invoke (int index, Object target, Object[] args) { if (index == 0 ) { ((Target) target).save(); return null ; } else if (index == 1 ) { ((Target) target).save((int ) args[0 ]); return null ; } else if (index == 2 ) { ((Target) target).save((long ) args[0 ]); return null ; } else { throw new RuntimeException ("无此异常" ); } } public static void main (String[] args) { TargetFastClass fastClass = new TargetFastClass (); int index = fastClass.getIndex(new Signature ("save" , "()V" )); fastClass.invoke(index, new Target (), new Object [0 ]); index = fastClass.getIndex(new Signature ("save" , "(J)V" )); fastClass.invoke(index, new Target (), new Object []{2L }); } }
save()
save(long)
模拟生成的与代理类相关的代理类
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public class ProxyFastClass { static Signature s0 = new Signature ("saveSuper" , "()V" ); static Signature s1 = new Signature ("saveSuper" , "(I)V" ); static Signature s2 = new Signature ("saveSuper" , "(J)V" ); public int getIndex (Signature signature) { if (s0.equals(signature)) { return 0 ; } else if (s1.equals(signature)) { return 1 ; } else if (s2.equals(signature)) { return 2 ; } return -1 ; } public Object invoke (int index, Object proxy, Object[] args) { if (index == 0 ) { ((Proxy) proxy).save(); return null ; } else if (index == 1 ) { ((Proxy) proxy).save((int ) args[0 ]); return null ; } else if (index == 2 ) { ((Proxy) proxy).save((long ) args[0 ]); return null ; } else { throw new RuntimeException ("无此异常" ); } } public static void main (String[] args) { ProxyFastClass proxyFastClass = new ProxyFastClass (); int index = proxyFastClass.getIndex(new Signature ("saveSuper" , "()V" )); proxyFastClass.invoke(index, new Proxy (), new Object [0 ]); } }
save()
总结
调用 MethodProxy.create()
方法创建 MethodProxy
对象时,要求传递带增强功能的方法名、带原始功能的方法名以及方法描述符。
根据方法名和方法描述符可以在调用生成的两个代理类中的 getIndex()
方法时获取方法的编号,之后:
调用 methodProxy.invoke()
方法时,就相当于调用 TargetFastClass
中的 invoke()
方法,并在这个 invoke()
方法中正常调用目标对象方法(Spring 底层的选择)。
调用 methodProxy.invokeSuper()
方法时,就相当于调用 ProxyFastClass
中的 invoke()
方法,并在这个 invoke()
方法中正常调用代理对象中带原始功能的方法。
与 JDK 中优化反射调用方法的对比
在 JDK 中需要反射调用 16 次方法后才会生成优化反射调用的代理类,而在 CGLib 中,当调用 MethodProxy.create()
方法时就会生成优化反射调用的代理类;
在 JDK 中一个方法的反射调用优化就要生成一个代理类,而在 CGLib 中,一个代理类生成两个 FastClass
代理类,每个FastClass
可以匹配到多个方法。
15. JDK 和 CGLib 的统一
15.1 advisor
切面有 aspect
和 advisor
两个概念,aspect
是多组通知(advice)和切点(pointcut)的组合,也是实际编码时使用的,advisor
则是更细粒度的切面,仅包含一个通知和切点,aspect
在生效之前会被拆解成多个 advisor
。
Spring 中对切点、通知、切面的抽象如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 classDiagram class Advice class MethodInterceptor class Advisor class PointcutAdvisor Pointcut <|-- AspectJExpressionPointcut Advice <|-- MethodInterceptor Advisor <|-- PointcutAdvisor PointcutAdvisor o-- "一" Pointcut PointcutAdvisor o-- "一" Advice <<interface > > Advice <<interface > > MethodInterceptor <<interface > > Pointcut <<interface > > Advisor <<interface > > PointcutAdvisor
本节将重点介绍 advisor
切面。
15.2 切面与代理对象的创建
在 Spring 中,切点通过接口 org.springframework.aop.Pointcut
来表示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public interface Pointcut { ClassFilter getClassFilter () ; MethodMatcher getMethodMatcher () ; Pointcut TRUE = TruePointcut.INSTANCE; }
Pointcut
接口有很多实现类,比如:
AnnotationMatchingPointcut
:通过注解进行匹配
AspectJExpressionPointcut
:通过 AspectJ 表达式进行匹配(本节的选择)
在 Spring 中,通知的表示也有很多接口,在此介绍最基本、最重要的接口 org.aopalliance.intercept.MethodInterceptor
,这个接口实现的通知属于环绕通知。
在 Spring 中,切面的实现也有很多,在此选择 DefaultPointcutAdvisor
,创建这种切面时,传递一个切点和通知。
最后创建代理对象时,无需显式实现 JDK 动态代理或 CGLib 动态代理,Spring 提供了名为 ProxyFactory
的工厂,其内部通过不同的情况选择不同的代理实现,更方便地创建代理对象。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 interface I1 { void foo () ; void bar () ; } static class Target1 implements I1 { @Override public void foo () { System.out.println("target1 foo" ); } @Override public void bar () { System.out.println("target1 bar" ); } } static class Target2 { public void foo () { System.out.println("target2 foo" ); } public void bar () { System.out.println("target2 bar" ); } } public static void main (String[] args) { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut (); pointcut.setExpression("execution(* foo())" ); MethodInterceptor advice = invocation -> { System.out.println("before..." ); Object result = invocation.proceed(); System.out.println("after..." ); return result; }; DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor (pointcut, advice); Target1 target = new Target1 (); ProxyFactory factory = new ProxyFactory (); factory.setTarget(target); factory.addAdvisor(advisor); I1 proxy = (I1) factory.getProxy(); System.out.println(proxy.getClass()); proxy.foo(); proxy.bar(); }
class com.itheima.a15.A15$Target1$$EnhancerBySpringCGLIB$$933570ca
before...
target1 foo
after...
target1 bar
foo()
方法被增强,但 bar()
并没有,并且选择了 CGLib
动态代理作为代理的实现。
Spring 是根据什么信息来选择不同的动态代理实现呢?
ProxyFactory
的父类 ProxyConfig
中有个名为 proxyTargetClass
的布尔类型成员变量:
当 proxyTargetClass == false
,并且目标对象所在类实现了接口时,将选择 JDK 动态代理;
当 proxyTargetClass == false
,但目标对象所在类未实现接口时,将选择 CGLib 动态代理;
当 proxyTargetClass == true
,总是选择 CGLib 动态代理。
上文中的 target
对象的所在类 Targer1
实现了 I1
接口,最终为什么依旧选择了 CGLib
动态代理作为代理类的创建方式呢?
这是因为并没有显式这是 target
对象的实现类,Spring
认为其并未实现接口。
设置 factory 对象的 interfaces 信息:
1 factory.setInterfaces(target.getClass().getInterfaces());
class com.itheima.a15.$Proxy0
before
target1 foo
after
target1 bar
此时选择的动态代理实现方式是 JDK 动态代理。
再设置 factory 对象的 proxyTargetClass
为 true
:
1 factory.setProxyTargetClass(true );
class com.itheima.a15.A15$Target1$$EnhancerBySpringCGLIB$$933570ca
before...
target1 foo
after...
target1 bar
此时选择的动态代理实现方式是 CGLib 动态代理。
再将 proxyTargetClass
的值修改回 false
,并修改目标对象的所在类为 Target2
,Target2
并未实现任何接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static void main (String[] args) { Target2 target = new Target2 (); ProxyFactory factory = new ProxyFactory (); factory.setTarget(target); factory.addAdvisor(advisor); factory.setInterfaces(target.getClass().getInterfaces()); factory.setProxyTargetClass(false ); Target2 proxy = (Target2) factory.getProxy(); System.out.println(proxy.getClass()); proxy.foo(); proxy.bar(); }
class com.itheima.a15.A15$Target1$$EnhancerBySpringCGLIB$$933570ca
before...
target1 foo
after...
target1 bar
此时选择的动态代理实现方式是 CGLib 动态代理。
ProxyFactory
是用来创建代理的核心实现,使用 AopProxyFactory
选择具体的代理实现:
JdkDynamicAopProxy
ObjenesisCglibAopProxy
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 26 27 28 29 30 31 32 33 34 classDiagram Advised <|-- ProxyFactory ProxyFactory o-- Target ProxyFactory o-- "多" Advisor ProxyFactory --> AopProxyFactory : 使用 AopProxyFactory --> AopProxy Advised <|-- 基于 CGLIB 的 Proxy 基于 CGLIB 的 Proxy <-- ObjenesisCglibAopProxy : 创建 AopProxy <|-- ObjenesisCglibAopProxy AopProxy <|-- JdkDynamicAopProxy 基于 JDK 的 Proxy <-- JdkDynamicAopProxy : 创建 Advised <|-- 基于 JDK 的 Proxy class AopProxy { +getProxy() Object } class ProxyFactory { proxyTargetClass : boolean } class ObjenesisCglibAopProxy { advised : ProxyFactory } class JdkDynamicAopProxy { advised : ProxyFactory } <<interface > > Advised <<interface > > AopProxyFactory <<interface > > AopProxy
AopProxyFactory
根据 proxyTargetClass
等设置选择 AopProxy
实现,AopProxy
通过 getProxy()
方法创建代理对象。
上述类图中的类与接口都实现了 Advised
接口,能够获得关联的切面集合与目标(实际上是从 ProxyFactory
中获取的)。
调用代理方法时,会借助 ProxyFactory
统一将通知转换为环绕通知 MethodInterceptor
。
16. 切点匹配
上一节中,选择 AspectJExpressionPointcut
作为切点的实现,判断编写的 AspectJ 表达式是否与某一方法匹配可以使用其 matches()
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static void main (String[] args) throws NoSuchMethodException { AspectJExpressionPointcut pt1 = new AspectJExpressionPointcut (); pt1.setExpression("execution(* bar())" ); System.out.println(pt1.matches(T1.class.getMethod("foo" ), T1.class)); System.out.println(pt1.matches(T1.class.getMethod("bar" ), T1.class)); AspectJExpressionPointcut pt2 = new AspectJExpressionPointcut (); pt2.setExpression("@annotation(org.springframework.transaction.annotation.Transactional)" ); System.out.println(pt2.matches(T1.class.getMethod("foo" ), T1.class)); System.out.println(pt2.matches(T1.class.getMethod("bar" ), T1.class)); } static class T1 { @Transactional public void foo () { } public void bar () { } }
false
true
true
false
@Transactional
是 Spring 中使用频率非常高的注解,那它底层是通过 AspectJExpressionPointcut
与 @annotation() 切点表达式
相结合对目标方法进行匹配的吗?
答案是否定的。@Transactional
注解除了可以作用在方法上,还可以作用在类(或接口)上。
在底层 @Transactional
注解的匹配使用到了 StaticMethodMatcherPointcut
,在此模拟一下:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public static void main (String[] args) throws NoSuchMethodException { StaticMethodMatcherPointcut pt3 = new StaticMethodMatcherPointcut () { @Override public boolean matches (Method method, Class<?> targetClass) { MergedAnnotations annotations = MergedAnnotations.from(method); if (annotations.isPresent(Transactional.class)) { return true ; } annotations = MergedAnnotations.from(targetClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY); return annotations.isPresent(Transactional.class); } }; System.out.println(pt3.matches(T1.class.getMethod("foo" ), T1.class)); System.out.println(pt3.matches(T1.class.getMethod("bar" ), T1.class)); System.out.println(pt3.matches(T2.class.getMethod("foo" ), T2.class)); System.out.println(pt3.matches(T3.class.getMethod("foo" ), T3.class)); } static class T1 { @Transactional public void foo () { } public void bar () { } } @Transactional static class T2 { public void foo () { } } @Transactional interface I3 { void foo () ; } static class T3 implements I3 { @Override public void foo () { } }
true
false
true
true
无论是 AspectJExpressionPointcut
还是 StaticMethodMatcherPointcut
,它们都实现了 MethodMatcher
接口,用来执行方法的匹配。
17. 从@Aspect 到 Adivisor
17.1 AnnotationAwareAspectJAutoProxyCreator
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 static class Target1 { public void foo () { System.out.println("target1 foo" ); } } static class Target2 { public void bar () { System.out.println("target2 bar" ); } } @Aspect static class Aspect1 { @Before("execution(* foo())") public void before () { System.out.println("aspect1 before..." ); } @After("execution(* foo())") public void after () { System.out.println("aspect1 after..." ); } } @Configuration static class Config { @Bean public Advisor advisor3 (MethodInterceptor advice3) { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut (); pointcut.setExpression("execution(* foo())" ); return new DefaultPointcutAdvisor (pointcut, advice3); } @Bean public MethodInterceptor advices () { return invocation -> { System.out.println("advice3 before..." ); Object result = invocation.proceed(); System.out.println("advice3 after..." ); return result; }; } }
编写 main()方法创建 Spring 容器,并添加必要的 Bean:
1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("aspect1" , Aspect1.class); context.registerBean("config" , Config.class); context.registerBean(ConfigurationClassPostProcessor.class); context.refresh(); for (String name : context.getBeanDefinitionNames()) { System.out.println(name); } context.close(); }
aspect1
config
org.springframework.context.annotation.ConfigurationClassPostProcessor
advisor3
advices
Spring 中存在一个名为 AnnotationAwareAspectJAutoProxyCreator
的 Bean 后置处理器,尽管它的名称中没有 BeanPostProcessor
的字样,但它确实是实现了 BeanPostProcessor
接口的。
AnnotationAwareAspectJAutoProxyCreator
有两个主要作用:
找到容器中所有的切面,针对高级切面,将其转换为低级切面;
根据切面信息,利用 ProxyFactory 创建代理对象。
AnnotationAwareAspectJAutoProxyCreator
实现了 BeanPostProcessor
,可以在 Bean 生命周期中的一些阶段对 Bean 进行拓展。AnnotationAwareAspectJAutoProxyCreator
可以在 Bean 进行 依赖注入之前 、Bean 初始化之后 对 Bean 进行拓展。
重点介绍 AnnotationAwareAspectJAutoProxyCreator
中的两个方法:
findEligibleAdvisors()
:位于父类 AbstractAdvisorAutoProxyCreator
中,从容器中寻找相匹配的切面。低级切面直接添加,高级切面转换为低级切面再添加。
wrapIfNecessary()
:位于父类 AbstractAutoProxyCreator
中,用于将有资格被代理的 Bean 进行包装,即创建代理对象。
findEligibleAdvisors()
方法
findEligibleAdvisors()
方法接收两个参数:
beanClass
:配合切面使用的目标类 Class 信息
beanName
:当前被代理的 Bean 的名称
修改 main() 方法,向容器中添加 AnnotationAwareAspectJAutoProxyCreator
后置处理器,测试 findEligibleAdvisors()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("aspect1" , Aspect1.class); context.registerBean("config" , Config.class); context.registerBean(ConfigurationClassPostProcessor.class); context.registerBean(AnnotationAwareAspectJAutoProxyCreator.class); context.refresh(); AnnotationAwareAspectJAutoProxyCreator creator = context.getBean(AnnotationAwareAspectJAutoProxyCreator.class); List<Advisor> advisors = creator.findEligibleAdvisors(Target1.class, "target1" ); advisors.forEach(System.out::println); context.close(); }
org.springframework.aop.interceptor.ExposeInvocationInterceptor.ADVISOR
org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.framework.autoproxy.A17$Config$$Lambda$105/0x000001bf4b0fe948@626abbd0]
InstantiationModelAwarePointcutAdvisor: expression [execution(* foo())]; advice method [public void org.springframework.aop.framework.autoproxy.A17$Aspect1.before()]; perClauseKind=SINGLETON
InstantiationModelAwarePointcutAdvisor: expression [execution(* foo())]; advice method [public void org.springframework.aop.framework.autoproxy.A17$Aspect1.after()]; perClauseKind=SINGLETON
打印出 4 个能配合 Target1
使用的切面信息,其中:
第一个切面 ExposeInvocationInterceptor.ADVISOR
是 Spring 为每个代理对象都会添加的切面;
第二个切面 DefaultPointcutAdvisor
是自行编写的低级切面;
第三个和第四个切面 InstantiationModelAwarePointcutAdvisor
是由高级切面转换得到的两个低级切面。
若按照 creator.findEligibleAdvisors(Target2.class, "target2")
的方式进行调用,控制台不会打印出任何信息,因为没有任何切面能够配合 Target2 使用。
wrapIfNecessary()
方法
wrapIfNecessary()
方法内部调用了 findEligibleAdvisors()
方法,若 findEligibleAdvisors()
方法返回的集合不为空,则表示需要创建代理对象。
如果需要创建对象,wrapIfNecessary()
方法返回的是代理对象,否则仍然是原对象。
wrapIfNecessary()
方法接收三个参数:
bean
:原始 Bean 实例
beanName
:Bean 的名称
cacheKey
:用于元数据访问的缓存 key
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) { Object o1 = creator.wrapIfNecessary(new Target1 (), "target1" , "target1" ); System.out.println(o1.getClass()); Object o2 = creator.wrapIfNecessary(new Target2 (), "target2" , "target2" ); System.out.println(o2.getClass()); context.close(); }
class org.springframework.aop.framework.autoproxy.A17$Target1$$EnhancerBySpringCGLIB$$634976f6
class org.springframework.aop.framework.autoproxy.A17$Target2
Target1
对象是被代理的,而 Target2
依旧是原对象。
如果将 o1 转换为 Target1,并调用 foo() 方法,foo() 方法将被增强:
1 2 3 4 5 6 7 public static void main (String[] args) { ((Target1) o1).foo(); context.close(); }
advice3 before...
aspect1 before...
target1 foo
aspect1 after...
advice3 after...
切面的顺序控制
根据上述打印的信息可知,低级切面相比于高级切面先一步被执行,这个执行顺序是可以被控制的。
针对高级切面来说,可以在类上使用 @Order
注解,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Aspect @Order(1) static class Aspect1 { @Before("execution(* foo())") public void before () { System.out.println("aspect1 before..." ); } @After("execution(* foo())") public void after () { System.out.println("aspect1 after..." ); } }
在高级切面中,@Order
只有放在类上才生效,放在方法上不会生效。比如高级切面中有多个前置通知,这些前置通知对应的方法上使用 @Order
注解是无法生效的。
针对低级切面,需要设置 advisor
的 order
值,而不是向高级切面那样使用 @Order
注解,使用 @Order
注解设置在 advisor3()
方法上是无用的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration static class Config { @Bean public Advisor advisor3 (MethodInterceptor advice3) { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut (); pointcut.setExpression("execution(* foo())" ); DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor (pointcut, advice3); advisor.setOrder(2 ); return advisor; } }
设置完成后,高级切面的执行优先级高于低级切面。执行 main() 方法验证执行顺序是否改变:
aspect1 before...
advice3 before...
target1 foo
advice3 after...
aspect1 after...
17.2 代理对象创建时机
使用 AnnotationAwareAspectJAutoProxyCreator
Bean 后置处理器创建代理对象的时机有以下两个选择:
Bean 的依赖注入之前
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 package org.springframework.aop.framework.autoproxy;public class A17_1 { public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean(ConfigurationClassPostProcessor.class); context.registerBean(Config.class); context.refresh(); context.close(); } @Configuration static class Config { @Bean public AnnotationAwareAspectJAutoProxyCreator annotationAwareAspectJAutoProxyCreator () { return new AnnotationAwareAspectJAutoProxyCreator (); } @Bean public AutowiredAnnotationBeanPostProcessor autowiredAnnotationBeanPostProcessor () { return new AutowiredAnnotationBeanPostProcessor (); } @Bean public CommonAnnotationBeanPostProcessor commonAnnotationBeanPostProcessor () { return new CommonAnnotationBeanPostProcessor (); } @Bean public Advisor advisor (MethodInterceptor advice) { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut (); pointcut.setExpression("execution(* foo())" ); return new DefaultPointcutAdvisor (pointcut, advice); } @Bean public MethodInterceptor advice () { return invocation -> { System.out.println("before..." ); return invocation.proceed(); }; } @Bean public Bean1 bean1 () { return new Bean1 (); } @Bean public Bean2 bean2 () { return new Bean2 (); } } static class Bean1 { public void foo () {} public Bean1 () { System.out.println("Bean1()" ); } @PostConstruct public void init () { System.out.println("Bean1 init()" ); } } static class Bean2 { public Bean2 () { System.out.println("Bean2()" ); } @Autowired public void setBean1 (Bean1 bean1) { System.out.println("Bean2 setBean1(bean1) class is: " + bean1.getClass()); } @PostConstruct public void init () { System.out.println("Bean2 init()" ); } } }
bean2
中注入了 bean1
Bean1()
Bean1 init()
Creating implicit proxy for bean 'bean1' with 0 common interceptors and 2 specific interceptors
Bean2()
Bean2 setBean1(bean1) class is: class org.springframework.aop.framework.autoproxy.A17_1$Bean1$$EnhancerBySpringCGLIB$$b7d6405
Bean2 init()
在 bean1
初始化完成后,额外打印了一句日志信息:
1 Creating implicit proxy for bean 'bean1' with 0 common interceptors and 2 specific interceptors
表示为 bean1
创建了隐式代理。
此时代理对象在 Bean 初始化完成
之后创建。
之后为 bean2
进行依赖注入时,注入的 bean1
是代理对象。
在 Bean1
类中添加 setBean2()
方法,表示向 bean1
中注入 bean2
,此时 bean1
依赖 bean2
,而 bean2
原本就依赖了 bean1
,出现循环依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static class Bean1 { public void foo () {} public Bean1 () { System.out.println("Bean1()" ); } @Autowired public void setBean2 (Bean2 bean2) { System.out.println("Bean1 setBean2(bean2) class is: " + bean2.getClass()); } @PostConstruct public void init () { System.out.println("Bean1 init()" ); } }
再次运行 main()
方法,查看 bean1
的代理对象的生成时机:
Bean1()
Bean2()
Creating implicit proxy for bean 'bean1' with 0 common interceptors and 2 specific interceptors
Bean2 setBean1(bean1) class is: class org.springframework.aop.framework.autoproxy.A17_1$Bean1$$EnhancerBySpringCGLIB$$5cff48bf
Bean2 init()
Bean1 setBean2(bean2) class is: class org.springframework.aop.framework.autoproxy.A17_1$Bean2
Bean1 init()
首先进行 bean1
的实例化,然后进行 bean1
的依赖注入,但此时容器中并没有 bean2
,因此需要进行 bean2
的实例化。
接下来进行 bean2
的依赖注入,向 bean2
中注入 bean1
,注入的 bean1
应该是被增强的,即它的代理对象,因此创建 bean1
的代理对象后再完成 bean2
的依赖注入。
接着继续 bean2
的生命周期,完成 bean2
的初始化阶段,最后回到 bean1
的依赖注入阶段,向 bean1
中注入 bean2
,最后完成 bean1
的初始化阶段。
总结
代理对象的创建时机:
无循环依赖时,在 Bean 初始化阶段之后创建;
有循环依赖时,在 Bean 实例化后、依赖注入之前创建,并将代理对象暂存于二级缓存。
Bean 的依赖注入阶段和初始化阶段不应该被增强,仍应被施加于原始对象。
17.3 高级切面转低级切面
调用 AnnotationAwareAspectJAutoProxyCreator
对象的 findEligibleAdvisors()
方法时,获取能配合目标 Class 使用的切面,最终返回 Advisor
列表。在搜索过程中,如果遇到高级切面,则会将其转换成低级切面。
现有切面类与目标类信息如下:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 static class Aspect { @Before("execution(* foo())") public void before1 () { System.out.println("before1" ); } @Before("execution(* foo())") public void before2 () { System.out.println("before2" ); } public void after () { System.out.println("after" ); } public void afterReturning () { System.out.println("afterReturning" ); } public void afterThrowing () { System.out.println("afterThrowing" ); } public Object around (ProceedingJoinPoint pjp) throws Throwable { try { System.out.println("around...before" ); return pjp.proceed(); } finally { System.out.println("around...after" ); } } } static class Target { public void foo () { System.out.println("target foo" ); } }
高级切面中与通知类型相关的常用注解有 5 个:
@Before
:前置通知
@AfterReturning
:后置通知
@AfterThrowing
:异常通知
@After
:最终通知
@Around
:环绕通知
以解析 @Before
注解为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static void main (String[] args) throws Throwable { AspectInstanceFactory factory = new SingletonAspectInstanceFactory (new Aspect ()); List<Advisor> list = new ArrayList <>(); for (Method method : Aspect.class.getDeclaredMethods()) { if (method.isAnnotationPresent(Before.class)) { String expression = method.getAnnotation(Before.class).value(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut (); pointcut.setExpression(expression); AspectJMethodBeforeAdvice advice = new AspectJMethodBeforeAdvice (method, pointcut, factory); Advisor advisor = new DefaultPointcutAdvisor (pointcut, advice); list.add(advisor); } } for (Advisor advisor : list) { System.out.println(advisor); } }
org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.aspectj.AspectJMethodBeforeAdvice: advice method [public void org.springframework.aop.framework.autoproxy.A17_2$Aspect.before2()]; aspect name '']
org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.aspectj.AspectJMethodBeforeAdvice: advice method [public void org.springframework.aop.framework.autoproxy.A17_2$Aspect.before1()]; aspect name '']
@Before
标记的前置通知会被转换成原始的 AspectJMethodBeforeAdvice
形式,该对象包含了以下信息:
通知对应的方法信息
切点信息
通知对象如何创建,本例公用一个 Aspect 对象
通知相关注解与原始通知类对应关系如下:
注解
对应的原始通知类
@Before
AspectJMethodBeforeAdvice
@AfterReturning
AspectJAfterReturningAdvice
@AfterThrowing
AspectJAfterThrowingAdvice
@After
AspectJAfterAdvice
@Around
AspectJAroundAdvice
18. 静态通知调用
18.1 统一转换成环绕通知
通知相关注解都对应一个原始通知类,在 Spring 底层会将这些通知转换成环绕通知 MethodInterceptor
。如果原始通知类本就实现了 MethodInterceptor
接口,则无需转换。
原始通知类
是否需要转换成 MethodInterceptor
AspectJMethodBeforeAdvice
√
AspectJAfterReturningAdvice
√
AspectJAfterThrowingAdvice
×
AspectJAfterAdvice
×
AspectJAroundAdvice
×
使用 ProxyFactory
无论基于哪种方式创建代理对象,最终调用 advice
(通知,或者说通知对应的方法)的都是 MethodInvocation
对象。
项目中存在的 advisor(原本的低级切面和由高级切面转换得到的低级切面)往往不止一个,它们一个套一个地被调用,因此需要一个调用链对象,即 MethodInvocation
。
MethodInvocation
需要知道 advice
有哪些,还需要知道目标对象是哪个。调用次序如下:
由上图可知,环绕 通知最适合作为 advice
,而 Before
、AfterReturning
都应该转换成环绕通知。
统一转换成环绕通知的形式,体现了设计模式中的适配器模式:
对外更方便使用和区分各种通知类型
对内统一都是环绕通知,统一使用 MethodInterceptor
表示
通过 ProxyFactory
对象的 getInterceptorsAndDynamicInterceptionAdvice()
方法将其他通知统一转换为 MethodInterceptor
环绕通知:
注解
原始通知类
适配器
拦截器
@Before
AspectJMethodBeforeAdvice
MethodBeforeAdviceAdapter
MethodBeforeAdviceInterceptor
@AfterReturning
AspectJAfterReturningAdvice
AspectJAfterReturningAdvice
AfterReturningAdviceInterceptor
转换得到的通知都是静态通知,体现在 getInterceptorsAndDynamicInterceptionAdvice()
方法中的 Interceptors
部分,这些通知在被调用时无需再次检查切点,直接调用即可。
代码测试
切面类与目标类
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 static class Aspect { @Before("execution(* foo())") public void before1 () { System.out.println("before1" ); } @Before("execution(* foo())") public void before2 () { System.out.println("before2" ); } public void after () { System.out.println("after" ); } @AfterReturning("execution(* foo())") public void afterReturning () { System.out.println("afterReturning" ); } @AfterThrowing("execution(* foo())") public void afterThrowing (Exception e) { System.out.println("afterThrowing " + e.getMessage()); } @Around("execution(* foo())") public Object around (ProceedingJoinPoint pjp) throws Throwable { try { System.out.println("around...before" ); return pjp.proceed(); } finally { System.out.println("around...after" ); } } } static class Target { public void foo () { System.out.println("target foo" ); } }
将高级切面转换成低级切面,并将通知统一转换成环绕通知:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @SuppressWarnings("all") public static void main (String[] args) throws Throwable { AspectInstanceFactory factory = new SingletonAspectInstanceFactory (new Aspect ()); List<Advisor> list = new ArrayList <>(); for (Method method : Aspect.class.getDeclaredMethods()) { if (method.isAnnotationPresent(Before.class)) { String expression = method.getAnnotation(Before.class).value(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut (); pointcut.setExpression(expression); AspectJMethodBeforeAdvice advice = new AspectJMethodBeforeAdvice (method, pointcut, factory); Advisor advisor = new DefaultPointcutAdvisor (pointcut, advice); list.add(advisor); } else if (method.isAnnotationPresent(AfterReturning.class)) { String expression = method.getAnnotation(AfterReturning.class).value(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut (); pointcut.setExpression(expression); AspectJAfterReturningAdvice advice = new AspectJAfterReturningAdvice (method, pointcut, factory); Advisor advisor = new DefaultPointcutAdvisor (pointcut, advice); list.add(advisor); } else if (method.isAnnotationPresent(Around.class)) { String expression = method.getAnnotation(Around.class).value(); AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut (); pointcut.setExpression(expression); AspectJAroundAdvice advice = new AspectJAroundAdvice (method, pointcut, factory); Advisor advisor = new DefaultPointcutAdvisor (pointcut, advice); list.add(advisor); } } for (Advisor advisor : list) { System.out.println(advisor); } Target target = new Target (); ProxyFactory proxyFactory = new ProxyFactory (); proxyFactory.setTarget(target); proxyFactory.addAdvisors(list); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" ); List<Object> methodInterceptorList = proxyFactory.getInterceptorsAndDynamicInterceptionAdvice(Target.class.getMethod("foo" ), Target.class); for (Object o : methodInterceptorList) { System.out.println(o); } }
org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.aspectj.AspectJMethodBeforeAdvice: advice method [public void org.springframework.aop.framework.A18$Aspect.before2()]; aspect name '']
org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.aspectj.AspectJAroundAdvice: advice method [public java.lang.Object org.springframework.aop.framework.A18$Aspect.around(org.aspectj.lang.ProceedingJoinPoint) throws java.lang.Throwable]; aspect name '']
org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.aspectj.AspectJMethodBeforeAdvice: advice method [public void org.springframework.aop.framework.A18$Aspect.before1()]; aspect name '']
org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.aspectj.AspectJAfterReturningAdvice: advice method [public void org.springframework.aop.framework.A18$Aspect.afterReturning()]; aspect name '']
org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@7ce6a65d
org.springframework.aop.aspectj.AspectJAroundAdvice: advice method [public java.lang.Object org.springframework.aop.framework.A18$Aspect.around(org.aspectj.lang.ProceedingJoinPoint) throws java.lang.Throwable]; aspect name ''
org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@1500955a
org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor@e874448
根据打印信息可知:
前置通知 AspectJMethodBeforeAdvice
被转换成 MethodBeforeAdviceInterceptor
环绕通知 AspectJAroundAdvice
保持不变
后置通知 AspectJAfterReturningAdvice
被转换成 AfterReturningAdviceInterceptor
18.2 调用链执行
高级切面成功转换成低级切面,切面中的通知也全部转换成环绕通知 MethodInterceptor
,最后还要调用这些通知和目标方法。
这个调用交由调用链对象 MethodInvocation
来完成,在调用链对象中存放了所有经过转换得到的环绕通知和目标方法。
MethodInvocation
是一个接口,其最根本的实现是 ReflectiveMethodInvocation
。
构建 ReflectiveMethodInvocation
对象需要 6 个参数:
proxy:代理对象
target:目标对象
method:目标对象中的方法对象
arguments:调用目标对象中的方法需要的参数
targetClass:目标对象的 Class 对象
interceptorsAndDynamicMethodMatchers:转换得到的环绕通知列表
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) throws Throwable { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" ); MethodInvocation methodInvocation = new ReflectiveMethodInvocation ( null , target, Target.class.getMethod("foo" ), new Object [0 ], Target.class, methodInterceptorList ); methodInvocation.proceed(); }
运行 main() 方法后会抛出异常:
Exception in thread "main" java.lang.IllegalStateException: No MethodInvocation found:
调用链对象明明已经创建好了呀!
这是因为调用链在执行过程会调用到很多通知,而某些通知内部可能需要使用调用链对象。因此需要将调用链对象存放在某一位置,使所有通知都能获取到调用链对象。
这个“位置”就是 当前线程 。
可以在所有通知的最外层再添加一个环绕通知,将调用链对象放入当前线程。
这里我们使用 Spring 提供的 ExposeInvocationInterceptor
作为最外层的环绕通知。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void main (String[] args) throws Throwable { Target target = new Target (); ProxyFactory proxyFactory = new ProxyFactory (); proxyFactory.setTarget(target); proxyFactory.addAdvice(ExposeInvocationInterceptor.INSTANCE); proxyFactory.addAdvisors(list); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" ); MethodInvocation methodInvocation = new ReflectiveMethodInvocation ( null , target, Target.class.getMethod("foo" ), new Object [0 ], Target.class, methodInterceptorList ); methodInvocation.proceed(); }
before2
before1
around...before
target foo
around...after
afterReturning
18.3 模拟实现调用链
调用链执行过程是一个递归过程。执行 proceed()
方法将调用调用链中下一个通知或目标方法。当调用链中没有通知时,就调用目标方法,反之调用下一个通知。
这体现了设计模式中的责任链模式。
目标类 Target
1 2 3 4 5 static class Target { public void foo () { System.out.println("Target foo()" ); } }
实现 MethodInterceptor
接口,编写两个环绕通知:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static class Advice1 implements MethodInterceptor { @Override public Object invoke (MethodInvocation invocation) throws Throwable { System.out.println("Advice1.before()" ); Object result = invocation.proceed(); System.out.println("Advice1.after()" ); return result; } } static class Advice2 implements MethodInterceptor { @Override public Object invoke (MethodInvocation invocation) throws Throwable { System.out.println("Advice2.before()" ); Object result = invocation.proceed(); System.out.println("Advice2.after()" ); return result; } }
实现 MethodInvocation
接口,实现自己的调用链:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 static class MyInvocation implements MethodInvocation { private final Object target; private final Method method; private final Object[] args; private final List<MethodInterceptor> methodInterceptorList; private int count = 1 ; public MyInvocation (Object target, Method method, Object[] args, List<MethodInterceptor> methodInterceptorList) { this .target = target; this .method = method; this .args = args; this .methodInterceptorList = methodInterceptorList; } @Override public Method getMethod () { return this .method; } @Override public Object[] getArguments() { return this .args; } @Override public Object proceed () throws Throwable { if (count > methodInterceptorList.size()) { return method.invoke(target, args); } MethodInterceptor interceptor = methodInterceptorList.get(count++ - 1 ); return interceptor.invoke(this ); } @Override public Object getThis () { return this .target; } @Override public AccessibleObject getStaticPart () { return method; } }
1 2 3 4 5 6 7 8 9 public static void main (String[] args) throws Throwable { Target target = new Target (); List<MethodInterceptor> list = new ArrayList <>(Arrays.asList( new Advice1 (), new Advice2 () )); MyInvocation invocation = new MyInvocation (target, Target.class.getMethod("foo" ), new Object [0 ], list); invocation.proceed(); }
Advice1.before()
Advice2.before()
Target foo()
Advice2.after()
Advice1.after()
18.4 代理对象调用流程
以 JDK 动态代理实现为例:
从 ProxyFactory
获得 Target
和环绕通知链,根据它们创建 MethodInvocation
对象,简称 mi
首次执行 mi.proceed()
后发现有下一个环绕通知,调用它的 invoke(mi)
进入环绕通知 1,执行前增强,再次调用 mi.proceed()
后又发现有下一个环绕通知,调用它的 invoke(mi)
进入环绕通知 2,执行前增强,调用 mi.proceed()
发现没有环绕通知,调用 mi.invokeJoinPoint()
执行目标方法
目标方法执行结束,将结果返回给环绕通知 2,执行环绕通知 2 的后增强
环绕通知 2 继续将结果返回给环绕通知 1,执行环绕通知 1 的后增强
环绕通知 1 返回最终的结果
下图中不同颜色对应一次环绕通知或目标的调用起始至终结:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 sequenceDiagram participant Proxy participant ih as InvocationHandler participant mi as MethodInvocation participant Factory as ProxyFactory participant mi1 as MethodInterceptor1 participant mi2 as MethodInterceptor2 participant Target Proxy ->> +ih : invoke() ih ->> +Factory : 获得 Target Factory -->> -ih : ih ->> +Factory : 获得 MethodInterceptor 链 Factory -->> -ih : ih ->> +mi : 创建 mi mi -->> -ih : rect rgb(0, 100, 0) ih ->> +mi : mi.proceed() mi ->> +mi1 : invoke(mi) mi1 ->> mi1 : 前增强 rect rgb(125, 120, 25) mi1 ->> mi : mi.proceed() mi ->> +mi2 : invoke(mi) mi2 ->> mi2 : 前增强 rect rgb(10, 90, 155) mi2 ->> mi : mi.proceed() mi ->> +Target : mi.invokeJoinPoint() Target ->> Target : Target -->> -mi2 : 结果 end mi2 ->> mi2 : 后增强 mi2 -->> -mi1 : 结果 end mi1 ->> mi1 : 后增强 mi1 -->> -mi : 结果 mi -->> -ih : end ih -->> -Proxy :
19. 动态通知调用
前文的示例都是静态通知调用,无需参数绑定,执行时无需切点信息,性能较高。
相应地就有动态通知调用,它需要参数绑定,执行时还需要切点信息,性能较低。比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Aspect static class MyAspect { @Before("execution(* foo(..))") public void before1 () { System.out.println("before1" ); } @Before("execution(* foo(..)) && args(x)") public void before2 (int x) { System.out.printf("before(%d)\n" , x); } }
目标类 Target
:
1 2 3 4 5 static class Target { public void foo (int x) { System.out.printf("target foo(%d)\n" , x); } }
配置类 MyConfig
:
1 2 3 4 5 6 7 8 9 10 11 12 @Configuration static class MyConfig { @Bean public AnnotationAwareAspectJAutoProxyCreator proxyCreator () { return new AnnotationAwareAspectJAutoProxyCreator (); } @Bean public MyAspect myAspect () { return new MyAspect (); } }
编写 main()方法,新建 Spring 容器,查找符合条件的切面,将所有通知转换成环绕通知:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static void main (String[] args) throws Throwable { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean(ConfigurationClassPostProcessor.class); context.registerBean(MyConfig.class); context.refresh(); AnnotationAwareAspectJAutoProxyCreator creator = context.getBean(AnnotationAwareAspectJAutoProxyCreator.class); List<Advisor> list = creator.findEligibleAdvisors(Target.class, "target" ); Target target = new Target (); ProxyFactory factory = new ProxyFactory (); factory.setTarget(target); factory.addAdvisors(list); List<Object> interceptorList = factory.getInterceptorsAndDynamicInterceptionAdvice(Target.class.getMethod("foo" , int .class), Target.class); for (Object o : interceptorList) { System.out.println(o); } }
org.springframework.aop.interceptor.ExposeInvocationInterceptor@12591ac8
org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@5a7fe64f
org.springframework.aop.framework.InterceptorAndDynamicMethodMatcher@38145825
第一个 ExposeInvocationInterceptor
对象是 Spring 添加的环绕通知,第二个 MethodBeforeAdviceInterceptor
对象是前置通知转换得到的环绕通知,那 InterceptorAndDynamicMethodMatcher
对象是什么呢?
1 2 3 4 5 6 7 8 9 10 11 class InterceptorAndDynamicMethodMatcher { final MethodInterceptor interceptor; final MethodMatcher methodMatcher; public InterceptorAndDynamicMethodMatcher (MethodInterceptor interceptor, MethodMatcher methodMatcher) { this .interceptor = interceptor; this .methodMatcher = methodMatcher; } }
InterceptorAndDynamicMethodMatcher
并没有实现 MethodInterceptor
接口,它 不是一个环绕通知,对应了动态通知调用。
因此 ProxyFactory
对象的 getInterceptorsAndDynamicInterceptionAdvice()
方法返回的不仅是转换得到的环绕通知,还有对应动态通知调用的 InterceptorAndDynamicMethodMatcher
对象。
InterceptorAndDynamicMethodMatcher
对象中包含了环绕通知 interceptor
对象和切点信息 methodMatcher
(前文使用过的 AspectJExpressionPointcut
也实现了 MethodMatcher
接口)。
尝试查看 InterceptorAndDynamicMethodMatcher
对象中包含的信息,但该类并未声明成 public
,其成员变量也未被 public
修饰,也没提供获取的方式,但可以使用反射:
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 26 public static void main (String[] args) throws Throwable { for (Object o : interceptorList) { showDetail(o); } } public static void showDetail (Object o) { try { Class<?> clazz = Class.forName("org.springframework.aop.framework.InterceptorAndDynamicMethodMatcher" ); if (clazz.isInstance(o)) { Field methodMatcher = clazz.getDeclaredField("methodMatcher" ); methodMatcher.setAccessible(true ); Field methodInterceptor = clazz.getDeclaredField("interceptor" ); methodInterceptor.setAccessible(true ); System.out.println("环绕通知和切点:" + o); System.out.println("\t切点为:" + methodMatcher.get(o)); System.out.println("\t通知为:" + methodInterceptor.get(o)); } else { System.out.println("普通环绕通知:" + o); } } catch (Exception e) { throw new RuntimeException (e); } }
普通环绕通知:org.springframework.aop.interceptor.ExposeInvocationInterceptor@5a7fe64f
普通环绕通知:org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@38145825
环绕通知和切点:org.springframework.aop.framework.InterceptorAndDynamicMethodMatcher@41330d4f
切点为:AspectJExpressionPointcut: (int x) execution(* foo(..)) && args(x)
通知为:org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@24c1b2d2
根据打印的切点信息可知,InterceptorAndDynamicMethodMatcher
对象的确对应了动态通知调用。
最后创建调用链对象,执行通知和原始方法:
1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) throws Throwable { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>" ); Object proxy = factory.getProxy(); MethodInvocation methodInvocation = new ReflectiveMethodInvocation ( proxy, target, Target.class.getMethod("foo" , int .class), new Object []{100 }, Target.class, interceptorList ) { }; methodInvocation.proceed(); }
before1
before(100)
target foo(100)
动态通知调用需要切点信息,需要对参数进行匹配和绑定,复杂程度高,性能比静态通知调用低。
20. RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter
20.1 DispatcherServlet 的初始化
选择内嵌 Tomcat 的 Spring 容器
1 2 3 4 public static void main (String[] args) { AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext (WebConfig.class); }
WebConfig
作为配置类,向 Spring 容器中添加内嵌 Web 容器工厂
、DispatcherServlet
和 DispatcherServlet 注册对象
。
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 26 27 28 29 30 31 32 @Configuration @ComponentScan public class WebConfig { @Bean public TomcatServletWebServerFactory tomcatServletWebServerFactory () { return new TomcatServletWebServerFactory (); } @Bean public DispatcherServlet dispatcherServlet () { return new DispatcherServlet (); } @Bean public DispatcherServletRegistrationBean dispatcherServletRegistrationBean (DispatcherServlet dispatcherServlet) { DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean (dispatcherServlet, "/" ); return registrationBean; } }
运行 main()方法,控制台打印出:
Tomcat initialized with port(s): 8080 (http)
Root WebApplicationContext: initialization completed in 1222 ms
Tomcat 容器初始化成功,Spring 容器初始化成功,但 DispatcherServlet
还未被初始化。
当 Tomcat 服务器 首次 使用到 DispatcherServlet
时,才会由 Tomcat 服务器初始化 DispatcherServlet
。
清空控制台信息,使用浏览器访问 localhost:8080
,控制台打印出:
信息: Initializing Spring DispatcherServlet 'dispatcherServlet'
[INFO ] Initializing Servlet 'dispatcherServlet'
[TRACE] No MultipartResolver 'multipartResolver' declared
[TRACE] No LocaleResolver 'localeResolver': using default [AcceptHeaderLocaleResolver]
[TRACE] No ThemeResolver 'themeResolver': using default [FixedThemeResolver]
[TRACE] No HandlerMappings declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties
[TRACE] No HandlerAdapters declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties
[TRACE] No HandlerExceptionResolvers declared in servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties
[TRACE] No RequestToViewNameTranslator 'viewNameTranslator': using default [DefaultRequestToViewNameTranslator]
[TRACE] No ViewResolvers declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties
[TRACE] No FlashMapManager 'flashMapManager': using default [SessionFlashMapManager]
[INFO] Completed initialization in 482 ms
完成 DispatcherServlet
的初始化。
Debug 断点查看DispatcherServlet
的初始化时机
断点 DispatcherServlet
的 onRefresh()
方法中 this.initStrategies(context)
所在行:
1 2 3 protected void onRefresh (ApplicationContext context) { this .initStrategies(context); }
以 DEBUG 方式重启程序,此时程序尚未执行到断点处。
再次在浏览器中访问 localhost:8080
,程序执行到断点处。
查看调用栈可知,是从 GenericServlet
的 init()
方法执行到 onRefresh()
方法的:
1 2 3 4 public void init (ServletConfig config) throws ServletException { this .config = config; this .init(); }
因此 DispatcherServlet
的初始化流程走的是 Servlet
的初始化流程。
使 DispatcherServlet
在 Tomcat 服务器启动时被初始化
修改添加到 Spring 容器的 DispatcherServlet
注册 Bean:
1 2 3 4 5 6 @Bean public DispatcherServletRegistrationBean dispatcherServletRegistrationBean (DispatcherServlet dispatcherServlet) { DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean (dispatcherServlet, "/" ); registrationBean.setLoadOnStartup(1 ); return registrationBean; }
设置 loadOnStartup
为一个正数。
当存在多个 DispatcherServlet
需要被注册时,设置的 loadOnStartup 越大,优先级越低。
再次重启程序,根据控制台输出的内容可知,不仅完成 Tomcat 和 Spring 容器的初始化,DispatcherServlet 也初始化成功。
抽取配置信息到配置文件中
使用 @PropertySource
注解设置配置类需要读取的配置文件,以便后续读取配置文件中的内容。
要读取配置文件中的内容,可以使用 @Value
注解,但该注解一次仅仅能够读取一个值,现实是往往需要从配置文件中读取多个值。
可以使用 @EnableConfigurationProperties
注解完成配置文件信息与对象的绑定,后续使用时作为 @Bean
注解标记的方法的参数直接在方法中使用即可:
1 2 server.port =9090 spring.mvc.servlet.load-on-startup =1
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 package com.itheima.a20;import org.springframework.boot.autoconfigure.web.ServerProperties;import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletRegistrationBean;import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.PropertySource;import org.springframework.web.servlet.DispatcherServlet;@Configuration @ComponentScan @PropertySource("classpath:application.properties") @EnableConfigurationProperties({WebMvcProperties.class, ServerProperties.class}) public class WebConfig { @Bean public TomcatServletWebServerFactory tomcatServletWebServerFactory (ServerProperties serverProperties) { return new TomcatServletWebServerFactory (serverProperties.getPort()); } @Bean public DispatcherServlet dispatcherServlet () { return new DispatcherServlet (); } @Bean public DispatcherServletRegistrationBean dispatcherServletRegistrationBean ( DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties) { DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean (dispatcherServlet, "/" ); registrationBean.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup()); return registrationBean; } }
再次重启程序,根据控制台输出的内容可知,Tomcat 此时监听的端口是 9090
,DispatcherServlet
也在 Tomcat 启动时被初始化。
DispatcherServlet
初始化时执行的操作
回到 DispatcherServlet
的 onRefresh()
方法,它又调用了 initStrategies()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected void onRefresh (ApplicationContext context) { this .initStrategies(context); } protected void initStrategies (ApplicationContext context) { this .initMultipartResolver(context); this .initLocaleResolver(context); this .initThemeResolver(context); this .initHandlerMappings(context); this .initHandlerAdapters(context); this .initHandlerExceptionResolvers(context); this .initRequestToViewNameTranslator(context); this .initViewResolvers(context); this .initFlashMapManager(context); }
在这个方法中初始化了一系列组件,重点介绍:
initHandlerMappings()
:初始化处理器映射器
initHandlerAdapters()
:初始化处理器适配器
initHandlerExceptionResolvers()
:初始化异常处理器
在所有的初始化方法中都有一个相似的逻辑,首先使用一个布尔值判断是否检测 所有 目标组件。
Spring 支持父子容器嵌套,如果判断的布尔值为 true
,那么 Spring 不仅会在当前容器中获取目标组件,还会在其所有父级容器中寻找。
以initHandlerMappings()
为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private void initHandlerMappings (ApplicationContext context) { this .handlerMappings = null ; if (this .detectAllHandlerMappings) { } else { } if (this .handlerMappings == null ) { this .handlerMappings = this .getDefaultStrategies(context, HandlerMapping.class); } }
20.2 RequestMappingHandlerMapping
HandlerMapping
即处理器映射器,用于建立请求路径和控制器方法的映射关系
RequestMappingHandlerMapping
是 HandlerMapping
的一种实现,根据类名可知,它是通过 @RequestMapping
注解来实现路径映射。
当 Spring 容器中没有 HandlerMapping
的实现时,尽管DispatcherServlet
在初始化时会添加一些默认的实现,但这些实现不会交由 Spring 管理,而是作为 DispatcherServlet
的成员变量。
在配置类中将 RequestMappingHandlerMapping
添加到 Spring 容器:
1 2 3 4 5 6 @Bean public RequestMappingHandlerMapping requestMappingHandlerMapping () { return new RequestMappingHandlerMapping (); }
自定义控制器类
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 26 27 @Slf4j @Controller public class Controller1 { @GetMapping("/test1") public ModelAndView test1 () throws Exception { log.debug("test1()" ); return null ; } @PostMapping("/test2") public ModelAndView test2 (@RequestParam("name") String name) { log.debug("test2({})" , name); return null ; } @PutMapping("/test3") public ModelAndView test3 (String token) { log.debug("test3({})" , token); return null ; } @RequestMapping("/test4") public User test4 () { log.debug("test4" ); return new User ("张三" , 18 ); } }
编写 main()方法,从 Spring 容器中获取 RequestMappingHandlerMapping
,再获取请求路径与映射器方法的映射关系,并根据给定请求获取控制器方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void main (String[] args) throws Exception { AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext (WebConfig.class); RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class); Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods(); handlerMethods.forEach((info, method) -> { log.info("{} ---> {}" , info, method); }); MockHttpServletRequest request = new MockHttpServletRequest ("GET" , "/test1" ) MockHttpServletResponse response = new MockHttpServletResponse (); HandlerExecutionChain executionChain = handlerMapping.getHandler(request); System.out.println(executionChain); }
{POST [/test2]} ---> com.itheima.a20.Controller1#test2(String)
{PUT [/test3]} ---> com.itheima.a20.Controller1#test3(String)
{GET [/test1]} ---> com.itheima.a20.Controller1#test1()
{ [/test4]} ---> com.itheima.a20.Controller1#test4()
HandlerExecutionChain with [com.itheima.a20.Controller1#test1()] and 0 interceptors
getHandler()
方法返回的对象是处理器执行链,不仅包含映射器方法,还包含需要执行的拦截器信息。
MockHttpServletRequest
的使用
需要导入以下依赖
1 2 3 4 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-test</artifactId > </dependency >
20.3 RequestMappingHandlerAdapter
RequestMappingHandlerAdapter
实现了 HandlerAdapter
接口,HandlerAdapter
用于执行控制器方法,而 RequestMapping
表明 RequestMappingHandlerAdapter
用于执行被 @RequestMapping
注解标记的控制器方法。
同样需要在配置类中将 RequestMappingHandlerAdapter
添加到 Spring 容器,但该类中需要测试的方法被 protected
修饰,无法直接使用,因此创建一个子类,将子类添加到 Spring 容器中:
1 2 3 4 5 6 public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter { @Override public ModelAndView invokeHandlerMethod (HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { return super .invokeHandlerMethod(request, response, handlerMethod); } }
1 2 3 4 5 @Bean public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter () {return new MyRequestMappingHandlerAdapter ();}
在 main()方法中测试 RequestMappingHandlerAdapter
的 invokeHandlerMethod()
方法:
1 2 3 4 5 @PostMapping("/test2") public ModelAndView test2 (@RequestParam("name") String name) { log.info("test2({})" , name); return null ; }
1 2 3 4 5 6 7 8 9 public static void main (String[] args) throws Exception { MockHttpServletRequest test2Req = new MockHttpServletRequest ("POST" , "/test2" ); request.setParameter("name" , "lxd" ); MockHttpServletResponse test2Resp = new MockHttpServletResponse (); MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class); handlerAdapter.invokeHandlerMethod(test2Req, test2Resp, ((HandlerMethod) handlerMapping.getHandler(request).getHandler())); }
com.itheima.a20.Controller1 - test2(lxd)
实现控制器方法的调用很简单,但如何将请求参数与方法参数相绑定的呢?
需要解析@RequestParam
注解
Spring 支持多种类的控制器方法参数,不同种类的参数使用不同的解析器,使用 MyRequestMappingHandlerAdapter
的 getArgumentResolvers()
方法获取所有参数解析器。
Spring 也支持许多种类的控制器方法返回值类型,使用 MyRequestMappingHandlerAdapter
的 getReturnValueHandlers()
方法获取所有返回值处理器。
1 2 3 4 5 6 7 8 System.out.println("【参数解析器】" ); for (HandlerMethodArgumentResolver resolver : handlerAdapter.getArgumentResolvers()) { System.out.println(resolver); } System.out.println("【返回值解析器】" ); for (HandlerMethodReturnValueHandler resolver : handlerAdapter.getReturnValueHandlers()) { System.out.println(resolver); }
【参数解析器】
org.springframework.web.method.annotation.RequestParamMethodArgumentResolver@2ab2710
org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver@253b380a
org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver@29c2c826
org.springframework.web.servlet.mvc.method.annotation.PathVariableMapMethodArgumentResolver@3350ebdd
org.springframework.web.servlet.mvc.method.annotation.MatrixVariableMethodArgumentResolver@6818d900
org.springframework.web.servlet.mvc.method.annotation.MatrixVariableMapMethodArgumentResolver@149f5761
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@2ba33e2c
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor@1f193686
org.springframework.web.servlet.mvc.method.annotation.RequestPartMethodArgumentResolver@31e72cbc
org.springframework.web.method.annotation.RequestHeaderMethodArgumentResolver@5fad41be
org.springframework.web.method.annotation.RequestHeaderMapMethodArgumentResolver@6dcd5639
org.springframework.web.servlet.mvc.method.annotation.ServletCookieValueMethodArgumentResolver@3b36e000
org.springframework.web.method.annotation.ExpressionValueMethodArgumentResolver@333cb916
org.springframework.web.servlet.mvc.method.annotation.SessionAttributeMethodArgumentResolver@629ae7e
org.springframework.web.servlet.mvc.method.annotation.RequestAttributeMethodArgumentResolver@1d25c1c
org.springframework.web.servlet.mvc.method.annotation.ServletRequestMethodArgumentResolver@de88ac6
org.springframework.web.servlet.mvc.method.annotation.ServletResponseMethodArgumentResolver@5bca7664
org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor@105b693d
org.springframework.web.servlet.mvc.method.annotation.RedirectAttributesMethodArgumentResolver@3fae596
org.springframework.web.method.annotation.ModelMethodProcessor@4a0df195
org.springframework.web.method.annotation.MapMethodProcessor@42fcc7e6
org.springframework.web.method.annotation.ErrorsMethodArgumentResolver@9255c05
org.springframework.web.method.annotation.SessionStatusMethodArgumentResolver@5da7cee2
org.springframework.web.servlet.mvc.method.annotation.UriComponentsBuilderMethodArgumentResolver@78830d9a
com.itheima.a20.TokenArgumentResolver@5ce4369b
org.springframework.web.servlet.mvc.method.annotation.PrincipalMethodArgumentResolver@7f829c76
org.springframework.web.method.annotation.RequestParamMethodArgumentResolver@1cb19dba
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@7c3ebc6b
【返回值解析器】
org.springframework.web.servlet.mvc.method.annotation.ModelAndViewMethodReturnValueHandler@1931d99
org.springframework.web.method.annotation.ModelMethodProcessor@6a9950f1
org.springframework.web.servlet.mvc.method.annotation.ViewMethodReturnValueHandler@7ad54c55
org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler@73017a80
org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBodyReturnValueHandler@6ae7deac
org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor@4a5905d9
org.springframework.web.servlet.mvc.method.annotation.HttpHeadersReturnValueHandler@1a3e5f23
org.springframework.web.servlet.mvc.method.annotation.CallableMethodReturnValueHandler@6293e39e
org.springframework.web.servlet.mvc.method.annotation.DeferredResultMethodReturnValueHandler@365553de
org.springframework.web.servlet.mvc.method.annotation.AsyncTaskMethodReturnValueHandler@34a0ef00
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@5c0f79f0
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor@21fdfefc
org.springframework.web.servlet.mvc.method.annotation.ViewNameMethodReturnValueHandler@3daa82be
org.springframework.web.method.annotation.MapMethodProcessor@ec1b2e4
com.itheima.a20.YmlReturnValueHander@29a69a35
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@67e28be3
自定义参数解析器
假如经常需要使用到请求头中的 Token 信息,自定义 @Token 注解,使用该注解标记由控制器方法的哪个参数来获取 Token 信息:
1 2 3 4 @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface Token {}
使 test3()
控制器方法参数被 @Token
标记:
1 2 3 4 5 @PutMapping("/test3") public ModelAndView test3 (@Token String token) { log.info("test3({})" , token); return null ; }
自定义参数解析器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class TokenArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter (MethodParameter parameter) { Token token = parameter.getParameterAnnotation(Token.class); return token != null ; } @Override public Object resolveArgument (MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { return webRequest.getHeader("token" ); } }
将参数解析器添加到 HandlerAdapter
中
1 2 3 4 5 6 7 @Bean public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter () { TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver (); MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter (); handlerAdapter.setCustomArgumentResolvers(List.of(tokenArgumentResolver)); return handlerAdapter; }
测试执行 test3()
控制器方法:
1 2 3 4 5 6 7 8 9 public static void main (String[] args) throws Exception { MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class); MockHttpServletRequest test3Req = new MockHttpServletRequest ("PUT" , "/test3" ); request.addHeader("token" , "token info" ); MockHttpServletResponse test3Resp = new MockHttpServletResponse (); HandlerExecutionChain executionChain = handlerMapping.getHandler(test3Req); handlerAdapter.invokeHandlerMethod(test3Req, test3Resp, (HandlerMethod) executionChain.getHandler()); }
com.itheima.a20.Controller1 - test3(token info)
自定义返回值处理器
当 @ResponseBody
标记了控制器方法时,方法的返回值会转换成 JSON 写入响应体中。
自定义 @Yml
注解,被 @Yml
注解标记的控制器方法的返回值会转换成 YAML 写入响应体中。
1 2 3 4 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Yml {}
使 test4()
控制器方法被 @Yml
注解标记:
1 2 3 4 5 6 7 8 9 10 11 12 13 @RequestMapping("/test4") @Yml public User test4 () { log.info("test4" ); return new User ("张三" , 18 ); } @Data @AllArgsConstructor static class User { private String name; private int age; }
自定义返回值处理器将返回值转换成 YAML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class YmlReturnValueHander implements HandlerMethodReturnValueHandler { @Override public boolean supportsReturnType (MethodParameter returnType) { Yml yml = returnType.getMethodAnnotation(Yml.class); return yml != null ; } @Override public void handleReturnValue (Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { String str = new Yaml ().dump(returnValue); HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); response.setContentType("text/plain;charset=utf-8" ); response.getWriter().print(str); mavContainer.setRequestHandled(true ); } }
将返回值处理器添加到 HandlerAdapter
中:
1 2 3 4 5 6 7 8 9 10 @Bean public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter () { TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver (); YmlReturnValueHander ymlReturnValueHander = new YmlReturnValueHander (); MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter (); handlerAdapter.setCustomArgumentResolvers(List.of(tokenArgumentResolver)); handlerAdapter.setCustomReturnValueHandlers(List.of(ymlReturnValueHander)); return handlerAdapter; }
测试执行 test4()
控制器方法:
1 2 3 4 5 6 7 8 9 public static void main (String[] args) throws Exception { MockHttpServletRequest test4Req = new MockHttpServletRequest ("GET" , "/test4" ); MockHttpServletResponse test4Resp = new MockHttpServletResponse (); handlerAdapter.invokeHandlerMethod(test4Req, test4Resp, ((HandlerMethod) handlerMapping.getHandler(test4Req).getHandler())); System.out.println(test4Resp.getContentAsString()); }
com.itheima.a20.Controller1 - test4
!!com.itheima.a20.Controller1$User {age: 18, name: 张三}
21. 参数解析器
Spring 提供了许多种类的控制器方法参数解析器,定义一个包含多个不同种类参数的控制器方法:
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 static class Controller { public void test ( @RequestParam("name1") String name1, // name1=张三 String name2,// name2=李四 @RequestParam("age") int age,// age=18 (参数类型转换) @RequestParam(name = "home", defaultValue = "${JAVA_HOME}") String home1, // spring 获取数据 @RequestParam("file") MultipartFile file, // 上传文件 @PathVariable("id") int id, // /test/124 /test/{id} @RequestHeader("Content-Type") String header, @CookieValue("token") String token, @Value("${JAVA_HOME}") String home2, // spring 获取数据 ${} #{} HttpServletRequest request, // request, response, session ... @ModelAttribute("abc") User user1, // name=zhang&age=18 User user2, // name=zhang&age=18 @RequestBody User user3 // json ) { } } @Data @ToString static class User { private String name; private int age; }
将控制方法封装成HandlerMethod
并打印参数信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static void main (String[] args) throws Exception { Method method = Controller.class.getMethod("test" , String.class, String.class, int .class, String.class, MultipartFile.class, int .class, String.class, String.class, String.class, HttpServletRequest.class, User.class, User.class, User.class); HandlerMethod handlerMethod = new HandlerMethod (new Controller (), method); for (MethodParameter parameter : handlerMethod.getMethodParameters()) { String annotations = Arrays.stream(parameter.getParameterAnnotations()) .map(i -> i.annotationType().getSimpleName()).collect(Collectors.joining()); String str = annotations.length() > 0 ? "@" + annotations + " " : "" ; parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer ()); System.out.println("【" + parameter.getParameterIndex() + "】" + str + " " + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName()); } }
【0】@RequestParam String name1
【1】String name2
【2】@RequestParam int age
【3】@RequestParam String home1
【4】@RequestParam MultipartFile file
【5】@PathVariable int id
【6】@RequestHeader String header
【7】@CookieValue String token
【8】@Value String home2
【9】HttpServletRequest request
【10】@ModelAttribute User user1
【11】User user2
【12】@RequestBody User user3
mock 请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private static HttpServletRequest mockRequest () { MockHttpServletRequest request = new MockHttpServletRequest (); request.setParameter("name1" , "zhangsan" ); request.setParameter("name2" , "lisi" ); request.addPart(new MockPart ("file" , "hello" , "hello" .getBytes(StandardCharsets.UTF_8))); Map<String, String> uriTemplateVariables = new AntPathMatcher ().extractUriTemplateVariables("/test/{id}" , "/test/123" ); request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables); request.setContentType("application/json" ); request.setCookies(new Cookie ("token" , "123456" )); request.setParameter("name" , "张三" ); request.setParameter("age" , "18" ); request.setContent(""" { "name":"李四", "age":18 } """ .getBytes(StandardCharsets.UTF_8)); return new StandardServletMultipartResolver ().resolveMultipart(request); }
21.2 @RequestParam
解析@RequestParam
需要使用RequestParamMethodArgumentResolver
,构造需要两个参数:
beanFactory
:Bean 工厂对象。需要解析 ${}
时,就需要指定 Bean 工厂对象
useDefaultResolution
:布尔类型参数。
false
表示只解析添加了 @RequestParam
注解的参数,
true
表示省略 @RequestParam
注解的参数也能进行解析。
RequestParamMethodArgumentResolver
通过 resolveArgument()
解析,方法所需四个参数:
parameter
:参数对象
mavContainer
:ModelAndView
容器,用来存储中间的 Model 结果
webRequest
:由 ServletWebRequest
封装后的请求对象
binderFactory
:数据绑定工厂,用于完成对象绑定和类型转换,比如将字符串类型的 "18"
转换成整型
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public static void main (String[] args) throws Exception { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (WebConfig.class); ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); HttpServletRequest request = mockRequest(); Method method = Controller.class.getMethod("test" , String.class, String.class, int .class, String.class, MultipartFile.class, int .class, String.class, String.class, String.class, HttpServletRequest.class, User.class, User.class, User.class); HandlerMethod handlerMethod = new HandlerMethod (new Controller (), method); ServletRequestDataBinderFactory binderFactory = new ServletRequestDataBinderFactory (null , null ); ModelAndViewContainer container = new ModelAndViewContainer (); for (MethodParameter parameter : handlerMethod.getMethodParameters()) { RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver (beanFactory, true ); String annotations = Arrays.stream(parameter.getParameterAnnotations()) .map(i -> i.annotationType().getSimpleName()).collect(Collectors.joining()); String str = annotations.length() > 0 ? "@" + annotations + " " : "" ; parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer ()); if (resolver.supportsParameter(parameter)) { Object value = resolver.resolveArgument(parameter, container, new ServletWebRequest (request), binderFactory); System.out.println("【" + parameter.getParameterIndex() + "】" + str + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName() + " --> " + value); } else { System.out.println("【" + parameter.getParameterIndex() + "】" + str + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName()); } } }
【0】@RequestParam String name1 --> zhangsan
【1】String name2 --> lisi
【2】@RequestParam int age --> 18 【String ---> Integer 类型转换】
【3】@RequestParam String home1 --> C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile file --> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@28975c28
Exception in thread "main" java.lang.IllegalStateException: Optional int parameter 'id' is present but cannot be translated into a null value due to being declared as a primitive type. Consider declaring it as object wrapper for the corresponding primitive type
前 5 个参数正确解析,在解析第 6 个参数时产生了异常
这是因为在构造RequestParamMethodArgumentResolver
时,将 useDefaultResolution
设置为 true
,针对未添加@RequestParam
注解的参数都用该参数解析器进行解析,第 6 个参数id
,由当前解析器解析结果为null
,无法将null
值赋给基本类型int
,第 6 个参数应交由其他参数解析器进行解析
多个参数解析器组合使用
不同类型的参数需要使用不同参数解析器进行解析,当前解析器不支持解析当前类型参数时,就需要换到下个解析器尝试解析。
可以将所有参数解析器添加到一个集合中,然后遍历这个集合,实现上述需求。
Spring 提供了名为 HandlerMethodArgumentResolverComposite
的类,对上述逻辑进行封装。
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 26 27 28 29 30 31 32 33 public static void main (String[] args) throws Exception { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, true ) ); if (composite.supportsParameter(parameter)) { Object value = composite.resolveArgument(parameter, container, new ServletWebRequest (request), binderFactory); System.out.println("【" + parameter.getParameterIndex() + "】" + str + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName() + " --> " + value); if (!(container.getModel()).isEmpty()) { System.out.println("模型数据为: " + container.getModel()); } } else { System.out.println("【" + parameter.getParameterIndex() + "】" + str + " " + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName()); } } }
21.2 @PathVariable
@PathVariable
需要使用到 PathVariableMethodArgumentResolver
参数解析器。构造时无需传入任何参数。
使用该解析器需要一个 Map
集合,该 Map
集合是 @RequestMapping
注解上指定的路径和实际 URL 路径进行匹配后,得到的路径上的参数与实际路径上的值的关系(获取这个 Map
并将其设置给 request
作用域由 HandlerMapping
完成)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static void main (String[] args) throws Exception { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver () ); } }
修改 RequestParamMethodArgumentResolver
参数解析器的构造,将 useDefaultResolution
设置为 false
,让程序暂时不抛出异常。
【0】@RequestParam String name1 --> zhangsan
【1】String name2 --> lisi
【2】@RequestParam int age --> 18
【3】@RequestParam String home1 --> C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile file --> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int id --> 123
@RequestHeader
需要使用到 RequestHeaderMethodArgumentResolver
参数解析器。构造时需要传入一个 Bean 工厂对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static void main (String[] args) throws Exception { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (beanFactory) ); } }
【0】@RequestParam String name1 --> zhangsan
【1】String name2 --> lisi
【2】@RequestParam int age --> 18
【3】@RequestParam String home1 --> C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile file --> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int id --> 123
【6】@RequestHeader String header --> application/json
21.4 @CookieValue
@CookieValue
注解的解析需要使用到 ServletCookieValueMethodArgumentResolver
参数解析器。构造时需要传入一个 Bean 工厂对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static void main (String[] args) throws Exception { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (beanFactory), new ServletCookieValueMethodArgumentResolver (beanFactory) ); } }
【0】@RequestParam String name1 --> zhangsan
【1】String name2 --> lisi
【2】@RequestParam int age --> 18
【3】@RequestParam String home1 --> C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile file --> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int id --> 123
【6】@RequestHeader String header --> application/json
【7】@CookieValue String token --> 123456
21.5 @Value
@Value
注解的解析需要使用到 ExpressionValueMethodArgumentResolver
参数解析器。构造时需要传入一个 Bean 工厂对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static void main (String[] args) throws Exception { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (beanFactory), new ServletCookieValueMethodArgumentResolver (beanFactory), new ExpressionValueMethodArgumentResolver (beanFactory) ); } }
【0】@RequestParam String name1 --> zhangsan
【1】String name2 --> lisi
【2】@RequestParam int age --> 18
【3】@RequestParam String home1 --> C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile file --> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int id --> 123
【6】@RequestHeader String header --> application/json
【7】@CookieValue String token --> 123456
【8】@Value String home2 --> C:\Program Files\Java\jdk1.8.0_60
21.6 HttpServletRequest
HttpServletRequest
类型的参数的解析需要使用到 ServletRequestMethodArgumentResolver
参数解析器。构造时无需传入任何参数。
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 public static void main (String[] args) throws Exception { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (beanFactory), new ServletCookieValueMethodArgumentResolver (beanFactory), new ExpressionValueMethodArgumentResolver (beanFactory), new ServletRequestMethodArgumentResolver () ); } }
【0】@RequestParam String name1 --> zhangsan
【1】String name2 --> lisi
【2】@RequestParam int age --> 18
【3】@RequestParam String home1 --> C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile file --> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int id --> 123
【6】@RequestHeader String header --> application/json
【7】@CookieValue String token --> 123456
【8】@Value String home2 --> C:\Program Files\Java\jdk1.8.0_60
【9】HttpServletRequest request --> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5c45d770
ServletRequestMethodArgumentResolver
参数解析器不仅可以解析 HttpServletRequest
类型的参数,还支持许多其他类型的参数,其支持的参数类型可在 supportsParameter()
方法中看到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public boolean supportsParameter (MethodParameter parameter) { Class<?> paramType = parameter.getParameterType(); return (WebRequest.class.isAssignableFrom(paramType) || ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType) || HttpSession.class.isAssignableFrom(paramType) || (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) || (Principal.class.isAssignableFrom(paramType) && !parameter.hasParameterAnnotations()) || InputStream.class.isAssignableFrom(paramType) || Reader.class.isAssignableFrom(paramType) || HttpMethod.class == paramType || Locale.class == paramType || TimeZone.class == paramType || ZoneId.class == paramType); }
21.7 @ModelAttribute
@ModelAttribute
需要使用到 ServletModelAttributeMethodProcessor
参数解析器。构造时需要传入一个布尔类型的值。为 false
时,表示 @ModelAttribute
是必须的。
针对 @ModelAttribute("abc") User user1
和 User user2
两种参数来说,尽管后者没有使用 @ModelAttribute
注解,但它们使用的是同一种解析器。
添加两个 ServletModelAttributeMethodProcessor
参数解析器,先解析带 @ModelAttribute
注解的参数,再解析不带 @ModelAttribute
注解的参数。
通过 ServletModelAttributeMethodProcessor
解析得到的数据还会被存入 ModelAndViewContainer
中。存储的数据结构是一个 Map
,其 key 为 @ModelAttribute
注解指定的 value 值,在未显式指定的情况下,默认为对象类型的首字母小写对应的字符串。
1 2 3 4 5 6 7 8 9 static class Controller { public void test ( // 指定 value @ModelAttribute("abc") User user1, // name=zhang&age=18 User user2, // name=zhang&age=18 @RequestBody User user3 // json ) { } }
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public static void main (String[] args) throws Exception { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (beanFactory), new ServletCookieValueMethodArgumentResolver (beanFactory), new ExpressionValueMethodArgumentResolver (beanFactory), new ServletRequestMethodArgumentResolver (), new ServletModelAttributeMethodProcessor (false ), new ServletModelAttributeMethodProcessor (true ) ); if (composite.supportsParameter(parameter)) { Object v = composite.resolveArgument(parameter, container, new ServletWebRequest (request), binderFactory); System.out.println(paramInfo + " -> " + v); ModelMap modelMap = container.getModel(); if (MapUtils.isNotEmpty(modelMap)) { System.out.println("模型数据: " + modelMap); } } else { System.out.println(paramInfo); } } }
【0】@RequestParam String name1 --> zhangsan
【1】String name2 --> lisi
【2】@RequestParam int age --> 18
【3】@RequestParam String home1 --> C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile file --> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int id --> 123
【6】@RequestHeader String header --> application/json
【7】@CookieValue String token --> 123456
【8】@Value String home2 --> C:\Program Files\Java\jdk1.8.0_60
【9】HttpServletRequest request --> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5c45d770
【10】@ModelAttribute User user1 --> A21.User(name=张三, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
【11】User user2 --> A21.User(name=张三, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
22:58:09.903 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - Read "application/json" to [A21.User(name=李四, age=18)]
【12】@RequestBody User user3 --> A21.User(name=李四, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
@RequestBody User user3
参数也被 ServletModelAttributeMethodProcessor
解析了,如果想使其数据通过 JSON 数据转换而来,则需要使用另一个参数解析器。
21.8 @RequestBody
@RequestBody
注解的解析需要使用到 RequestResponseBodyMethodProcessor
参数解析器。构造时需要传入一个消息转换器列表。
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 26 27 28 29 public static void main (String[] args) throws Exception { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (beanFactory), new ServletCookieValueMethodArgumentResolver (beanFactory), new ExpressionValueMethodArgumentResolver (beanFactory), new ServletRequestMethodArgumentResolver (), new ServletModelAttributeMethodProcessor (false ), new RequestResponseBodyMethodProcessor (Collections.singletonList(new MappingJackson2HttpMessageConverter ())), new ServletModelAttributeMethodProcessor (true ) ); } }
【0】@RequestParam String name1 --> zhangsan
【1】String name2 --> lisi
【2】@RequestParam int age --> 18
【3】@RequestParam String home1 --> C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile file --> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int id --> 123
【6】@RequestHeader String header --> application/json
【7】@CookieValue String token --> 123456
【8】@Value String home2 --> C:\Program Files\Java\jdk1.8.0_60
【9】HttpServletRequest request --> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5c45d770
【10】@ModelAttribute User user1 --> A21.User(name=张三, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
【11】User user2 --> A21.User(name=张三, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
23:10:33.282 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - Read "application/json" to [A21.User(name=李四, age=18)]
【12】@RequestBody User user3 --> A21.User(name=李四, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
@RequestBody User user3
参数数据通过 JSON 数据得到,与上一节的解析进行区分。
除此之外,添加的参数解析器的顺序也影响着解析结果
1 2 3 new ServletModelAttributeMethodProcessor (false ),new RequestResponseBodyMethodProcessor (Collections.singletonList(new MappingJackson2HttpMessageConverter ())),new ServletModelAttributeMethodProcessor (true )
先添加解析 @ModelAttribute
的解析器,再添加解析 @RequestBody
的解析器,最后添加解析省略了 @ModelAttribute
的解析器。如果更换最后两个解析器的顺序,那么 @RequestBody User user3
将会被 ServletModelAttributeMethodProcessor
解析,而不是 RequestResponseBodyMethodProcessor
。
因此 String name2
参数也能通过添加同种参数但不同构造参数的解析器进行解析,注意添加的解析器的顺序,先处理对象,再处理单个参数:
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 26 27 28 29 30 public static void main (String[] args) throws Exception { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (beanFactory), new ServletCookieValueMethodArgumentResolver (beanFactory), new ExpressionValueMethodArgumentResolver (beanFactory), new ServletRequestMethodArgumentResolver (), new ServletModelAttributeMethodProcessor (false ), new RequestResponseBodyMethodProcessor (Collections.singletonList(new MappingJackson2HttpMessageConverter ())), new ServletModelAttributeMethodProcessor (true ), new RequestParamMethodArgumentResolver (beanFactory, true ) ); } }
【0】@RequestParam String name1 --> zhangsan
【1】String name2 --> lisi
【2】@RequestParam int age --> 18
【3】@RequestParam String home1 --> C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile file --> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int id --> 123
【6】@RequestHeader String header --> application/json
【7】@CookieValue String token --> 123456
【8】@Value String home2 --> C:\Program Files\Java\jdk1.8.0_60
【9】HttpServletRequest request --> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5c45d770
【10】@ModelAttribute User user1 --> A21.User(name=张三, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
【11】User user2 --> A21.User(name=张三, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
23:10:33.282 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - Read "application/json" to [A21.User(name=李四, age=18)]
【12】@RequestBody User user3 --> A21.User(name=李四, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
22. 获取参数名
在项目的src
目录外创建Bean2.java
文件,使其不会被 IDEA 自动编译
1 2 3 4 5 public class Bean2 { public void foo (String name, int age) { } }
将命令行切换到 Bean2.java
文件所在目录的位置,执行 javac .\Bean2.java
命令手动编译 Bean2.java
。查看 Bean2.class
文件的内容:
1 2 3 4 5 6 7 public class Bean2 { public Bean2 () { } public void foo (String var1, int var2) { } }
编译生成的 class
文件中的 foo()
方法的参数名称不再是 name
和 age
,也就是说直接使用 javac 命令进行编译得到的字节码文件不会保存方法的参数名称。
执行javac -parameters .\Bean2.java
再次编译 Bean2.java
,并查看得到的 Bean2.class
文件内容:
1 2 3 4 5 6 7 public class Bean2 { public Bean2 () { } public void foo (String name, int age) { } }
foo()
方法的参数名称得以保留。
使用 javap -c -v .\Bean2.class
命令反编译Bean2.class
,foo()
方法的反编译结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 public void foo (java.lang.String, int ) ; descriptor: (Ljava/lang/String;I)V flags: ACC_PUBLIC Code: stack=0 , locals=3 , args_size=3 0 : return LineNumberTable: line 4 : 0 MethodParameters: Name Flags name age }
foo()
方法的参数信息被保存在 MethodParameters
中,可以使用 反射 获取:
1 2 3 4 5 6 7 public static void main (String[] args) throws Exception { Method foo = Bean2.class.getMethod("foo" , String.class, int .class); for (Parameter parameter : foo.getParameters()) { System.out.println(parameter.getName()); } }
name
age
使用javac -g .\Bean2.java
命令进行编译也会保留方法的参数信息。再次使用javap
反编译 Bean2.class
,foo()
方法的反编译结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void foo (java.lang.String, int ) ; descriptor: (Ljava/lang/String;I)V flags: ACC_PUBLIC Code: stack=0 , locals=3 , args_size=3 0 : return LineNumberTable: line 4 : 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 this LBean2; 0 1 1 name Ljava/lang/String; 0 1 2 age I }
foo()
方法的参数信息被保存在LocalVariableTable
中,不能使用反射获取,但可以使用 ASM 获取,使用 Spring 封装的解析工具:
1 2 3 4 5 6 7 8 9 public static void main (String[] args) throws Exception { Method foo = Bean2.class.getMethod("foo" , String.class, int .class); LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer (); String[] parameterNames = discoverer.getParameterNames(foo); System.out.println(Arrays.toString(parameterNames)); }
[name, age]
在【21. 参数解析器】中并没有使用 LocalVariableTableParameterNameDiscoverer
,而是使用的是 DefaultParameterNameDiscoverer
。DefaultParameterNameDiscoverer
将两种实现进行了统一:
1 2 3 4 5 6 7 8 9 10 11 public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer { public DefaultParameterNameDiscoverer () { if (KotlinDetector.isKotlinReflectPresent() && !NativeDetector.inNativeImage()) { addDiscoverer(new KotlinReflectionParameterNameDiscoverer ()); } addDiscoverer(new StandardReflectionParameterNameDiscoverer ()); addDiscoverer(new LocalVariableTableParameterNameDiscoverer ()); } }
javac -g
的局限性
假设有这样一个接口:
1 2 3 public interface Bean1 { public void foo (String name, int age) ; }
如果使用 javac -g .\Bean1.java
命令进行编译后,再利用javap
查看 foo()
方法的反编译结果:
1 2 3 public abstract void foo (java.lang.String, int ) ; descriptor: (Ljava/lang/String;I)V flags: ACC_PUBLIC, ACC_ABSTRACT
并没有记录抽象方法 foo()
的参数信息。
使用 javac -parameters .\Bean1.java
1 2 3 4 5 6 7 public abstract void foo (java.lang.String, int ) ; descriptor: (Ljava/lang/String;I)V flags: ACC_PUBLIC, ACC_ABSTRACT MethodParameters: Name Flags name age
参数信息得以保留。
23. 对象绑定与类型转换
23.1 三种转换接口
底层第一套转换接口与实现
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 26 27 28 classDiagram Formatter --|> Printer Formatter --|> Parser class Converters { Set~GenericConverter~ } class Converter class ConversionService class FormattingConversionService ConversionService <|-- FormattingConversionService FormattingConversionService o-- Converters Printer --> Adapter1 Adapter1 --> Converters Parser --> Adapter2 Adapter2 --> Converters Converter --> Adapter3 Adapter3 --> Converters <<interface > > Formatter <<interface > > Printer <<interface > > Parser <<interface > > Converter <<interface > > ConversionService
Printer
把其它类型转为 String
Parser
把 String
转为其它类型
Formatter
综合 Printer
与 Parser
功能
Converter
把任意类型S
转为任意类型T
Printer
、Parser
、Converter
经过适配转换成 GenericConverter
放入Converters
集合
FormattingConversionService
利用其他接口实现转换
底层第二套转换接口
由 JDK 提供,而不是 Spring。
1 2 3 4 5 6 classDiagram PropertyEditorRegistry o-- "多" PropertyEditor <<interface > > PropertyEditorRegistry <<interface > > PropertyEditor
PropertyEditor
把 String
与其它类型相互转换
PropertyEditorRegistry
可以注册多个 PropertyEditor
对象
与第一套接口直接可以通过 FormatterPropertyEditorAdapter
来进行适配
高层接口与实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 classDiagram TypeConverter <|-- SimpleTypeConverter TypeConverter <|-- BeanWrapperImpl TypeConverter <|-- DirectFieldAccessor TypeConverter <|-- ServletRequestDataBinder SimpleTypeConverter --> TypeConverterDelegate BeanWrapperImpl --> TypeConverterDelegate DirectFieldAccessor --> TypeConverterDelegate ServletRequestDataBinder --> TypeConverterDelegate TypeConverterDelegate --> ConversionService TypeConverterDelegate --> PropertyEditorRegistry <<interface > > TypeConverter <<interface > > ConversionService <<interface > > PropertyEditorRegistry
它们都实现了 TypeConverter
这个高层转换接口,在转换时,会用到TypeConverterDelegate
委派ConversionService
与 PropertyEditorRegistry
真正执行转换(Facade 门面模式)
首先查看是否存在实现了 PropertyEditorRegistry
的自定义转换器,@InitBinder
注解实现的就是自定义转换器 (用了适配器模式把 Formatter
转为需要的 PropertyEditor
)
再看有没有 ConversionService
转换
再利用默认的 PropertyEditor
转换
最后有一些特殊处理
SimpleTypeConverter
仅做类型转换
BeanWrapperImpl
利用 Property
,即 Getter/Setter
,为 Bean 的属性赋值,必要时进行类型转换
DirectFieldAccessor
利用Field
,即字段(成员变量),为 Bean 的字段赋值,必要时进行类型转换
ServletRequestDataBinder
为 bean 的属性执行绑定,当需要时做类型转换,根据 directFieldAccess
选择走 Property
还是 Field
,具备校验与获取校验结果功能
23.2 使用示例
SimpleTypeConverter
1 2 3 4 5 6 7 8 9 public class TestSimpleConverter { public static void main (String[] args) { SimpleTypeConverter converter = new SimpleTypeConverter (); Integer number = converter.convertIfNecessary("13" , int .class); System.out.println(number); Date date = converter.convertIfNecessary("1999/03/04" , Date.class); System.out.println(date); } }
13
Thu Mar 04 00:00:00 CST 1999
BeanWrapperImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class TestBeanWrapper { public static void main (String[] args) { MyBean bean = new MyBean (); BeanWrapperImpl wrapper = new BeanWrapperImpl (bean); wrapper.setPropertyValue("a" , "10" ); wrapper.setPropertyValue("b" , "hello" ); wrapper.setPropertyValue("c" , "1999/03/04" ); System.out.println(bean); } @Getter @Setter @ToString static class MyBean { private int a; private String b; private Date c; } }
TestBeanWrapper.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)
DirectFieldAccessor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class TestFieldAccessor { public static void main (String[] args) { MyBean bean = new MyBean (); DirectFieldAccessor accessor = new DirectFieldAccessor (bean); accessor.setPropertyValue("a" , "10" ); accessor.setPropertyValue("b" , "hello" ); accessor.setPropertyValue("c" , "1999/03/04" ); System.out.println(bean); } @ToString static class MyBean { private int a; private String b; private Date c; } }
TestFieldAccessor.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)
DataBinder
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class TestDataBinder { public static void main (String[] args) { MyBean bean = new MyBean (); DataBinder binder = new DataBinder (bean); MutablePropertyValues pvs = new MutablePropertyValues (); pvs.add("a" , "10" ); pvs.add("b" , "hello" ); pvs.add("c" , "1999/03/04" ); binder.bind(pvs); System.out.println(bean); } @Getter @Setter @ToString static class MyBean { private int a; private String b; private Date c; } }
TestDataBinder.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)
如果 MyBean
没有提供 Getter/Setter
方法,可以调用 DataBinder
的 initDirectFieldAccess()
方法使数据绑定逻辑走字段赋值,而不是属性赋值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class TestDataBinder { public static void main (String[] args) { MyBean bean = new MyBean (); DataBinder binder = new DataBinder (bean); binder.initDirectFieldAccess(); MutablePropertyValues pvs = new MutablePropertyValues (); pvs.add("a" , "10" ); pvs.add("b" , "hello" ); pvs.add("c" , "1999/03/04" ); binder.bind(pvs); System.out.println(bean); } @ToString static class MyBean { private int a; private String b; private Date c; } }
TestDataBinder.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)
Web 环境下的数据绑定
Web 环境下的数据绑定需要使用 DataBinder
的子类 ServletRequestDataBinder
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static void main (String[] args) { MyBean bean = new MyBean (); DataBinder dataBinder = new ServletRequestDataBinder (bean); MockHttpServletRequest request = new MockHttpServletRequest (); request.setParameter("a" , "10" ); request.setParameter("b" , "hello" ); request.setParameter("c" , "1999/03/04" ); dataBinder.bind(new ServletRequestParameterPropertyValues (request)); System.out.println(bean); } @Getter @Setter @ToString static class MyBean { private int a; private String b; private Date c; }
TestServletDataBinder.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)
23.3 绑定器工厂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Getter @Setter @ToString public static class User { private Date birthday; private Address address; } @Getter @Setter @ToString public static class Address { private String name; }
在 Web 环境下进行数据绑定:
1 2 3 4 5 6 7 8 9 10 11 public static void main (String[] args) { MockHttpServletRequest request = new MockHttpServletRequest (); request.setParameter("birthday" , "1999|01|02" ); request.setParameter("address.name" , "北京" ); User user = new User (); ServletRequestDataBinder dataBinder = new ServletRequestDataBinder (user); dataBinder.bind(new ServletRequestParameterPropertyValues (request)); System.out.println(user); }
TestServletDataBinderFactory.User(birthday=null, address=TestServletDataBinderFactory.Address(name=北京))
birthday
绑定失败,要想使其绑定成功,需要自定义转换器,有两种方式:
使用 Spring 提供的 ConversionService
使用 JDK 提供的 PropertyEditorRegistry
创建 DataBinder
的职责交由DataBinderFactory
完成,以便添加各种选项,拓展不同的自定义转换器。
1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest (); request.setParameter("birthday" , "1999|01|02" ); request.setParameter("address.name" , "北京" ); User user = new User (); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory (null , null ); WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest (request), user, "user" ); dataBinder.bind(new ServletRequestParameterPropertyValues (request)); System.out.println(user); }
运行 main()
方法后,控制台输出的结果不变。
利用 @InitBinder
自定义转换器
声明一个 Controller
类,其中包含一个被 @InitBinder
注解标记的方法:
1 2 3 4 5 6 7 static class MyController { @InitBinder public void myMethod (WebDataBinder dataBinder) { dataBinder.addCustomFormatter(new MyDateFormatter ("用 @InitBinder 进行拓展" )); } }
以WebDataBinder
作为方法参数,在方法体中添加自定义转换器MyDateFormatter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Slf4j public class MyDateFormatter implements Formatter <Date> { private final String desc; public MyDateFormatter (String desc) { this .desc = desc; } @Override public String print (Date date, Locale locale) { SimpleDateFormat sdf = new SimpleDateFormat ("yyyy|MM|dd" ); return sdf.format(date); } @Override public Date parse (String text, Locale locale) throws ParseException { log.debug(">>>>>> 进入了: {}" , desc); SimpleDateFormat sdf = new SimpleDateFormat ("yyyy|MM|dd" ); return sdf.parse(text); } }
在构造DataBinderFactory
时传入InvocableHandlerMethod
列表,列表中包含根据 Controller 对象、Controller 类中被 @InitBinder
注解标记的方法对象构造的InvocableHandlerMethod
对象,DataBinderFactory
就知道有哪些InitBinder
方法,在createBinder
时,回调每一个InitBinder
,传递dataBinder
,自由进行拓展
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class TestServletDataBinderFactory { public static void main (String[] args) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest (); request.setParameter("birthday" , "1999|01|02" ); request.setParameter("address.name" , "北京" ); User user = new User (); InvocableHandlerMethod method = new InvocableHandlerMethod (new MyController (), MyController.class.getMethod("myMethod" , WebDataBinder.class)); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory (List.of(method), null ); WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest (request), user, "user" ); dataBinder.bind(new ServletRequestParameterPropertyValues (request)); System.out.println(user); } @Getter @Setter @ToString public static class User { private Date birthday; private Address address; } @Getter @Setter @ToString public static class Address { private String name; } static class MyController { @InitBinder public void myMethod (WebDataBinder dataBinder) { dataBinder.addCustomFormatter(new MyDateFormatter ("用 @InitBinder 进行拓展" )); } } }
再次执行main(),birthday
被成功绑定:
com.itheima.a23.MyDateFormatter - >>>>>> 进入了: 用 @InitBinder 进行拓展
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=北京))
这种方式使用了 JDK 提供的PropertyEditorRegistry
,证据就在 WebDataBinder
的 addCustomFormatter()
方法中:
1 2 3 4 public void addCustomFormatter (Formatter<?> formatter) { FormatterPropertyEditorAdapter adapter = new FormatterPropertyEditorAdapter (formatter); getPropertyEditorRegistry().registerCustomEditor(adapter.getFieldType(), adapter); }
ConversionService
拓展
选择 FormattingConversionService
作为 ConversionService
的实现,向其中添加自定义转换器MyDateFormatter
。
构造 DataBinderFactory
时传入 WebBindingInitializer
的实现,因此将FormattingConversionService
封装成 ConfigurableWebBindingInitializer
传入 DataBinderFactory
的构造方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void main (String[] args) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest (); request.setParameter("birthday" , "1999|01|02" ); request.setParameter("address.name" , "北京" ); User user = new User (); FormattingConversionService service = new FormattingConversionService (); service.addFormatter(new MyDateFormatter ("用 ConversionService 方式拓展转换功能" )); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer (); initializer.setConversionService(service); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory (null , initializer); WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest (request), user, "user" ); dataBinder.bind(new ServletRequestParameterPropertyValues (request)); System.out.println(user); }
com.itheima.a23.MyDateFormatter - >>>>>> 进入了: 用 ConversionService 方式拓展转换功能
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=北京))
如果同时存在 @InitBinder
和 ConversionService
,将以 @InitBinder
为主,@InitBinder
实现的转换器属于自定义转换器,自定义转换器的优先级更高:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static void main (String[] args) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest (); request.setParameter("birthday" , "1999|01|02" ); request.setParameter("address.name" , "北京" ); User user = new User (); InvocableHandlerMethod method = new InvocableHandlerMethod (new MyController (), MyController.class.getMethod("myMethod" , WebDataBinder.class)); FormattingConversionService service = new FormattingConversionService (); service.addFormatter(new MyDateFormatter ("用 ConversionService 方式拓展转换功能" )); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer (); initializer.setConversionService(service); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory (List.of(method), initializer); WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest (request), user, "user" ); dataBinder.bind(new ServletRequestParameterPropertyValues (request)); System.out.println(user); }
com.itheima.a23.MyDateFormatter - >>>>>> 进入了: 用 @InitBinder 进行拓展
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=北京))
默认的 ConversionService
ConversionService
有一个默认实现 DefaultFormattingConversionService
,它还是 FormattingConversionService
的子类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static void main (String[] args) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest (); request.setParameter("birthday" , "1999|01|02" ); request.setParameter("address.name" , "北京" ); User user = new User (); DefaultFormattingConversionService service = new DefaultFormattingConversionService (); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer (); initializer.setConversionService(service); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory (null , initializer); WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest (request), user, "user" ); dataBinder.bind(new ServletRequestParameterPropertyValues (request)); System.out.println(user); }
TestServletDataBinderFactory.User(birthday=null, address=TestServletDataBinderFactory.Address(name=北京))
birthday
绑定失败,默认的 ConversionService
需要搭配注解 @DateTimeFormat
使用。在目标类的字段上使用该注解标记,并指定被转换的日期格式:
1 2 3 4 5 6 7 8 @Getter @Setter @ToString public static class User { @DateTimeFormat(pattern = "yyyy|MM|dd") private Date birthday; private Address address; }
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=北京))
在 SpringBoot
中还提供了 ApplicationConversionService
,它也是 FormattingConversionService
的子类,上述代码将 DefaultFormattingConversionService
换成 ApplicationConversionService
也能达到相同效果。
23.4 Spring 的泛型操作技巧
有一基类 BaseDao
,接收一个泛型参数:
1 2 3 4 5 public class BaseDao <T> { T findOne () { return null ; } }
围绕 BaseDao
有如下五个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class EmployeeDao extends BaseDao {} public class Student {} public class StudentDao extends BaseDao <Student> {} public class Teacher {} public class TeacherDao extends BaseDao <Teacher> {}
尝试获取 BaseDao
子类泛型参数:
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 26 27 public static void main (String[] args) { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>" ); Type teacherDaoType = TeacherDao.class.getGenericSuperclass(); System.out.println("TeacherDao type: " + teacherDaoType); System.out.println("TeacherDao type class: " + teacherDaoType.getClass()); Type employeeDaoType = EmployeeDao.class.getGenericSuperclass(); System.out.println("EmployeeDao type: " + employeeDaoType); System.out.println("EmployeeDao type class: " + employeeDaoType.getClass()); if (teacherDaoType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) teacherDaoType; System.out.println(parameterizedType.getActualTypeArguments()[0 ]); } System.out.println(">>>>>>>>>>>>>>>>>>>>>>>" ); Class<?> t = GenericTypeResolver.resolveTypeArgument(TeacherDao.class, BaseDao.class); System.out.println(t); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>" ); System.out.println(ResolvableType.forClass(StudentDao.class).getSuperType().getGeneric().resolve()); }
TeacherDao type: com.itheima.a23.BaseDao
TeacherDao type class: class sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl
EmployeeDao type: class com.itheima.a23.BaseDao
EmployeeDao type class: class java.lang.Class
class com.itheima.a23.Teacher
>>>>>>>>>>>>>>>>>>>>>>>
class com.itheima.a23.Teacher
>>>>>>>>>>>>>>>>>>>>>>>
class com.itheima.a23.Student
24. ControllerAdivice 之 @InitBinder
准备 @InitBinder
在整个 HandlerAdapter
调用过程中所处的位置:
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 26 27 28 29 30 31 sequenceDiagram participant adapter as HandlerAdapter participant bf as WebDataBinderFactory participant mf as ModelFactory participant ihm as ServletInvocableHandlerMethod participant ar as ArgumentResolvers participant rh as ReturnValueHandlers participant container as ModelAndViewContainer rect rgb(200, 150, 255) adapter ->> +bf: 准备 @InitBinder bf -->> -adapter: end adapter ->> +mf: 准备 @ModelAttribute mf ->> +container: 添加 Model 数据 container -->> -mf: mf -->> -adapter: adapter ->> +ihm: invokeAndHandle ihm ->> +ar: 获取 args ar ->> ar: 有的解析器涉及 RequestBodyAdvice ar ->> container: 有的解析器涉及数据绑定生成 Model 数据 ar -->> -ihm: args ihm ->> ihm: method.invoke(bean,args) 得到 returnValue ihm ->> +rh: 处理 returnValue rh ->> rh: 有的处理器涉及 ResponseBodyAdvice rh ->> +container: 添加 Model 数据,处理视图名,是否渲染等 container -->> -rh: rh -->> -ihm: ihm -->> -adapter: adapter ->> +container: 获取 ModelAndView container -->> -adapter:
RequestMappingHandlerAdapter
在图中缩写为 HandlerAdapter
HandlerMethodArgumentResolverComposite
在图中缩写为 ArgumentResolvers
HandlerMethodReturnValueHandlerComposite
在图中缩写为 ReturnValueHandlers
功能与使用
@InitBinder
注解只能作用在方法上,通常搭配@ControllerAdvice
和 @Controller
以及他们的衍生注解使用。比如:
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 26 27 28 29 30 31 32 33 34 35 36 37 @Configuration public class WebConfig { @ControllerAdvice static class MyControllerAdvice { @InitBinder public void binder3 (WebDataBinder webDataBinder) { webDataBinder.addCustomFormatter(new MyDateFormatter ("binder3 转换器" )); } } @Controller static class Controller1 { @InitBinder public void binder1 (WebDataBinder webDataBinder) { webDataBinder.addCustomFormatter(new MyDateFormatter ("binder1 转换器" )); } public void foo () { } } @Controller static class Controller2 { @InitBinder public void binder21 (WebDataBinder webDataBinder) { webDataBinder.addCustomFormatter(new MyDateFormatter ("binder21 转换器" )); } @InitBinder public void binder22 (WebDataBinder webDataBinder) { webDataBinder.addCustomFormatter(new MyDateFormatter ("binder22 转换器" )); } public void bar () { } } }
当 @InitBinder
加在被 @ControllerAdvice
标记的类中,是对 所有 控制器都生效的自定义类型转换器。当加在被@Controller
标记的类中,是只对当前 控制器生效的自定义类型转换器。
@InitBinder
的来源有两个:
@ControllerAdvice
标记的类中@InitBinder
标记的方法,由 RequestMappingHandlerAdapter
在初始化时解析并记录
@Controller
标记的类中 @InitBinder
标记的方法,由 RequestMappingHandlerAdapter
在控制器方法首次执行时解析并记录
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 26 27 28 29 30 31 32 33 public static void main (String[] args) throws Exception { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (WebConfig.class); RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter (); handlerAdapter.setApplicationContext(context); handlerAdapter.afterPropertiesSet(); log.debug("1. 刚开始..." ); showBindMethods(handlerAdapter); context.close(); } @SuppressWarnings("all") private static void showBindMethods (RequestMappingHandlerAdapter handlerAdapter) throws NoSuchFieldException, IllegalAccessException { Field initBinderAdviceCache = RequestMappingHandlerAdapter.class.getDeclaredField("initBinderAdviceCache" ); initBinderAdviceCache.setAccessible(true ); Map<ControllerAdviceBean, Set<Method>> globalMap = (Map<ControllerAdviceBean, Set<Method>>) initBinderAdviceCache.get(handlerAdapter); log.debug("全局的 @InitBinder 方法 {}" , globalMap.values().stream() .flatMap(ms -> ms.stream().map(m -> m.getName())) .collect(Collectors.toList()) ); Field initBinderCache = RequestMappingHandlerAdapter.class.getDeclaredField("initBinderCache" ); initBinderCache.setAccessible(true ); Map<Class<?>, Set<Method>> controllerMap = (Map<Class<?>, Set<Method>>) initBinderCache.get(handlerAdapter); log.debug("控制器的 @InitBinder 方法 {}" , controllerMap.entrySet().stream() .flatMap(e -> e.getValue().stream().map(v -> e.getKey().getSimpleName() + "." + v.getName())) .collect(Collectors.toList()) ); }
com.itheima.a24.A24 - 1. 刚开始...
com.itheima.a24.A24 - 全局的 @InitBinder 方法 [binder3]
com.itheima.a24.A24 - 控制器的 @InitBinder 方法 []
全局的@InitBinder
方法被解析并记录,但控制器中被 @InitBinder
标记的方法并没有被解析记录。
模拟调用控制器方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static void main (String[] args) throws Exception { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (WebConfig.class); RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter (); handlerAdapter.setApplicationContext(context); handlerAdapter.afterPropertiesSet(); log.info("1. 刚开始..." ); showBindMethods(handlerAdapter); Method getDataBinderFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getDataBinderFactory" , HandlerMethod.class); getDataBinderFactory.setAccessible(true ); log.info("2. 模拟调用 Controller1 的 foo 方法..." ); getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod (new WebConfig .Controller1(), WebConfig.Controller1.class.getMethod("foo" ))); showBindMethods(handlerAdapter); log.info("3. 模拟调用 Controller2 的 bar 方法时..." ); getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod (new WebConfig .Controller2(), WebConfig.Controller2.class.getMethod("bar" ))); showBindMethods(handlerAdapter); context.close(); }
com.itheima.a24.A24 - 1. 刚开始...
com.itheima.a24.A24 - 全局的 @InitBinder 方法 [binder3]
com.itheima.a24.A24 - 控制器的 @InitBinder 方法 []
com.itheima.a24.A24 - 2. 模拟调用 Controller1 的 foo 方法...
com.itheima.a24.A24 - 全局的 @InitBinder 方法 [binder3]
com.itheima.a24.A24 - 控制器的 @InitBinder 方法 [Controller1.binder1]
com.itheima.a24.A24 - 3. 模拟调用 Controller2 的 bar 方法时...
com.itheima.a24.A24 - 全局的 @InitBinder 方法 [binder3]
com.itheima.a24.A24 - 控制器的 @InitBinder 方法 [Controller2.binder22, Controller2.binder21, Controller1.binder1]
首次调用控制器中的方法时,控制器中被 @InitBinder
标记方法被解析记录。
25. 控制器方法执行流程
ServletInvocableHandlerMethod
的组成
1 2 3 4 5 6 7 8 9 10 11 classDiagram class ServletInvocableHandlerMethod { +invokeAndHandle(ServletWebRequest,ModelAndViewContainer) } HandlerMethod <|-- ServletInvocableHandlerMethod HandlerMethod o-- bean HandlerMethod o-- method ServletInvocableHandlerMethod o-- WebDataBinderFactory ServletInvocableHandlerMethod o-- ParameterNameDiscoverer ServletInvocableHandlerMethod o-- HandlerMethodArgumentResolverComposite ServletInvocableHandlerMethod o-- HandlerMethodReturnValueHandlerComposite
HandlerMethod
需要:
bean
即是哪个 Controller
method
即是 Controller 中的哪个方法
ServletInvocableHandlerMethod
需要:
WebDataBinderFactory
负责对象绑定、类型转换
ParameterNameDiscoverer
负责参数名解析
HandlerMethodArgumentResolverComposite
负责解析参数
HandlerMethodReturnValueHandlerComposite
负责处理返回值
控制器方法执行流程
以 RequestMappingHandlerAdapter
为起点,创建 WebDataBinderFactory
,添加自定义类型转换器,再创建 ModelFactory
,添加Model
数据:
1 2 3 4 5 6 7 8 9 10 11 sequenceDiagram participant adapter as RequestMappingHandlerAdapter participant bf as WebDataBinderFactory participant mf as ModelFactory participant container as ModelAndViewContainer adapter ->> +bf: 初始化 advice: 解析@InitBinder bf -->> -adapter: 添加自定义类型转换器 adapter ->> +mf: 初始化 advice: 解析@ModelAttribute mf ->> +container: 添加 Model 数据 container -->> -mf: mf -->> -adapter:
接下来调用 ServletInvocableHandlerMethod
,主要完成三件事:
准备参数
反射调用控制器方法
处理返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 sequenceDiagram participant adapter as RequestMappingHandlerAdapter participant ihm as ServletInvocableHandlerMethod participant ar as HandlerMethodArgumentResolverComposite participant rh as HandlerMethodReturnValueHandlerComposite participant container as ModelAndViewContainer adapter ->> +ihm: invokeAndHandle ihm ->> +ar: 获取 args ar ->> ar: 有的解析器涉及 RequestBodyAdvice ar ->> container: 有的解析器涉及数据绑定生成模型数据 container -->> ar: ar -->> -ihm: args ihm ->> ihm: method.invoke(bean,args) 得到 returnValue ihm ->> +rh: 处理 returnValue rh ->> rh: 有的处理器涉及 ResponseBodyAdvice rh ->> +container: 添加 Model 数据,处理视图名,是否渲染等 container -->> -rh: rh -->> -ihm: ihm -->> -adapter: adapter ->> +container: 获取 ModelAndView container -->> -adapter:
代码演示
提供配置类 WebConfig
,其中包含一个控制器 Controller1
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Configuration public class WebConfig { @Controller static class Controller1 { @ResponseStatus(HttpStatus.OK) public ModelAndView foo (User user) { System.out.println("foo" ); return null ; } } @Getter @Setter @ToString static class User { private String name; } }
创建 Spring 容器,Mock 请求,创建 HandlerMethod
对象指定需要执行的控制器方法,创建 DataBinderFactory
数据绑定工厂。向创建的 HandlerMethod
对象中添加数据绑定工厂、参数名称解析器、参数解析器(暂不考虑返回值的处理),最后创建模型视图容器,调用 HandlerMethod
的 invokeAndHandle
方法执行控制器方法:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (WebConfig.class); MockHttpServletRequest request = new MockHttpServletRequest (); request.setParameter("name" , "lxd" ); ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod (new WebConfig .Controller1(), WebConfig.Controller1.class.getMethod("foo" , WebConfig.User.class)); ServletRequestDataBinderFactory binderFactory = new ServletRequestDataBinderFactory (null , null ); handlerMethod.setDataBinderFactory(binderFactory); handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer ()); handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context)); ModelAndViewContainer container = new ModelAndViewContainer (); handlerMethod.invokeAndHandle(new ServletWebRequest (request), container); System.out.println(container.getModel()); context.close(); } public static HandlerMethodArgumentResolverComposite getArgumentResolvers (AnnotationConfigApplicationContext context) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (context.getDefaultListableBeanFactory(), false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (context.getDefaultListableBeanFactory()), new ServletCookieValueMethodArgumentResolver (context.getDefaultListableBeanFactory()), new ExpressionValueMethodArgumentResolver (context.getDefaultListableBeanFactory()), new ServletRequestMethodArgumentResolver (), new ServletModelAttributeMethodProcessor (false ), new RequestResponseBodyMethodProcessor (List.of(new MappingJackson2HttpMessageConverter ())), new ServletModelAttributeMethodProcessor (true ), new RequestParamMethodArgumentResolver (context.getDefaultListableBeanFactory(), true ) ); return composite; }
foo
{user=WebConfig.User(name=lxd), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
26. ControllerAdvice 之 @ModelAttribute
准备 @ModelAttribute
在整个 HandlerAdapter
调用过程中所处的位置
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 26 27 28 29 30 31 sequenceDiagram participant adapter as HandlerAdapter participant bf as WebDataBinderFactory participant mf as ModelFactory participant ihm as ServletInvocableHandlerMethod participant ar as ArgumentResolvers participant rh as ReturnValueHandlers participant container as ModelAndViewContainer adapter ->> +bf: 准备 @InitBinder bf -->> -adapter: rect rgb(100, 150, 255) adapter ->> +mf: 准备 @ModelAttribute mf ->> +container: 添加 Model 数据 container -->> -mf: mf -->> -adapter: end adapter ->> +ihm: invokeAndHandle ihm ->> +ar: 获取 args ar ->> ar: 有的解析器涉及 RequestBodyAdvice ar ->> container: 有的解析器涉及数据绑定生成 Model 数据 ar -->> -ihm: args ihm ->> ihm: method.invoke(bean,args) 得到 returnValue ihm ->> +rh: 处理 returnValue rh ->> rh: 有的处理器涉及 ResponseBodyAdvice rh ->> +container: 添加 Model 数据,处理视图名,是否渲染等 container -->> -rh: rh -->> -ihm: ihm -->> -adapter: adapter ->> +container: 获取 ModelAndView container -->> -adapter:
功能与使用
@ModelAttribute
可以作用在参数上和方法上。
当其作用在参数上时,由 ServletModelAttributeMethodProcessor
解析。由解析器调用对象的构造方法,创建空对象,调用数据绑定工厂,将空对象与请求中的请求参数绑定,得到的结果作为模型数据添加到ModelAndViewContainer
中,当未指定 @ModelAttribute
的 value 时,添加到 ModelAndViewContainer
中的 key 是对象类型首字母小写对应的字符串。
当其作用在方法上时:
如果该方法加在由 @Controller
注解标记的类中,会针对当前控制器 中每个方法调用时补充模型数据。@ModelAttribute
标记的方法,如果该方法有返回值,自动将返回值添加到 ModelAndViewContainer
中。当未指定 @ModelAttribute
的 value
时,添加到 ModelAndViewContainer
中的 key 是返回值类型首字母小写对应的字符串。
如果该方法在被 @ControllerAdvice
注解标记的类中,所有控制器方法 调用时都会加入的模型数据。
作用在方法上的 @ModelAttribute
注解由 RequestMappingHandlerAdapter
解析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Controller static class Controller1 { @ResponseStatus(HttpStatus.OK) public ModelAndView foo (@ModelAttribute("u") User user) { System.out.println("foo" ); return null ; } } @ControllerAdvice static class MyControllerAdvice { @ModelAttribute("a") public String aa () { return "aa" ; } }
先不使用 RequestMappingHandlerAdapter
对作用在方法上的 @ModelAttribute
注解进行解析,沿用【25. 控制器方法执行流程】中的 main()
方法:
1 2 foo {u=WebConfig.User(name=lxd), org.springframework.validation.BindingResult.u=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
再解析方法上的 @ModelAttribute
注解:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public static void main (String[] args) throws Exception { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (WebConfig.class); RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter (); adapter.setApplicationContext(context); adapter.afterPropertiesSet(); MockHttpServletRequest request = new MockHttpServletRequest (); request.setParameter("name" , "lxd" ); ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod ( new Controller1 (), Controller1.class.getMethod("foo" , User.class)); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory (null , null ); handlerMethod.setDataBinderFactory(factory); handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer ()); handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context)); ModelAndViewContainer container = new ModelAndViewContainer (); Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory" , HandlerMethod.class, WebDataBinderFactory.class); getModelFactory.setAccessible(true ); ModelFactory modelFactory = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, factory); modelFactory.initModel(new ServletWebRequest (request), container, handlerMethod); handlerMethod.invokeAndHandle(new ServletWebRequest (request), container); System.out.println(container.getModel()); context.close(); }
foo
{a=aa, user=WebConfig.User(name=lxd), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
27. 返回值处理器
含有多种返回值的控制器
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @Slf4j static class Controller { public ModelAndView test1 () { log.info("test1()" ); ModelAndView mav = new ModelAndView ("view1" ); mav.addObject("name" , "张三" ); return mav; } public String test2 () { log.info("test2()" ); return "view2" ; } @ModelAttribute public User test3 () { log.info("test3()" ); return new User ("李四" , 20 ); } public User test4 () { log.info("test4()" ); return new User ("王五" , 30 ); } public HttpEntity<User> test5 () { log.info("test5()" ); return new HttpEntity <>(new User ("赵六" , 40 )); } public HttpHeaders test6 () { log.info("test6()" ); HttpHeaders headers = new HttpHeaders (); headers.add("Content-Type" , "text/html" ); return headers; } @ResponseBody public User test7 () { log.info("test7()" ); return new User ("钱七" , 50 ); } } @Getter @Setter @ToString @AllArgsConstructor public static class User { private String name; private int age; }
测试渲染视图需要用到的配置
为测试对视图的渲染,采用 Freemarker 进行测试,先导入 Freemarker 依赖:
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-freemarker</artifactId > </dependency >
Freemarker 配置类:
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 26 27 28 29 30 31 32 33 34 35 @Configuration public class WebConfig { @Bean public FreeMarkerConfigurer freeMarkerConfigurer () { FreeMarkerConfigurer configurer = new FreeMarkerConfigurer (); configurer.setDefaultEncoding("utf-8" ); configurer.setTemplateLoaderPath("classpath:templates" ); return configurer; } @Bean public FreeMarkerViewResolver viewResolver (FreeMarkerConfigurer configurer) { FreeMarkerViewResolver resolver = new FreeMarkerViewResolver () { @Override protected AbstractUrlBasedView instantiateView () { FreeMarkerView view = new FreeMarkerView () { @Override protected boolean isContextRequired () { return false ; } }; view.setConfiguration(configurer.getConfiguration()); return view; } }; resolver.setContentType("text/html;charset=utf-8" ); resolver.setPrefix("/" ); resolver.setSuffix(".ftl" ); resolver.setExposeSpringMacroHelpers(false ); return resolver; } }
渲染视图使用的方法:
1 2 3 4 5 6 7 8 9 10 11 12 @SuppressWarnings("all") private static void renderView (ApplicationContext context, ModelAndViewContainer container, ServletWebRequest webRequest) throws Exception { log.debug(">>>>>> 渲染视图" ); FreeMarkerViewResolver resolver = context.getBean(FreeMarkerViewResolver.class); String viewName = container.getViewName() != null ? container.getViewName() : new DefaultRequestToViewNameTranslator ().getViewName(webRequest.getRequest()); log.debug("没有获取到视图名, 采用默认视图名: {}" , viewName); View view = resolver.resolveViewName(viewName, Locale.getDefault()); view.render(container.getModel(), webRequest.getRequest(), webRequest.getResponse()); System.out.println(new String (((MockHttpServletResponse) webRequest.getResponse()).getContentAsByteArray(), StandardCharsets.UTF_8)); }
提供构造 HandlerMethodReturnValueHandlerComposite
对象的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 public static HandlerMethodReturnValueHandlerComposite getReturnValueHandler () { HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite (); composite.addHandlers(Arrays.asList( new ModelAndViewMethodReturnValueHandler (), new ViewNameMethodReturnValueHandler (), new ServletModelAttributeMethodProcessor (false ), new HttpEntityMethodProcessor (Collections.singletonList(new MappingJackson2HttpMessageConverter ())), new HttpHeadersReturnValueHandler (), new RequestResponseBodyMethodProcessor (Collections.singletonList(new MappingJackson2HttpMessageConverter ())), new ServletModelAttributeMethodProcessor (true ) )); return composite; }
测试返回值处理器的方法 testReturnValueProcessor()
利用两个函数式接口 Comsumer
,对 Mock 的请求进行补充,或者在请求处理完毕后,输出 Mock 的响应信息。
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 26 27 28 29 @SneakyThrows private static void testReturnValueProcessor (ApplicationContext context, String methodName, Consumer<MockHttpServletRequest> requestConsumer, Consumer<MockHttpServletResponse> responseConsumer) { Method method = Controller.class.getMethod(methodName); Controller controller = new Controller (); Object returnValue = method.invoke(controller); HandlerMethod handlerMethod = new HandlerMethod (context, method); ModelAndViewContainer container = new ModelAndViewContainer (); HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler(); MethodParameter returnType = handlerMethod.getReturnType(); MockHttpServletRequest request = new MockHttpServletRequest (); Optional.ofNullable(requestConsumer).ifPresent(i -> i.accept(request)); MockHttpServletResponse response = new MockHttpServletResponse (); ServletWebRequest webRequest = new ServletWebRequest (request, response); if (composite.supportsReturnType(returnType)) { composite.handleReturnValue(returnValue, returnType, container, webRequest); System.out.println(container.getModel()); System.out.println(container.getViewName()); if (!container.isRequestHandled()) { renderView(context, container, webRequest); } else { Optional.ofNullable(responseConsumer).ifPresent(i -> i.accept(response)); } } }
27.1 ModelAndView
ModelAndView
类型的返回值由 ModelAndViewMethodReturnValueHandler
处理,构造时无需传入任何参数。
解析 ModelAndView
时,将其中的视图和模型数据分别提取出来,放入 ModelAndViewContainer
中,之后根据视图信息找到对应的模板页面,再将模型数据填充到模板页面中,完成视图的渲染。
1 2 3 4 5 6 7 8 public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (WebConfig.class); test1(context); } private static void test1 (ApplicationContext context) { testReturnValueProcessor(context, "test1" , null , null ); }
对应的模板页面 view1.ftl
:
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="zh" > <head > <meta charset ="UTF-8" /> <title > view1</title > </head > <body > <h1 > Hello! ${name}</h1 > </body > </html >
20:40:56.969 [main] INFO com.itheima.a27.A27\$Controller - test1()
{name=张三}
view1
20:40:57.212 [main] INFO com.itheima.a27.A27 - \>>>>>> 渲染视图
20:40:57.212 [main] INFO com.itheima.a27.A27 - 没有获取到视图名, 采用默认视图名: view1
20:40:57.275 [main] DEBUG com.itheima.a27.WebConfig\$1\$1 - View name 'view1', model {name=张三}
20:40:57.280 [main] DEBUG com.itheima.a27.WebConfig\$1$1 - Rendering [/view1.ftl]
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="zh" > <head > <meta charset ="UTF-8" /> <title > view1</title > </head > <body > <h1 > Hello! 张三</h1 > </body > </html >
27.2 字符串类型
控制器方法的返回值是字符串类型时,返回的字符串即为视图的名称。与 ModelAndView
类型的返回值相比,不包含模型数据。
此种类型的返回值由 ViewNameMethodReturnValueHandler
处理,构造时无需传入任何参数
1 2 3 private static void test2 (ApplicationContext context) { testReturnValueProcessor(context, "test2" , null , null ); }
对应的模板页面 view2.ftl
:
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="zh" > <head > <meta charset ="UTF-8" /> <title > view2</title > </head > <body > <h1 > Hello!</h1 > </body > </html >
20:45:09.540 [main] INFO com.itheima.a27.A27\$Controller - test2()
{}
view2
20:45:09.797 [main] INFO com.itheima.a27.A27 - \>>>>>> 渲染视图
20:45:09.798 [main] INFO com.itheima.a27.A27 - 没有获取到视图名, 采用默认视图名: view2
20:45:09.856 [main] DEBUG com.itheima.a27.WebConfig\$1$1 - View name 'view2', model {}
20:45:09.861 [main] DEBUG com.itheima.a27.WebConfig\$1\$1 - Rendering [/view2.ftl]
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="zh" > <head > <meta charset ="UTF-8" /> <title > view2</title > </head > <body > <h1 > Hello!</h1 > </body > </html >
27.3 @ModelAttribute
@ModelAttribute
的用法在【26. ControllerAdvice 之 @ModelAttribute】中已经介绍过,简单来说,当 @ModelAttribute
注解作用在方法上时,会将方法的返回值作为模型数据添加到 ModelAndViewContainer
中。
@ModelAttribute
标记的方法的返回值由 ServletModelAttributeMethodProcessor
解析,构造时需要传入一个布尔类型数据 annotationNotRequired
,表示 @ModelAttribute
注解是否不是必须的。
模型数据已经有了,但视图名称又是什么呢?
在实际开发场景中,控制器方法需要被 @RequestMapping
标记,并指定请求地址,比如:
1 2 3 4 5 6 @ModelAttribute @RequestMapping("/test3") public User test3 () { log.debug("test3()" ); return new User ("李四" , 20 ); }
当未找到视图名称时,默认以请求路径作为视图名称。
但在本节测试中省略了路径映射这一步,因此需要通过编程的方式将请求路径解析后的结果放入 request 作用域中。
1 2 3 4 5 6 7 8 9 10 11 12 13 private static Consumer<MockHttpServletRequest> mockHttpServletRequestConsumer (String methodName) { return req -> { req.setRequestURI("/" + methodName); UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(req); }; } private static void test3 (ApplicationContext context) { String methodName = "test3" ; testReturnValueProcessor(context, methodName, mockHttpServletRequestConsumer(methodName), null ); }
对应的模板页面test3.ftl
:
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="zh" > <head > <meta charset ="UTF-8" /> <title > test3</title > </head > <body > <h1 > Hello! ${user.name} ${user.age}</h1 > </body > </html >
20:48:56.651 [main] INFO com.itheima.a27.A27\$Controller - test3()
{user=A27.User(name=李四, age=20)}
null
20:48:56.908 [main] INFO com.itheima.a27.A27 - \>>>>>> 渲染视图
20:48:56.911 [main] INFO com.itheima.a27.A27 - 没有获取到视图名, 采用默认视图名: test3
20:48:56.967 [main] DEBUG com.itheima.a27.WebConfig\$1\$1 - View name 'test3', model {user=A27.User(name=李四, age=20)}
20:48:56.973 [main] DEBUG com.itheima.a27.WebConfig\$1\$1 - Rendering [/test3.ftl]
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="zh" > <head > <meta charset ="UTF-8" /> <title > test3</title > </head > <body > <h1 > Hello! 李四 20</h1 > </body > </html >
针对控制器方法 test4()
也可以按照相同方式测试:
1 2 3 4 private static void test4 (ApplicationContext context) { String methodName = "test4" ; testReturnValueProcessor(context, methodName, mockHttpServletRequestConsumer(methodName), null ); }
对应的模板页面 test4.ftl
:
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="zh" > <head > <meta charset ="UTF-8" /> <title > test4</title > </head > <body > <h1 > Hello! ${user.name} ${user.age}</h1 > </body > </html >
20:50:20.654 [main] INFO com.itheima.a27.A27\$Controller - test4()
{user=A27.User(name=王五, age=30)}
null
20:50:20.907 [main] INFO com.itheima.a27.A27 - \>>>>>> 渲染视图
20:50:20.909 [main] INFO com.itheima.a27.A27 - 没有获取到视图名, 采用默认视图名: test4
20:50:20.976 [main] DEBUG com.itheima.a27.WebConfig\$1\$1 - View name 'test4', model {user=A27.User(name=王五, age=30)}
20:50:20.982 [main] DEBUG com.itheima.a27.WebConfig\$1\$1 - Rendering [/test4.ftl]
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="zh" > <head > <meta charset ="UTF-8" /> <title > test4</title > </head > <body > <h1 > Hello! 王五 30</h1 > </body > </html >
与解析参数类似,返回值处理器的执行顺序也有严格要求。
27.4 HttpEntity
HttpEntity
类型的返回值由 HttpEntityMethodProcessor
处理,构造时需要传入一个消息转换器列表。
这种类型的返回值表示响应完成,无需经过视图的解析、渲染流程再生成响应。可在处理器的handleReturnValue()
方法中得以论证:
1 2 3 4 5 6 7 8 public void handleReturnValue (@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { mavContainer.setRequestHandled(true ); }
HttpEntity
中包含了状态码、响应体信息和响应头信息。
尝试在请求处理完毕后,输出响应体信息:
1 2 3 4 5 6 7 8 9 10 private static final Consumer<MockHttpServletResponse> RESPONSE_CONSUMER = resp -> { for (String name : resp.getHeaderNames()) { System.out.println(name + " = " + resp.getHeader(name)); } System.out.println(new String (resp.getContentAsByteArray(), StandardCharsets.UTF_8)); }; private static void test5 (ApplicationContext context) { testReturnValueProcessor(context, "test5" , null , RESPONSE_CONSUMER); }
com.itheima.a27.A27\$Controller - test5()
{}
null
Content-Type = application/json
{"name":"赵六","age":40}
与 HttpEntity
相比,HttpHeaders
只包含响应头信息,HttpHeaders
类型的返回值由 HttpHeadersReturnValueHandler
处理,构造时无需传入任何参数。
与 HttpEntity
一样,这种类型的返回值也表示响应完成,无需经过视图的解析、渲染流程再生成响应,也可在处理器的 handleReturnValue()
方法中得以论证(省略源码)。
1 2 3 private static void test6 (ApplicationContext context) { testReturnValueProcessor(context, "test6" , null , RESPONSE_CONSUMER); }
20:53:54.546 [main] INFO com.itheima.a27.A27$Controller - test6()
{}
null
Content-Type:text/html
27.6 @ResponseBody
@ResponseBody
标记的方法的返回值由 RequestResponseBodyMethodProcessor
处理,构造时需要传入一个消息转换器列表。
这样的返回值也表示响应完成,无需经过视图的解析、渲染流程再生成响应,也可在处理器的 handleReturnValue()
方法中得以论证(省略源码)。
1 2 3 private static void test7 (ApplicationContext context) { testReturnValueProcessor(context, "test7" , null , RESPONSE_CONSUMER); }
com.itheima.a27.A27$Controller - test7()
{}
null
Content-Type:application/json
{"name":"钱七","age":50}
28. 消息转换器
在构造参数解析器 RequestResponseBodyMethodProcessor
、返回值解析器 HttpEntityMethodProcessor
和 RequestResponseBodyMethodProcessor
时,都需要传入消息转换器列表。
消息转换器的基类是 HttpMessageConverter
。
介绍两个常见的消息转换器的实现:
MappingJackson2HttpMessageConverter
MappingJackson2XmlHttpMessageConverter
定义User
类
1 2 3 4 5 6 7 8 9 10 11 12 13 @Getter @Setter @ToString public static class User { private String name; private int age; @JsonCreator public User (@JsonProperty("name") String name, @JsonProperty("age") int age) { this .name = name; this .age = age; } }
将 User
对象转换成 JSON
格式的数据:
1 2 3 4 5 6 7 8 9 10 @SneakyThrows public static void test1 () { MockHttpOutputMessage message = new MockHttpOutputMessage (); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter (); if (converter.canWrite(User.class, MediaType.APPLICATION_JSON)) { converter.write(new User ("张三" , 18 ), MediaType.APPLICATION_JSON, message); System.out.println(message.getBodyAsString()); } }
{“name”:“张三”,“age”:18}
将 User
对象转换成 XML
格式的数据:
1 2 3 4 5 6 7 8 9 @SneakyThrows public static void test2 () { MockHttpOutputMessage message = new MockHttpOutputMessage (); MappingJackson2XmlHttpMessageConverter converter = new MappingJackson2XmlHttpMessageConverter (); if (converter.canWrite(User.class, MediaType.APPLICATION_XML)) { converter.write(new User ("李四" , 20 ), MediaType.APPLICATION_XML, message); System.out.println(message.getBodyAsString()); } }
使用 MappingJackson2XmlHttpMessageConverter
时,需要额外导入依赖:
1 2 3 4 <dependency > <groupId > com.fasterxml.jackson.dataformat</groupId > <artifactId > jackson-dataformat-xml</artifactId > </dependency >
1 <User > <name > 李四</name > <age > 20</age > </User >
将 JSON
格式的数据转换成 User
对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @SneakyThrows public static void test3 () { String json = "{\n" + " \"name\": \"李四\",\n" + " \"age\": 20\n" + "}" ; MockHttpInputMessage message = new MockHttpInputMessage (json.getBytes(StandardCharsets.UTF_8)); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter (); if (converter.canRead(User.class, MediaType.APPLICATION_JSON)) { Object read = converter.read(User.class, message); System.out.println(read); } }
A28.User(name=李四, age=20)
如果存在多个消息转换器呢?
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 26 @SneakyThrows public static void test4 () { MockHttpServletRequest request = new MockHttpServletRequest (); MockHttpServletResponse response = new MockHttpServletResponse (); ServletWebRequest webRequest = new ServletWebRequest (request, response); RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor (Arrays.asList( new MappingJackson2HttpMessageConverter (), new MappingJackson2XmlHttpMessageConverter () )); processor.handleReturnValue( new User ("张三" , 18 ), new MethodParameter (A28.class.getMethod("user" ), -1 ), new ModelAndViewContainer (), webRequest ); System.out.println(new String (response.getContentAsByteArray(), StandardCharsets.UTF_8)); } @ResponseBody public User user () { return null ; }
将以添加的消息转换器顺序为主,比如此处会将 User
对象转换成 JSON
格式的数据:
1 { "name" : "张三" , "age" : 18 }
调换添加的消息转换器顺序:
1 2 3 4 5 6 7 8 9 10 11 @SneakyThrows public static void test4 () { RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor (Arrays.asList( new MappingJackson2XmlHttpMessageConverter (), new MappingJackson2HttpMessageConverter () )); }
这下会将 User
对象转换成 XML
格式的数据:
1 <User > <name > 张三</name > <age > 18</age > </User >
再将添加的消息转换器顺序还原,在请求头中添加 Accept
信息,指定数据格式为 XML
:
1 2 3 4 5 6 7 8 9 10 11 12 @SneakyThrows public static void test4 () { request.addHeader(HttpHeaders.ACCEPT, MimeTypeUtils.APPLICATION_XML_VALUE); RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor (Arrays.asList( new MappingJackson2HttpMessageConverter (), new MappingJackson2XmlHttpMessageConverter () )); }
尽管转换成 JSON 的转换器在前,但会以请求头中指定的 Accept
信息为主:
1 <User > <name > 张三</name > <age > 18</age > </User >
在上文基础上,指定响应的 Content-Type
为 application/json
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @SneakyThrows public static void test4 () { request.addHeader(HttpHeaders.ACCEPT, MimeTypeUtils.APPLICATION_XML_VALUE); response.setContentType(MimeTypeUtils.APPLICATION_JSON_VALUE); RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor (Arrays.asList( new MappingJackson2HttpMessageConverter (), new MappingJackson2XmlHttpMessageConverter () )); }
此时又会以 Content-Type
的信息为主:
1 { "name" : "张三" , "age" : 18 }
总结
@ResponseBody
注解由 RequestResponseBodyMethodProcessor
解析,但涉及到的数据格式转换由返回值处理器调用消息转换器完成。
当存在多个消息转换器时,如果选择 MediaType
:
首先看 @RequestMapping
注解的 produces
为主,相当于设置了响应的 Content-Type
,比如:
1 2 3 4 5 @ResponseBody @RequestMapping(produces = MimeTypeUtils.APPLICATION_JSON_VALUE) public User user () { return null ; }
再看请求头中的 Accept 是否指定了目标格式
最后按照消息转换器的添加顺序进行转换
29. ControllerAdvice 之 ResponseBodyAdvice
ResponseBodyAdvice
增强 在整个 HandlerAdapter 调用过程中所处的位置
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 26 27 28 29 30 31 sequenceDiagram participant adapter as HandlerAdapter participant bf as WebDataBinderFactory participant mf as ModelFactory participant ihm as ServletInvocableHandlerMethod participant ar as ArgumentResolvers participant rh as ReturnValueHandlers participant container as ModelAndViewContainer adapter ->> +bf: 准备 @InitBinder bf -->> -adapter: adapter ->> +mf: 准备 @ModelAttribute mf ->> +container: 添加 Model 数据 container -->> -mf: mf -->> -adapter: adapter ->> +ihm: invokeAndHandle ihm ->> +ar: 获取 args ar ->> ar: 有的解析器涉及 RequestBodyAdvice ar ->> container: 有的解析器涉及数据绑定生成 Model 数据 ar -->> -ihm: args ihm ->> ihm: method.invoke(bean,args) 得到 returnValue ihm ->> +rh: 处理 returnValue rect rgb(20, 130, 25) rh ->> rh: 有的处理器涉及 ResponseBodyAdvice end rh ->> +container: 添加 Model 数据,处理视图名,是否渲染等 container -->> -rh: rh -->> -ihm: ihm -->> -adapter: adapter ->> +container: 获取 ModelAndView container -->> -adapter:
ResponseBodyAdvice
是一个接口,对于实现了这个接口并被 @ControllerAdvice
标记的类来说,能够在调用每个控制器方法返回结果前,调用重写的 ResponseBodyAdvice
接口中的 beforeBodyWrite()
方法对返回值进行增强。
现有一个控制器类与内部使用到的 User
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Controller public static class MyController { @ResponseBody public User user () { return new User ("王五" , 18 ); } } @Getter @Setter @ToString @AllArgsConstructor public static class User { private String name; private int age; }
调用控制器方法,并输出响应数据:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public class A29 { public static void main (String[] args) throws Exception { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (WebConfig.class); ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod ( context.getBean(WebConfig.MyController.class), WebConfig.MyController.class.getMethod("user" ) ); handlerMethod.setDataBinderFactory(new ServletRequestDataBinderFactory (Collections.emptyList(), null )); handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer ()); handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context)); handlerMethod.setHandlerMethodReturnValueHandlers(getReturnValueHandlers(context)); MockHttpServletRequest request = new MockHttpServletRequest (); MockHttpServletResponse response = new MockHttpServletResponse (); ModelAndViewContainer container = new ModelAndViewContainer (); handlerMethod.invokeAndHandle(new ServletWebRequest (request, response), container); System.out.println(new String (response.getContentAsByteArray(), StandardCharsets.UTF_8)); } public static HandlerMethodArgumentResolverComposite getArgumentResolvers (AnnotationConfigApplicationContext context) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (context.getDefaultListableBeanFactory(), false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (context.getDefaultListableBeanFactory()), new ServletCookieValueMethodArgumentResolver (context.getDefaultListableBeanFactory()), new ExpressionValueMethodArgumentResolver (context.getDefaultListableBeanFactory()), new ServletRequestMethodArgumentResolver (), new ServletModelAttributeMethodProcessor (false ), new RequestResponseBodyMethodProcessor (List.of(new MappingJackson2HttpMessageConverter ())), new ServletModelAttributeMethodProcessor (true ), new RequestParamMethodArgumentResolver (context.getDefaultListableBeanFactory(), true ) ); return composite; } public static HandlerMethodReturnValueHandlerComposite getReturnValueHandlers (AnnotationConfigApplicationContext context) { HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite (); composite.addHandler(new RequestResponseBodyMethodProcessor ( Collections.singletonList(new MappingJackson2HttpMessageConverter ()) )); composite.addHandler(new ServletModelAttributeMethodProcessor (true )); return composite; } }
{“name”:“王五”,“age”:18}
在实际开发场景中常常需要对返回的数据类型进行统一,比如都返回 Result
类型:
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 26 27 28 29 30 31 @Getter @Setter @JsonInclude(JsonInclude.Include.NON_NULL) public class Result { private int code; private String msg; private Object data; @JsonCreator private Result (@JsonProperty("code") int code, @JsonProperty("data") Object data) { this .code = code; this .data = data; } private Result (int code, String msg) { this .code = code; this .msg = msg; } public static Result ok () { return new Result (200 , null ); } public static Result ok (Object data) { return new Result (200 , data); } public static Result error (String msg) { return new Result (500 , "服务器内部错误:" + msg); } }
除了直接让控制器方法返回 Result
外,还可以使用 ResponseBodyAdvice
进行增强:
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 26 27 28 29 30 @Configuration public class WebConfig { @ControllerAdvice static class MyControllerAdvice implements ResponseBodyAdvice <Object> { @Override public boolean supports (MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { if (returnType.getMethodAnnotation(ResponseBody.class) != null || AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null ) { return true ; } return false ; } @Override public Object beforeBodyWrite (Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof Result) { return body; } return Result.ok(body); } } }
进行上述增强后,再运行 main()
方法, 输出结果不变, 这是因为没有将实现的 ResponseBodyAdvice
添加到返回值处理器中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static HandlerMethodReturnValueHandlerComposite getReturnValueHandlers (AnnotationConfigApplicationContext context) { List<ControllerAdviceBean> annotatedBeans = ControllerAdviceBean.findAnnotatedBeans(context); List<Object> responseBodyAdviceList = annotatedBeans.stream() .filter(b -> b.getBeanType() != null && ResponseBodyAdvice.class.isAssignableFrom(b.getBeanType())) .collect(Collectors.toList()); HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite (); composite.addHandler(new RequestResponseBodyMethodProcessor ( Collections.singletonList(new MappingJackson2HttpMessageConverter ()), responseBodyAdviceList )); composite.addHandler(new ServletModelAttributeMethodProcessor (true )); return composite; }
再次运行 main()
方法,控制台输出:
{“code”:200,“data”:{“name”:“王五”,“age”:18}}
如果将控制器方法修改成以下形式,也能输出相同的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Controller @ResponseBody public static class MyController { public User user () { return new User ("王五" , 18 ); } } @RestController public static class MyController { public User user () { return new User ("王五" , 18 ); } }
30. 异常处理
DispatcherServlet
中对异常处理的核心方法是 processHandlerException()
,在这个方法中会对所有异常解析器进行遍历,然后使用每个异常解析器对异常信息进行处理。
存放异常解析器的是 DispatcherServlet
中泛型为 HandlerExceptionResolver
、名为 handlerExceptionResolvers
的列表成员变量。
HandlerExceptionResolver
是一个接口,本节讲解解析 @ExceptionHandler
注解的异常解析器 ExceptionHandlerExceptionResolver
。
四个控制器类,测试异常处理方法被 @ResponseBody
注解标记、异常处理方法返回 ModelAndView
、嵌套异常和对异常处理方法的参数处理:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 static class Controller1 { public void foo () { } @ResponseBody @ExceptionHandler public Map<String, Object> handle (ArithmeticException e) { return Collections.singletonMap("error" , e.getMessage()); } } static class Controller2 { public void foo () { } @ExceptionHandler public ModelAndView handler (ArithmeticException e) { return new ModelAndView ("test2" , Collections.singletonMap("error" , e.getMessage())); } } static class Controller3 { public void foo () { } @ResponseBody @ExceptionHandler public Map<String, Object> handle (IOException e) { return Collections.singletonMap("error" , e.getMessage()); } } static class Controller4 { public void foo () {} @ExceptionHandler @ResponseBody public Map<String, Object> handle (Exception e, HttpServletRequest request) { System.out.println(request); return Collections.singletonMap("error" , e.getMessage()); } }
根据handlerMethod
得知异常是在哪个类中触发,在类中是否存在标注@ExceptionHandler
的方法,将方法参数类型与实际异常类型匹配,匹配成功,则进行异常处理。
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 26 27 28 29 30 31 public static void main (String[] args) throws Exception { ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver (); resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter ())); resolver.afterPropertiesSet(); MockHttpServletRequest request = new MockHttpServletRequest (); MockHttpServletResponse response = new MockHttpServletResponse (); HandlerMethod handlerMethod = new HandlerMethod (new Controller1 (), Controller1.class.getMethod("foo" )); Exception e = new ArithmeticException ("/ zero" ); resolver.resolveException(request, response, handlerMethod, e); System.out.println(new String (response.getContentAsByteArray(), StandardCharsets.UTF_8)); System.out.println("-----------------------------------------------------------" ); handlerMethod = new HandlerMethod (new Controller2 (), Controller2.class.getMethod("foo" )); ModelAndView modelAndView = resolver.resolveException(request, response, handlerMethod, e); System.out.println(modelAndView.getModel()); System.out.println(modelAndView.getViewName()); System.out.println("-----------------------------------------------------------" ); e = new Exception ("e1" , new RuntimeException ("e2" , new IOException ("e3" ))); handlerMethod = new HandlerMethod (new Controller3 (), Controller3.class.getMethod("foo" )); resolver.resolveException(request, response, handlerMethod, e); System.out.println(new String (response.getContentAsByteArray(), StandardCharsets.UTF_8)); System.out.println("-----------------------------------------------------------" ); e = new Exception ("e1" ); handlerMethod = new HandlerMethod (new Controller4 (), Controller4.class.getMethod("foo" )); resolver.resolveException(request, response, handlerMethod, e); System.out.println(new String (response.getContentAsByteArray(), StandardCharsets.UTF_8)); }
{"error":"/ zero"}
-----------------------------------------------------------
{error=/ zero}
test2
-----------------------------------------------------------
{"error":"/ zero"}{"error":"e3"}
-----------------------------------------------------------
org.springframework.mock.web.MockHttpServletRequest@298a5e20
{"error":"/ zero"}{"error":"e3"}{"error":"e1"}
31. ControllerAdvice 之 @ExceptionHandler
控制器中被 @ExceptionHandler
标记的异常处理方法只会在当前控制器中生效,如果想要某个异常处理方法全局生效,则需要将异常处理方法编写在被 @ControllerAdvice
注解标记的类中。
一个“朴素”的控制器类:
1 2 3 4 static class Controller1 { public void foo () { } }
当不存在任何异常处理方法时,调用控制器中的foo()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @SneakyThrows public static void main (String[] args) { MockHttpServletRequest request = new MockHttpServletRequest (); MockHttpServletResponse response = new MockHttpServletResponse (); ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver (); resolver.setMessageConverters(Collections.singletonList(new MappingJackson2XmlHttpMessageConverter ())); resolver.afterPropertiesSet(); HandlerMethod handlerMethod = new HandlerMethod (new Controller1 (), Controller1.class.getMethod("foo" )); Exception exception = new Exception ("e1" ); resolver.resolveException(request, response, handlerMethod, exception); System.out.println(new String (response.getContentAsByteArray(), StandardCharsets.UTF_8)); }
main()
方法运行后,控制台不输出任何信息。
编写配置类,向 Spring 容器中添加 ExceptionHandlerExceptionResolver
,并声明全局异常处理方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Configuration public class WebConfig { @ControllerAdvice static class MyControllerAdvice { @ExceptionHandler @ResponseBody public Map<String, Object> handle (Exception e) { return Collections.singletonMap("error" , e.getMessage()); } } @Bean public ExceptionHandlerExceptionResolver resolver () { ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver (); resolver.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter ())); return resolver; } }
ExceptionHandlerExceptionResolver
不再直接 new ,而是从 Spring 容器中获取:
1 2 3 4 5 6 7 8 9 10 11 12 13 @SneakyThrows public static void main (String[] args) { MockHttpServletRequest request = new MockHttpServletRequest (); MockHttpServletResponse response = new MockHttpServletResponse (); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (WebConfig.class); ExceptionHandlerExceptionResolver resolver = context.getBean(ExceptionHandlerExceptionResolver.class); HandlerMethod handlerMethod = new HandlerMethod (new Controller1 (), Controller1.class.getMethod("foo" )); Exception exception = new Exception ("e1" ); resolver.resolveException(request, response, handlerMethod, exception); System.out.println(new String (response.getContentAsByteArray(), StandardCharsets.UTF_8)); }
{"error":"e1"}
ExceptionHandlerExceptionResolver 触发时机
ExceptionHandlerExceptionResolver
是在什么时机下找到ControllerAdvice
中的异常处理方法呢?
ExceptionHandlerExceptionResolver
的afterPropertiesSet
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void afterPropertiesSet () { this .initExceptionHandlerAdviceCache(); this .initMessageConverters(); List handlers; if (this .argumentResolvers == null ) { handlers = this .getDefaultArgumentResolvers(); this .argumentResolvers = (new HandlerMethodArgumentResolverComposite ()).addResolvers(handlers); } if (this .returnValueHandlers == null ) { handlers = this .getDefaultReturnValueHandlers(); this .returnValueHandlers = (new HandlerMethodReturnValueHandlerComposite ()).addHandlers(handlers); } }
进入initExceptionHandlerAdviceCache()
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 26 27 28 29 30 31 32 33 34 35 private void initExceptionHandlerAdviceCache () { if (this .getApplicationContext() != null ) { List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(this .getApplicationContext()); Iterator var2 = adviceBeans.iterator(); while (var2.hasNext()) { ControllerAdviceBean adviceBean = (ControllerAdviceBean)var2.next(); Class<?> beanType = adviceBean.getBeanType(); if (beanType == null ) { throw new IllegalStateException ("Unresolvable type for ControllerAdviceBean: " + adviceBean); } ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver (beanType); if (resolver.hasExceptionMappings()) { this .exceptionHandlerAdviceCache.put(adviceBean, resolver); } if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) { this .responseBodyAdvice.add(adviceBean); } } if (this .logger.isDebugEnabled()) { int handlerSize = this .exceptionHandlerAdviceCache.size(); int adviceSize = this .responseBodyAdvice.size(); if (handlerSize == 0 && adviceSize == 0 ) { this .logger.debug("ControllerAdvice beans: none" ); } else { this .logger.debug("ControllerAdvice beans: " + handlerSize + " @ExceptionHandler, " + adviceSize + " ResponseBodyAdvice" ); } } } }
与ExceptionHandler
类似的还有前面介绍的RequestMappingHandlerAdapter
RequestMappingHandlerAdapter
的afterPropertiesSet()
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void afterPropertiesSet () { this .initControllerAdviceCache(); this .initMessageConverters(); List handlers; if (this .argumentResolvers == null ) { handlers = this .getDefaultArgumentResolvers(); this .argumentResolvers = (new HandlerMethodArgumentResolverComposite ()).addResolvers(handlers); } if (this .initBinderArgumentResolvers == null ) { handlers = this .getDefaultInitBinderArgumentResolvers(); this .initBinderArgumentResolvers = (new HandlerMethodArgumentResolverComposite ()).addResolvers(handlers); } if (this .returnValueHandlers == null ) { handlers = this .getDefaultReturnValueHandlers(); this .returnValueHandlers = (new HandlerMethodReturnValueHandlerComposite ()).addHandlers(handlers); } }
进入initControllerAdviceCache()
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 private void initControllerAdviceCache () { if (this .getApplicationContext() != null ) { List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(this .getApplicationContext()); List<Object> requestResponseBodyAdviceBeans = new ArrayList (); Iterator var3 = adviceBeans.iterator(); while (var3.hasNext()) { ControllerAdviceBean adviceBean = (ControllerAdviceBean)var3.next(); Class<?> beanType = adviceBean.getBeanType(); if (beanType == null ) { throw new IllegalStateException ("Unresolvable type for ControllerAdviceBean: " + adviceBean); } Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS); if (!attrMethods.isEmpty()) { this .modelAttributeAdviceCache.put(adviceBean, attrMethods); } Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS); if (!binderMethods.isEmpty()) { this .initBinderAdviceCache.put(adviceBean, binderMethods); } if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) { requestResponseBodyAdviceBeans.add(adviceBean); } } if (!requestResponseBodyAdviceBeans.isEmpty()) { this .requestResponseBodyAdvice.addAll(0 , requestResponseBodyAdviceBeans); } if (this .logger.isDebugEnabled()) { int modelSize = this .modelAttributeAdviceCache.size(); int binderSize = this .initBinderAdviceCache.size(); int reqCount = this .getBodyAdviceCount(RequestBodyAdvice.class); int resCount = this .getBodyAdviceCount(ResponseBodyAdvice.class); if (modelSize == 0 && binderSize == 0 && reqCount == 0 && resCount == 0 ) { this .logger.debug("ControllerAdvice beans: none" ); } else { this .logger.debug("ControllerAdvice beans: " + modelSize + " @ModelAttribute, " + binderSize + " @InitBinder, " + reqCount + " RequestBodyAdvice, " + resCount + " ResponseBodyAdvice" ); } } } }
32. Tomcat 异常处理
可以使用@ExceptionHandler
和 @ControllerAdvice
全局对控制器方法中抛出的异常进行处理,但针对诸如 filter
等,非控制器方法中的异常就变得无能为力了。
因此需要一个更上层的“异常处理者”,这个“异常处理者”就是 Tomcat 服务器。
32.1 Tomcat 的错误页处理
首先将“老三样”利用配置类添加到 Spring 容器中,还要将 RequestMappingHandlerMapping
和 RequestMappingHandlerAdapter
也添加到 Spring 容器中。
必要的控制器也不能少,控制器方法手动制造异常,但不提供使用 @ExceptionHandler
实现的异常处理方法,将产生的异常交由 Tomcat 处理:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @Configuration public class WebConfig { @Bean public TomcatServletWebServerFactory servletWebServerFactory () { return new TomcatServletWebServerFactory (); } @Bean public DispatcherServlet dispatcherServlet () { return new DispatcherServlet (); } @Bean public DispatcherServletRegistrationBean servletRegistrationBean (DispatcherServlet dispatcherServlet) { DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean (dispatcherServlet, "/" ); registrationBean.setLoadOnStartup(1 ); return registrationBean; } @Bean public RequestMappingHandlerMapping requestMappingHandlerMapping () { return new RequestMappingHandlerMapping (); } @Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter () { RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter (); handlerAdapter.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter ())); return handlerAdapter; } @Controller public static class MyController { @RequestMapping("test") public ModelAndView test () { int i = 1 / 0 ; return null ; } } }
利用 AnnotationConfigServletWebServerApplicationContext
创建 Spring Web 容器,并输出所有的路径映射信息:
1 2 3 4 5 6 public static void main (String[] args) { AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext (WebConfig.class); RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class); handlerMapping.getHandlerMethods().forEach((k, v) -> System.out.println("映射路径: " + k + "\t方法信息: " + v)); }
映射路径: { [/test]} 方法信息: com.itheima.a32.WebConfig$MyController#test()
在浏览器中访问 http://localhost:8080/test
地址:
显示 Tomcat 的错误处理页,并在页面中输出了错误信息。
Tomcat 默认提供的错误处理方式返回的是 HTML 格式的数据,但需要返回 JSON 格式的数据又该怎么自定义呢?
修改 Tomcat 默认的错误处理路径,并添加后置处理器进行注册:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Bean public ErrorPageRegistrar errorPageRegistrar () { return webServerFactory -> webServerFactory.addErrorPages(new ErrorPage ("/error" )); } @Bean public ErrorPageRegistrarBeanPostProcessor errorPageRegistrarBeanPostProcessor () { return new ErrorPageRegistrarBeanPostProcessor (); }
重启程序,再次在浏览器中访问 http://localhost:8080/test
,此时页面上不再显示 Tomcat 的默认错误处理页,而是产生了 404
错误。
这是因为整个程序中并没有名称为 error
的页面,或者为/error
的请求路径。在控制器中添加请求路径为 /error
的控制器方法,该方法被 @ResponseBody
标记,最终返回 JSON 格式的数据:
1 2 3 4 5 6 7 @RequestMapping("/error") @ResponseBody public Map<String, Object> error (HttpServletRequest request) { Throwable e = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); return Collections.singletonMap("error" , e.getMessage()); }
再次重启程序,控制台输出的路径映射信息多了一条:
映射路径: { [/error]} 方法信息: com.itheima.a32.WebConfig\$MyController#error(HttpServletRequest)
映射路径: { [/test]} 方法信息: com.itheima.a32.WebConfig$MyController#test()
在浏览器中访问 http://localhost:8080/test
:
32.2 BasicErrorController
BasicErrorController
是由 SpringBoot 提供的类,它也是一个控制器:
1 2 3 4 5 @Controller @RequestMapping({"${server.error.path:${error.path:/error}}"}) public class BasicErrorController extends AbstractErrorController { }
它的映射路径会先从配置文件中读取,在未进行任何配置的情况下,默认路径是 /error
。
向容器中添加 BasicErrorController
,构造 BasicErrorController
时需要传递两个参数:
errorAttributes
:需要显示的错误信息对象
errorProperties
:读取配置文件中的键值信息,定制处理
1 2 3 4 @Bean public BasicErrorController basicErrorController () { return new BasicErrorController (new DefaultErrorAttributes (), new ErrorProperties ()); }
移除前文添加的error()
控制器方法。
再次重启程序,控制台输出的路径映射信息为:
映射路径: { [/error]} 方法信息: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
映射路径: { [/test]} 方法信息: com.itheima.a32.WebConfig$MyController#test()
映射路径: { [/error], produces [text/html]} 方法信息: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)
路径映射信息多了两条,它们的请求路径一样,但根据不同的请求来源返回不同格式的数据。
使用接口测试工具访问
如果采用 Postman 等接口测试工具访问 http://localhost:8080/test
路径时,将返回 JSON 格式的数据,比如
1 2 3 4 5 6 { "timestamp" : 1737641294401 , "status" : 500 , "error" : "Internal Server Error" , "path" : "/test" }
timestamp
、status
等响应内容就是错误属性 errorAttributes
的中包含的内容。
返回的数据中并没有显示异常信息,可以通过配置文件进行配置:
1 server.error.include-exception =true
也可以在添加 BasicErrorController
到 Spring 容器中时,设置错误属性 errorProperties
:
1 2 3 4 5 6 @Bean public BasicErrorController basicErrorController () { ErrorProperties errorProperties = new ErrorProperties (); errorProperties.setIncludeException(true ); return new BasicErrorController (new DefaultErrorAttributes (), errorProperties); }
重启程序,再次使用接口测试工具访问 http://localhost:8080/test
:
1 2 3 4 5 6 7 { "timestamp" : 1737641411625 , "status" : 500 , "error" : "Internal Server Error" , "exception" : "java.lang.ArithmeticException" , "path" : "/test" }
使用浏览器访问
如果使用浏览器访问 http://localhost:8080/test
,又会回到“解放前”,显示与 Tomcat 的默认错误处理页相同的内容。
这是因为使用浏览器访问时,将调用 BasicErrorController
中的 errorHtml()
控制器方法:
1 2 3 4 5 6 7 8 9 10 @RequestMapping( produces = {"text/html"} ) public ModelAndView errorHtml (HttpServletRequest request, HttpServletResponse response) { HttpStatus status = this .getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(this .getErrorAttributes(request, this .getErrorAttributeOptions(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = this .resolveErrorView(request, response, status, model); return modelAndView != null ? modelAndView : new ModelAndView ("error" , model); }
该方法返回 ModelAndView
,并且在没有添加新的错误视图的情况下,尝试寻找视图名称为 error
的视图。
这里既没有添加新的错误视图,也没有名称为 error
的视图,因此最终又会交由 Tomcat 进行处理。
尝试向 Spring 容器中添加一个 View
视图,Bean 的名字 必须 是 error
:
1 2 3 4 5 6 7 8 @Bean public View error () { return (model, request, response) -> { System.out.println(model); response.setContentType("text/html;charset=utf-8" ); response.getWriter().print("<h3>服务器内部错误</h3>" ); }; }
为了能够在查找指定名称的视图时,按照 View 类型的 Bean 的名称进行匹配,还需要添加一个解析器:
1 2 3 4 5 @Bean public ViewResolver viewResolver () { return new BeanNameViewResolver (); }
重启程序,使用浏览器访问 http://localhost:8080/test
:
控制台还打印出:
{timestamp=Thu Jan 23 22:14:25 CST 2025, status=500, error=Internal Server Error, exception=java.lang.ArithmeticException, path=/test}
33. BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter
33.1 功能与使用
BeanNameUrlHandlerMapping
与 RequestMappingHandlerMapping
类似,也是用于解析请求路径,只不过 BeanNameUrlHandlerMapping
将根据请求路径在 Spring 容器中寻找同名的 Bean,对请求进行处理,这个 Bean 必须 以/
开头。比如:请求路径为 /c1
,寻找的 Bean 的名称也是/c1
。
SimpleControllerHandlerAdapter
与 RequestMappingHandlerAdapter
也类似,也是用于调用控制器方法,但要求控制器类必须实现 org.springframework.web.servlet.mvc.Controller
接口。
现有三个控制器类:
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 @Component("/c1") public static class Controller1 implements Controller { @Override public ModelAndView handleRequest (HttpServletRequest request, HttpServletResponse response) throws Exception { response.getWriter().print("this is c1" ); return null ; } } @Component("/c2") public static class Controller2 implements Controller { @Override public ModelAndView handleRequest (HttpServletRequest request, HttpServletResponse response) throws Exception { response.getWriter().print("this is c2" ); return null ; } } @Bean("/c3") public Controller controller3 () { return (request, response) -> { response.getWriter().print("this is c3" ); return null ; }; }
提供配置类 WebConfig,添加 Web 换件下必要的 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 26 27 28 29 30 31 32 33 @Configuration public class WebConfig { @Bean public TomcatServletWebServerFactory servletWebServerFactory () { return new TomcatServletWebServerFactory (); } @Bean public DispatcherServlet dispatcherServlet () { return new DispatcherServlet (); } @Bean public DispatcherServletRegistrationBean servletRegistrationBean (DispatcherServlet dispatcherServlet) { DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean (dispatcherServlet, "/" ); registrationBean.setLoadOnStartup(1 ); return registrationBean; } @Bean public BeanNameUrlHandlerMapping beanNameUrlHandlerMapping () { return new BeanNameUrlHandlerMapping (); } @Bean public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter () { return new SimpleControllerHandlerAdapter (); } }
1 2 3 4 5 6 7 public class A33 { public static void main (String[] args) { AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext (WebConfig.class); } }
运行 main()
方法后,在浏览器中访问 http://localhost:8080/c1
,页面上显示 this is c1
。更换请求路径为 c2
、c3
后,也会出现类似的信息。
33.2 自定义实现
在配置类 WebConfig
中移除 Spring 提供的 BeanNameUrlHandlerMapping
与 SimpleControllerHandlerAdapter
,手动编码实现它们的功能。
为了与前文的测试形成对比,将 Controller2
的 Bean 名称设置为 c2
,而不是 /c2
,使其不能被解析到。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 @Component static class MyHandlerMapping implements HandlerMapping { @Override public HandlerExecutionChain getHandler (HttpServletRequest request) throws Exception { String key = request.getRequestURI(); Controller controller = controllerMap.get(key); if (controller == null ) { return null ; } return new HandlerExecutionChain (controller); } @Autowired private ApplicationContext context; private Map<String, Controller> controllerMap; @PostConstruct public void init () { controllerMap = context.getBeansOfType(Controller.class).entrySet().stream() .filter(i -> i.getKey().startsWith("/" )) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } } @Component static class MyHandlerAdapter implements HandlerAdapter { @Override public boolean supports (Object handler) { return handler instanceof Controller; } @Override public ModelAndView handle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof Controller) { ((Controller) handler).handleRequest(request, response); } return null ; } @Override public long getLastModified (HttpServletRequest request, Object handler) { return -1 ; } }
运行 main()
方法后,在浏览器中访问:
http://localhost:8080/c1
,页面上显示 this is c1
;
http://localhost:8080/c2
,页面上显示 404
;
http://localhost:8080/c3
,页面上显示 this is c3
。
34. RouterFunctionMapping 与 HandlerFunctionAdapter
RouterFunctionMapping
在初始化时,在 Spring 容器中收集所有 RouterFunction
,RouterFunction
包括两部分:
RequestPredicate
:设置映射条件
HandlerFunction
:处理逻辑
当请求到达时,根据映射条件找到 HandlerFunction
,即 handler
,然后使用 HandlerFunctionAdapter
调用 handler
。
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 26 27 28 29 30 31 32 33 34 35 36 37 @Configuration public class WebConfig { @Bean public TomcatServletWebServerFactory servletWebServerFactory () { return new TomcatServletWebServerFactory (8080 ); } @Bean public DispatcherServlet dispatcherServlet () { return new DispatcherServlet (); } @Bean public DispatcherServletRegistrationBean servletRegistrationBean (DispatcherServlet dispatcherServlet) { return new DispatcherServletRegistrationBean (dispatcherServlet, "/" ); } @Bean public RouterFunctionMapping routerFunctionMapping () { return new RouterFunctionMapping (); } @Bean public HandlerFunctionAdapter handlerFunctionAdapter () { return new HandlerFunctionAdapter (); } @Bean public RouterFunction<ServerResponse> r1 () { return route(GET("/r1" ), req -> ok().body("this is r1" )); } @Bean public RouterFunction<ServerResponse> r2 () { return route(GET("/r2" ), req -> ok().body("this is r2" )); } }
1 2 3 4 5 6 public class A34 { public static void main (String[] args) { AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext (WebConfig.class); } }
运行 main()
方法后,在浏览器中访问 http://localhost:8080/r1
,页面上显示 this is r1
,访问 r2
时也类似。
35. SimpleUrlHandlerMapping 与 HttpRequestHandlerAdapter
35.1 功能与使用
概括一下,这两个主要用于静态资源处理,SimpleUrlHandlerMapping
用于静态资源映射,而静态资源处理器是 ResourceHttpRequestHandler
,HttpRequestHandlerAdapter
用于处理器调用。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Configuration public class WebConfig { @Bean public TomcatServletWebServerFactory servletWebServerFactory () { return new TomcatServletWebServerFactory (8080 ); } @Bean public DispatcherServlet dispatcherServlet () { return new DispatcherServlet (); } @Bean public DispatcherServletRegistrationBean servletRegistrationBean (DispatcherServlet dispatcherServlet) { return new DispatcherServletRegistrationBean (dispatcherServlet, "/" ); } @Bean public SimpleUrlHandlerMapping simpleUrlHandlerMapping (ApplicationContext context) { SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping (); Map<String, ResourceHttpRequestHandler> map = context.getBeansOfType(ResourceHttpRequestHandler.class); mapping.setUrlMap(map); return mapping; } @Bean public HttpRequestHandlerAdapter httpRequestHandlerAdapter () { return new HttpRequestHandlerAdapter (); } @Bean("/**") public ResourceHttpRequestHandler handler1 () { ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler (); handler.setLocations(Collections.singletonList(new ClassPathResource ("static/" ))); return handler; } @Bean("/img/**") public ResourceHttpRequestHandler handler2 () { ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler (); handler.setLocations(Collections.singletonList(new ClassPathResource ("images/" ))); return handler; } }
添加的两个 ResourceHttpRequestHandler
类型的 Bean,分别设置了它们处理 ClassPath 路径下哪个目录下的静态资源,那如何将请求路径与静态资源访问路径进行映射呢?
也就是说,当要访问 ClassPath 路径下的 static
目录下的静态资源时,应该通过哪个请求路径呢?
可以利用通配符设置添加的 ResourceHttpRequestHandler
类型的 Bean 的名称。
比如设置 Bean 的名称为 /**
,那么在访问 localhost:8080/r1.html
时,就会尝试访问 ClassPath 路径下 static
目录中名为 r1.html
的静态资源;又比如设置 Bean 的名称为 /img/**
,那么在访问 localhost:8080/img/1.jpg
时, 就会尝试访问 ClassPath 路径下 images
目录中名为 1.jpg
的静态资源。
35.2 资源解析器
ResourceHttpRequestHandler
用于对静态资源进行处理,但静态资源解析的功能是由 ResourceResolver
完成的。
ResourceHttpRequestHandler
实现了 InitializingBean
接口,查看重写的 afterPropertiesSet()
:
1 2 3 4 5 6 7 8 9 @Override public void afterPropertiesSet () throws Exception { resolveResourceLocations(); if (this .resourceResolvers.isEmpty()) { this .resourceResolvers.add(new PathResourceResolver ()); } }
当使用的资源解析器列表为空时,默认添加最基本的资源解析器 PathResourceResolver
。
尝试添加额外的资源解析器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Bean("/**") public ResourceHttpRequestHandler handler1 () { ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler (); handler.setLocations(Collections.singletonList(new ClassPathResource ("static/" ))); handler.setResourceResolvers(List.of( new CachingResourceResolver (new ConcurrentMapCache ("cache1" )), new EncodedResourceResolver (), new PathResourceResolver () )); return handler; }
添加了三个资源解析器:
CachingResourceResolver
:对静态资源进行缓存
EncodedResourceResolver
:对静态资源进行压缩
PathResourceResolver
:最基本的资源处理器
还要注意添加的顺序,先尝试从缓存中获取,再尝试获取压缩文件,最后才是直接从磁盘上读取。
针对 EncodedResourceResolver
来说,Spring 不会自行对静态资源进行压缩,需要在配置类中提供压缩方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @PostConstruct @SuppressWarnings("all") public void initGzip () throws IOException { Resource resource = new ClassPathResource ("static" ); File dir = resource.getFile(); for (File file : dir.listFiles(pathname -> pathname.getName().endsWith(".html" ))) { System.out.println(file); try (FileInputStream fis = new FileInputStream (file); GZIPOutputStream fos = new GZIPOutputStream (new FileOutputStream (file.getAbsoluteFile() + ".gz" ))) { byte [] bytes = new byte [8 * 1024 ]; int len; while ((len = fis.read(bytes)) != -1 ) { fos.write(bytes, 0 , len); } } } }
配置类对应的 Bean 初始化阶段时,将 ClassPath 路径下 static
目录中的静态资源进行压缩。
比如 static
目录下的 r1.html
会被压缩成 r1.html.gz
,在访问 r1.html
时,会访问压缩文件 r1.html.gz
,由浏览器识别并解压成 r1.html
进行访问,减少网络传输数据量。
35.3 欢迎页处理
将访问 根路径 的请求,映射到某一欢迎页。这个功能由 WelcomePageHandlerMapping
完成。
设置静态资源欢迎页为 ClassPath 下 static
目录中的 index.html
文件:
1 2 3 4 5 6 7 8 9 10 @Bean public WelcomePageHandlerMapping welcomePageHandlerMapping (ApplicationContext context) { Resource resource = context.getResource("classpath:static/index.html" ); return new WelcomePageHandlerMapping ( null , context, resource, "/**" ); }
程序会根据配置的欢迎页映射器生成一个实现了 Controller
接口的处理器,使用 SimpleControllerHandlerAdapter
执行生成的处理器:
1 2 3 4 @Bean public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter () { return new SimpleControllerHandlerAdapter (); }
重启程序,控制台会输出一条如下的日志,表示欢迎页配置成功:
o.s.b.a.w.s.WelcomePageHandlerMapping - Adding welcome page: class path resource [static/index.html]
如果重启程序后访问 localhost:8080
并没有跳转到配置的欢迎页,可以重新编译项目后在运行。
总结
WelcomePageHandlerMapping
作为欢迎页映射器,只将根路径,即 /
映射到配置的欢迎页。
它内置了一个处理器,名为 ParameterizableViewController
,该处理器不执行逻辑,仅根据固定的视图名 forward:index.html
去寻找视图。
SimpleControllerHandlerAdapter
用于调用处理器,根据重定向到根路径的 index.html 页面,执行静态资源处理器,访问 static
目录下的 index.html
文件(在配置类中自行配置的)。
35.4 映射器与适配器总结
HandlerMapping
用于建立请求路径与控制器之间的映射关系:
RequestMappingHandlerMapping
:解析 @RequestMapping
及其派生注解,建立请求路径与控制器方法之间的映射关系
WelcomePageHandlerMapping
:映射 /
根路径,寻找欢迎页
BeanNameUrlHandlerMapping
:与 Bean 的名称进行匹配,要求名称必须以 /
开头
RouterFunctionMapping
:将 RequestPredicate
映射到 HandlerFunction
SimpleUrlHandlerMapping
:静态资源映射
映射器之间的顺序也是有要求的,SpringBoot 中的映射器按上述顺序排序。
HandlerAdapter
用于对各种处理器进行适配调用(适配器 模式):
RequestMappingHandlerAdapter
:执行被 @RequestMapping
标记的控制器方法,内部还会使用参数解析器、返回值处理器对控制器方法的参数、返回值进行处理(组合 模式)
SimpleControllerHandlerAdapter
:执行实现了 Controller
接口的处理器
HandlerFunctionAdapter
:处理 HandlerFunction
函数式接口
HttpRequestHandlerAdapter
:处理 HttpRequestHandler
接口,用于静态资源处理
ResourceHttpRequestHandler
中的 setResourceResolvers()
方法是 责任链 模式体现
36. MVC 处理流程
当浏览器发送一个请求 http://localhost:8080/hello
后,请求到达服务器,其处理流程是:
服务器提供了 DispatcherServlet
,它使用的是标准 Servlet 技术
路径:默认映射路径为 /
,即会匹配到所有请求 URL,可作为请求的统一入口,也被称之为前控制器
jsp 不会匹配到 DispatcherServlet
其它有路径的 Servlet 匹配优先级也高于 DispatcherServlet
创建:在 SpringBoot 中,由 DispatcherServletAutoConfiguration
这个自动配置类提供 DispatcherServlet
的 bean
初始化:DispatcherServlet
初始化时会优先到容器里寻找各种组件,作为它的成员变量
HandlerMapping
,初始化时记录映射关系
HandlerAdapter
,初始化时准备参数解析器、返回值处理器、消息转换器
HandlerExceptionResolver
,初始化时准备参数解析器、返回值处理器、消息转换器
ViewResolver
DispatcherServlet
会利用 RequestMappingHandlerMapping
查找控制器方法
例如根据 /hello
路径找到 @RequestMapping("/hello")
对应的控制器方法
控制器方法会被封装为 HandlerMethod
对象,并结合匹配到的拦截器一起返回给 DispatcherServlet
HandlerMethod
和拦截器合在一起称为 HandlerExecutionChain
(调用链)对象
DispatcherServlet
接下来会:
调用拦截器的 preHandle
方法,返回一个布尔类型的值。若返回 true,则放行,进行后续调用,反之拦截请求,不进行后续调用;
RequestMappingHandlerAdapter
调用处理器方法,准备数据绑定工厂、模型工厂、将 HandlerMethod
完善为 ServletInvocableHandlerMethod
@ControllerAdvice 全局增强点 1️⃣:利用 @ModelAttribute
补充模型数据
@ControllerAdvice 全局增强点 2️⃣:利用 @InitBinder
补充自定义类型转换器
使用 HandlerMethodArgumentResolver
准备参数
@ControllerAdvice 全局增强点 3️⃣:利用 RequestBodyAdvice
接口对请求体增强
调用 ServletInvocableHandlerMethod
使用 HandlerMethodReturnValueHandler
处理返回值
根据 ModelAndViewContainer
获取 ModelAndView
如果返回的 ModelAndView
为 null,不走第 4 步视图解析及渲染流程
例如,有的返回值处理器调用了 HttpMessageConverter
来将结果转换为 JSON,这时 ModelAndView
就为 null
如果返回的 ModelAndView
不为 null,会在第 4 步走视图解析及渲染流程
@ControllerAdvice 全局增强点 4️⃣:利用 ResponseBodyAdvice
对响应体增强
调用拦截器的 postHandle
方法
处理异常或视图渲染
如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver
处理异常流程
@ControllerAdvice
全局增强点 5️⃣:利用 @ExceptionHandler
进行统一异常处理
正常,走视图解析及渲染流程
调用拦截器的 afterCompletion()
方法
37. SpringBoot 骨架项目
使用 IDEA 创建 SpringBoot 项目时,会创建出 .mvn
目录、HELP.md
、mvnw
和 mvnw.cmd
等不必要的文件。
如果是 Linux 环境下,执行以下命令获取 SpringBoot 的骨架,并添加 web
、mysql
、mybatis
依赖:
1 curl -G https://start.spring.io/pom.xml -d dependencies=web,mysql,mybatis -o pom.xml
也可以使用 Postman 等接口测试工具来实现。
更多用法执行以下命令进行参考:
1 curl https://start.spring.io
38. SpringBoot War 项目
38.1 项目的构建
利用 IDEA 创建新模块 test_war
,区别在于选择的打包方式是 War
:
选择依赖时,勾选 Spring Web。
一般来说,选择 War
作为打包方式都是为了使用 JSP,因为 JSP 不能配合 Jar
打包方式使用。
JSP 文件的存放路径是固定的,在 src/main 目录下的 webapp
目录,如果没有 webapp
目录,需要自行创建。之后新建 hello.jsp
:
1 2 3 4 5 6 7 8 9 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h3>Hello!</h3> </body> </html>
之后新建控制器类 HelloController
,编写控制器方法 hello()
,返回值类型是 String
,要求返回的是视图名称:
1 2 3 4 5 6 7 @Controller public class HelloController { @RequestMapping("/hello") public String hello () { return "hello" ; } }
最后要在配置文件中配置视图的前缀、后缀,使控制器方法返回的视图名称对应视图名称的 JSP 页面:
1 2 spring.mvc.view.prefix =/ spring.mvc.view.suffix =.jsp
38.2 项目的测试
使用外置 Tomcat 测试
首先得安装外置 Tomcat,省略安装步骤。
然后在 IDEA 的 Run/Debug Configurations 中进行配置,选择安装的外置 Tomcat:
然后在 Deployment 中指定当前项目的部署方式和应用程序上下文路径:
尽管使用外置 Tomcat 进行测试,但主启动类不能少:
1 2 3 4 5 6 @SpringBootApplication public class TestWarApplication { public static void main (String[] args) { SpringApplication.run(TestWarApplication.class, args); } }
除此之外,还要编写 ServletInitializer
,在外置 Tomcat 启动时,找到 SpringBoot 项目的主启动类,执行 SpringBoot 流程:
1 2 3 4 5 6 public class ServletInitializer extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure (SpringApplicationBuilder application) { return application.sources(TestWarApplication.class); } }
如果没有 ServletInitializer
类,则无法使 SpringBoot 项目使用外置 Tomcat。
运行程序后,访问 localhost:8080/hello
,页面进入编写的 hello.jsp
页面。
使用内嵌 Tomcat 测试
打包方式为 Jar 时,直接运行主启动类,然后访问对应的请求路径即可跳转到指定的视图中,那打包访问变成 War 之后,使用这种方式还能够成功跳转吗?
程序运行成功后,访问 localhost:8080/hello
,页面并没有按照预期跳转到 hello.jsp
页面中,而是下载了该页面。
这是因为内嵌 Tomcat 中不具备 JSP 解析能力,如果要想使其具备解析 JSP 的能力,需要添加依赖:
1 2 3 4 5 <dependency > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-jasper</artifactId > <scope > provided</scope > </dependency >
之后再访问 localhost:8080/hello
,页面进入编写的 hello.jsp
页面。
使用内嵌 Tomcat 测试遇到的问题
添加 tomcat-embed-jasper
依赖后,访问 localhost:8080/hello
,仍在下载 hello.jsp
。
答:清理浏览器缓存,在浏览器的 DevTools 中的 Network 内 勾选 Disable cache
以禁用缓存。
添加 tomcat-embed-jasper
依赖后,访问 localhost:8080/hello
,页面 404。
答:设置运行主启动类的 Run/Debug Configurations 中的 Working directory 为当前模块所在目录。
参考链接:springboot 在 idea 多模块下子模块的 web 项目用内置 tomcat 启动访问 jsp 报 404
39. SpringBoot 启动过程
39.1 SpringApplication 的构造
SpringBoot 的主启动类
1 2 3 4 5 6 @SpringBootApplication public class BootApplication { public static void main (String[] args) { SpringApplication.run(BootApplication.class, args); } }
其中 SpringApplication#run()
方法是核心方法:
1 2 3 4 5 6 7 public static ConfigurableApplicationContext run (Class<?> primarySource, String... args) { return run(new Class <?>[] { primarySource }, args); } public static ConfigurableApplicationContext run (Class<?>[] primarySources, String[] args) { return new SpringApplication (primarySources).run(args); }
最终使用 new
关键字构造了 SpringApplication
对象,然后调用了非静态 run() 方法
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public SpringApplication (Class<?>... primarySources) { this (null , primarySources); } @SuppressWarnings({ "unchecked", "rawtypes" }) public SpringApplication (ResourceLoader resourceLoader, Class<?>... primarySources) { this .resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null" ); this .primarySources = new LinkedHashSet <>(Arrays.asList(primarySources)); this .webApplicationType = WebApplicationType.deduceFromClasspath(); this .bootstrapRegistryInitializers = new ArrayList <>( getSpringFactoriesInstances(BootstrapRegistryInitializer.class)); setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); this .mainApplicationClass = deduceMainApplicationClass(); }
构造 SpringApplication
对象时做了如下几件事:
获取 Bean Definition
源
推断应用类型
添加 ApplicationContext
初始化器
添加事件监听器
主类推断
获取 Bean Definition
源
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 @Configuration public class A39_1 { public static void main (String[] args) { SpringApplication spring = new SpringApplication (A39_1.class); ConfigurableApplicationContext context = spring.run(args); Arrays.stream(context.getBeanDefinitionNames()).forEach(i -> { System.out.println("name: " + i + " 来源: " + context.getBeanFactory().getBeanDefinition(i).getResourceDescription()); }); context.close(); } static class Bean1 { } static class Bean2 { } @Bean public Bean2 bean2 () { return new Bean2 (); } }
运行 main() 方法后,控制台打印出错误信息:
***************************
APPLICATION FAILED TO START
***************************
Description:
Web application could not be started as there was no org.springframework.boot.web.servlet.server.ServletWebServerFactory bean defined in the context.
Action:
Check your application's dependencies for a supported servlet web server.
Check the configured web application type.
这是因为添加了 spring-boot-starter-web
依赖,但 Spring 容器中并没有 ServletWebServerFactory
类型的 Bean。向容器中添加即可:
1 2 3 4 @Bean public TomcatServletWebServerFactory servletWebServerFactory () { return new TomcatServletWebServerFactory (); }
之后在运行 main() 方法:
name: org.springframework.context.annotation.internalConfigurationAnnotationProcessor 来源: null
name: org.springframework.context.annotation.internalAutowiredAnnotationProcessor 来源: null
name: org.springframework.context.annotation.internalCommonAnnotationProcessor 来源: null
name: org.springframework.context.event.internalEventListenerProcessor 来源: null
name: org.springframework.context.event.internalEventListenerFactory 来源: null
name: a39_1 来源: null
name: org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory 来源: null
name: bean2 来源: com.itheima.a39.A39_1
name: servletWebServerFactory 来源: com.itheima.a39.A39_1
来源为 null
的 Bean 是由 Spring 提供的“内置” Bean。
使用 XML 配置文件添加 Bean,并利用 setSources()
方法设置创建 ApplicationContext
的其他源:
1 2 3 4 5 public static void main (String[] args) { SpringApplication spring = new SpringApplication (A39_1.class); spring.setSources(Collections.singleton("classpath:b01.xml" )); }
再次运行 main() 方法,控制台打印的内容多了一条:
name: bean1 来源: class path resource [b01.xml]
推断应用类型
应用类型的推断在构造方法中可以看到:
1 2 3 4 5 6 public SpringApplication (ResourceLoader resourceLoader, Class<?>... primarySources) { this .webApplicationType = WebApplicationType.deduceFromClasspath(); }
推断逻辑由 WebApplicationType
枚举中的 deduceFromClasspath()
方法完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static WebApplicationType deduceFromClasspath () { if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null ) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null ) && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null )) { return WebApplicationType.REACTIVE; } for (String className : SERVLET_INDICATOR_CLASSES) { if (!ClassUtils.isPresent(className, null )) { return WebApplicationType.NONE; } } return WebApplicationType.SERVLET; }
利用反射调用 deduceFromClasspath()
方法:
1 2 3 4 5 6 7 8 9 10 @SneakyThrows public static void main (String[] args) { Method deduceFromClasspath = WebApplicationType.class.getDeclaredMethod("deduceFromClasspath" ); deduceFromClasspath.setAccessible(true ); System.out.println("\t应用类型为: " + deduceFromClasspath.invoke(null )); }
应用类型为: SERVLET
添加 ApplicationContext
初始化器
调用 SpringApplication
对象的 run()
方法时会创建 ApplicationContext
,最后调用 ApplicationContext
的 refresh()
方法完成初始化。
在创建与初始化完成之间的一些拓展功能就由 ApplicationContext
初始化器完成。
在 SpringApplication
的构造方法中,添加的初始化器信息从配置文件中读取:
1 2 3 4 5 6 public SpringApplication (ResourceLoader resourceLoader, Class<?>... primarySources) { setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); }
也可以调用 SpringApplication
对象的 addInitializers()
方法添加自定义初始化器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @SneakyThrows public static void main (String[] args) { spring.addInitializers(applicationContext -> { if (applicationContext instanceof GenericApplicationContext) { GenericApplicationContext context = (GenericApplicationContext) applicationContext; context.registerBean("bean3" , Bean3.class); } }); ConfigurableApplicationContext context = spring.run(args); Arrays.stream(context.getBeanDefinitionNames()).forEach(i -> { System.out.println("name: " + i + " 来源: " + context.getBeanFactory().getBeanDefinition(i).getResourceDescription()); }); context.close(); } static class Bean3 {}
运行 main() 方法后,控制台打印的 Bean 又多了一条:
name: bean3 来源: null
添加事件监听器
与添加 ApplicationContext
初始化器一样,在 SpringApplication
的构造方法中,添加的事件监听器信息从配置文件中读取:
1 2 3 4 5 6 public SpringApplication (ResourceLoader resourceLoader, Class<?>... primarySources) { setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); }
可以调用 SpringApplication
对象的 addListeners()
方法添加自定义事件监听器:
1 2 3 4 5 6 7 8 @SneakyThrows public static void main (String[] args) { spring.addListeners(event -> System.out.println("\t事件为: " + event)); context.close(); }
运行 main() 方法后,控制台打印的事件信息汇总后如下:
事件类型为: class org.springframework.boot.context.event.ApplicationStartingEvent
事件类型为: class org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
事件类型为: class org.springframework.boot.context.event.ApplicationContextInitializedEvent
事件类型为: class org.springframework.boot.context.event.ApplicationPreparedEvent
事件类型为: class org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent
事件类型为: class org.springframework.context.event.ContextRefreshedEvent
事件类型为: class org.springframework.boot.context.event.ApplicationStartedEvent
事件类型为: class org.springframework.boot.availability.AvailabilityChangeEvent
事件类型为: class org.springframework.boot.context.event.ApplicationReadyEvent
事件类型为: class org.springframework.boot.availability.AvailabilityChangeEvent
事件类型为: class org.springframework.boot.availability.AvailabilityChangeEvent
事件类型为: class org.springframework.context.event.ContextClosedEvent
主类推断
主类推断在构造方法中可以看到:
1 2 3 4 5 public SpringApplication (ResourceLoader resourceLoader, Class<?>... primarySources) { this .mainApplicationClass = deduceMainApplicationClass(); }
推断逻辑由 deduceMainApplicationClass()
方法完成,利用反射调用该方法:
1 2 3 4 5 6 7 8 9 10 @SneakyThrows public static void main (String[] args) { Method deduceMainApplicationClass = SpringApplication.class.getDeclaredMethod("deduceMainApplicationClass" ); deduceMainApplicationClass.setAccessible(true ); System.out.println("\t主类是: " + deduceMainApplicationClass.invoke(spring)); }
主类是: class com.itheima.a39.A39_1
39.2 SpringApplication#run()的分析
第一步:获取 SpringApplicationRunListeners
在执行 run()
方法时,首先会获取到 SpringApplicationRunListeners
,它是事件发布器的组合,能够在 SpringBoot 启动的各个阶段中发布事件。
SpringApplicationRunListeners
中使用 SpringApplicationRunListener
来描述单个事件发布器,SpringApplicationRunListener
是一个接口,它有且仅有一个实现类 EventPublishingRunListener
。
在 SpringBoot 中,事件发布器都是在配置文件中读取,从 META-INF/spring.factories
中读取,该文件中有这样一句:
1 2 3 org.springframework.boot.SpringApplicationRunListener =\ org.springframework.boot.context.event.EventPublishingRunListener
自行实现从 META-INF/spring.factories
配置文件中读取事件发布器信息,并发布各种事件:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class A39_2 { @SneakyThrows public static void main (String[] args) { SpringApplication app = new SpringApplication (); app.addListeners(i -> System.out.println(i.getClass())); List<String> names = SpringFactoriesLoader.loadFactoryNames( SpringApplicationRunListener.class, A39_2.class.getClassLoader() ); for (String name : names) { Class<?> clazz = Class.forName(name); Constructor<?> constructor = clazz.getConstructor(SpringApplication.class, String[].class); SpringApplicationRunListener publisher = (SpringApplicationRunListener) constructor.newInstance(app, args); DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext (); publisher.starting(bootstrapContext); publisher.environmentPrepared(bootstrapContext, new StandardEnvironment ()); GenericApplicationContext context = new GenericApplicationContext (); publisher.contextPrepared(context); publisher.contextLoaded(context); context.refresh(); publisher.started(context, null ); publisher.ready(context, null ); publisher.failed(context, new Exception ("出错了" )); } } }
在 SpringBoot 启动过程中,总共发布 7 种事件。
运行 main() 方法后,控制台打印出:
class org.springframework.boot.context.event.ApplicationStartingEvent
class org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
class org.springframework.boot.context.event.ApplicationContextInitializedEvent
class org.springframework.boot.context.event.ApplicationPreparedEvent
class org.springframework.context.event.ContextRefreshedEvent
class org.springframework.boot.context.event.ApplicationStartedEvent
class org.springframework.boot.availability.AvailabilityChangeEvent
class org.springframework.boot.context.event.ApplicationReadyEvent
class org.springframework.boot.availability.AvailabilityChangeEvent
class org.springframework.boot.context.event.ApplicationFailedEvent
但打印出的事件种类并不止 7 种,这是因为包含了其他事件发布器发布的事件,EventPublishingRunListener
发布的事件的全限定类名包含 boot.context.event
,根据这个条件重新计算,恰好 7 个。
第二步:封装启动 args
调用 DefaultApplicationArguments
的构造方法,传入 args
即可:
1 2 3 4 5 6 7 8 @SneakyThrows @SuppressWarnings("all") public static void main (String[] args) { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 2. 封装启动 args" ); DefaultApplicationArguments arguments = new DefaultApplicationArguments (args); }
第三步:准备 Environment
添加命令行参数
Environment
即环境对象,是对配置信息的抽象,配置信息的来源有多种,比如:系统环境变量、properties 配置文件、YAML 配置文件等等。
SpringBoot 提供了名为 ApplicationEnvironment
的类表示环境对象,它是 Spring 中 StandardEnvironment
环境对象的子类。
默认情况下,创建的 ApplicationEnvironment
对象中配置信息的来源只有两个:
1 2 3 4 5 6 7 8 package org.springframework.boot;public class Step3 { public static void main (String[] args) { ApplicationEnvironment env = new ApplicationEnvironment (); env.getPropertySources().forEach(System.out::println); } }
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
针对相同名称的配置信息,按照来源的先后顺序获取。
获取 JAVA_HOME
的配置信息:
1 2 3 4 public static void main (String[] args) { ApplicationEnvironment env = new ApplicationEnvironment (); System.out.println(env.getProperty("JAVA_HOME" )); }
D:\environment\JDK1.8
由于 PropertiesPropertySource
中并不存在名为 JAVA_HOME
的配置信息,因此从系统环境变量 SystemEnvironmentPropertySource
中获取 JAVA_HOME
的配置信息。
在 IDEA 的 Run/Debug Configurations 中的 VM options 添加 -DJAVA_HOME=abc
,使得 PropertiesPropertySource
中存在名为 JAVA_HOME
的配置信息:
之后再运行 main() 方法,控制台打印出:
abc
如果想从配置文件 application.properties
中读取配置信息,可以添加配置信息的来源。配置文件的优先级最低,添加来源时调用 addLast()
方法:
1 2 3 4 5 6 7 8 @SneakyThrows public static void main (String[] args) { ApplicationEnvironment env = new ApplicationEnvironment (); env.getPropertySources().addLast(new ResourcePropertySource (new ClassPathResource ("application.properties" ))); env.getPropertySources().forEach(System.out::println); System.out.println(env.getProperty("author.name" )); }
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='class path resource [application.properties]'}
"lxda"
而在 SpringBoot 中,这里只添加 SimpleCommandLinePropertySource
,并且它的优先级最高,使用 addFirst()
方法添加:
1 2 3 4 5 6 7 8 9 @SneakyThrows public static void main (String[] args) { ApplicationEnvironment env = new ApplicationEnvironment (); env.getPropertySources().addLast(new ResourcePropertySource (new ClassPathResource ("application.properties" ))); env.getPropertySources().addFirst(new SimpleCommandLinePropertySource (args)); env.getPropertySources().forEach(System.out::println); System.out.println(env.getProperty("author.name" )); }
运行 main() 方法前,需要添加程序参数 --author.name=lxda:
SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='class path resource [application.properties]'}
lxda
第四步:添加 ConfigurationPropertySources
有一 step4.properties
文件,其内容如下:
1 2 3 user.first-name =George user.middle_name =Walker user.lastName =Bush
尝试读取文件中的内容:
1 2 3 4 5 6 7 8 9 10 11 @SneakyThrows public static void main (String[] args) { ApplicationEnvironment env = new ApplicationEnvironment (); env.getPropertySources().addLast( new ResourcePropertySource ("step4" , new ClassPathResource ("step4.properties" )) ); env.getPropertySources().forEach(System.out::println); System.out.println(env.getProperty("user.first-name" )); System.out.println(env.getProperty("user.middle-name" )); System.out.println(env.getProperty("user.last-name" )); }
step4.properties
文件中配置信息的 key 是 user.middle_name
,但在读取时,使用的是 user.middle-name
;还有 user.lastName
的 key,但读取时使用 user.last-name
。能读取成功吗?
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='step4'}
George
null
null
显然是不行的,为了能读取成功,需要实现 松散绑定,添加 ConfigurationPropertySources
:
1 2 3 4 5 6 @SneakyThrows public static void main (String[] args) { ConfigurationPropertySources.attach(env); }
ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='step4'}
George
Walker
Bush
第五步:使用 EnvironmentPostProcessorApplicationListener
进行环境对象后置处理
在第三步中 只 添加 SimpleCommandLinePropertySource
,读取 properties、YAML 配置文件的源就是在第五步中添加的。
完成这样功能需要使用到 EnvironmentPostProcessor
,其具体实现是 ConfigDataEnvironmentPostProcessor
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static void main (String[] args) { SpringApplication app = new SpringApplication (); ApplicationEnvironment env = new ApplicationEnvironment (); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强前" ); env.getPropertySources().forEach(System.out::println); ConfigDataEnvironmentPostProcessor processor1 = new ConfigDataEnvironmentPostProcessor ( new DeferredLogs (), new DefaultBootstrapContext () ); processor1.postProcessEnvironment(env, app); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强后" ); env.getPropertySources().forEach(System.out::println); System.out.println(env.getProperty("author.name" )); RandomValuePropertySourceEnvironmentPostProcessor processor2 = new RandomValuePropertySourceEnvironmentPostProcessor (new DeferredLog ()); processor2.postProcessEnvironment(env, app); }
>>>>>>>>>>>>>>>>>>>>>>>> 增强前
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
>>>>>>>>>>>>>>>>>>>>>>>> 增强后
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
"lxda"
EnvironmentPostProcessor
还有一个有趣的实现:RandomValuePropertySourceEnvironmentPostProcessor
,该实现提供了随机值的生成。
1 2 3 4 5 6 7 8 9 public static void main (String[] args) { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 再次增强后" ); env.getPropertySources().forEach(System.out::println); System.out.println(env.getProperty("random.string" )); System.out.println(env.getProperty("random.int" )); System.out.println(env.getProperty("random.uuid" )); }
>>>>>>>>>>>>>>>>>>>>>>>> 再次增强后
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
5ef4038a709215938cbd3e1c031f66dd
1481116109
18548e0b-8bad-458b-b38e-bf793aa24ced
在 SpringBoot 中的实现是不会采取上述示例代码的方式来添加后置处理器,同样会从 META-INF/spring.factories
配置文件中读取并初始化后置处理器:
1 2 3 4 5 6 7 8 org.springframework.boot.env.EnvironmentPostProcessor =\ org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\ org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor,\ org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor,\ org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\ org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\ org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor
SpringBoot 中读取 META-INF/spring.factories
配置文件初始化环境后置处理器,再执行处理逻辑的功能由 EnvironmentPostProcessorApplicationListener
完成。它是一个事件监听器,同样是在 META-INF/spring.factories
配置文件中读取并初始化的:
1 2 3 4 5 6 7 8 org.springframework.context.ApplicationListener =\ org.springframework.boot.ClearCachesApplicationListener,\ org.springframework.boot.builder.ParentContextCloserApplicationListener,\ org.springframework.boot.context.FileEncodingApplicationListener,\ org.springframework.boot.context.config.AnsiOutputApplicationListener,\ org.springframework.boot.context.config.DelegatingApplicationListener,\ org.springframework.boot.context.logging.LoggingApplicationListener,\ org.springframework.boot.env.EnvironmentPostProcessorApplicationListener
要想该监听器成功监听到事件,需要在第五步中发布一个事件,而事件的发布由第一步获取的事件发布器完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void main (String[] args) { SpringApplication app = new SpringApplication (); app.addListeners(new EnvironmentPostProcessorApplicationListener ()); ApplicationEnvironment env = new ApplicationEnvironment (); List<String> names = SpringFactoriesLoader.loadFactoryNames(EnvironmentPostProcessor.class, Step5.class.getClassLoader()); names.forEach(System.out::println); EventPublishingRunListener publisher = new EventPublishingRunListener (app, args); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强前" ); env.getPropertySources().forEach(System.out::println); publisher.environmentPrepared(new DefaultBootstrapContext (), env); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强后" ); env.getPropertySources().forEach(System.out::println); }
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor
org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor
org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor
org.springframework.boot.autoconfigure.integration.IntegrationPropertiesEnvironmentPostProcessor
>>>>>>>>>>>>>>>>>>>>>>>> 增强前
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
>>>>>>>>>>>>>>>>>>>>>>>> 增强后
PropertiesPropertySource {name='systemProperties'}
OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
配置文件中 EnvironmentPostProcessor
的实现有很多,但根据上述打印出的信息,生效的并不多,是否生效与项目的依赖配置有关。
第六步:绑定 spring.main
前缀的配置信息到 SpringApplication
对象
使用 @ConfigurationProperties
注解可以指定一个前缀,SpringBoot 将根据指定的前缀和属性名称在配置文件中寻找对应的信息并完成注入,其底层是利用 Binder
实现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @SneakyThrows public static void main (String[] args) { SpringApplication app = new SpringApplication (); ApplicationEnvironment env = new ApplicationEnvironment (); env.getPropertySources().addLast( new ResourcePropertySource ("step4" , new ClassPathResource ("step4.properties" )) ); User user = Binder.get(env).bind("user" , User.class).get(); System.out.println(user); User existUser = new User (); Binder.get(env).bind("user" , Bindable.ofInstance(existUser)); System.out.println(existUser); } @Getter @Setter @ToString static class User { private String firstName; private String middleName; private String lastName; }
Step6.User(firstName=George, middleName=Walker, lastName=Bush)
Step6.User(firstName=George, middleName=Walker, lastName=Bush)
在第六步中,绑定 spring.main
前缀的配置信息到 SpringApplication
对象也是利用了 Binder
。
假设 step6.properties
配置文件的信息如下:
1 2 spring.main.banner-mode =off spring.main.lazy-initialization =true
绑定 spring.main 开头的配置信息到 SpringApplication
对象中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @SneakyThrows public static void main (String[] args) { SpringApplication app = new SpringApplication (); ApplicationEnvironment env = new ApplicationEnvironment (); env.getPropertySources().addLast( new ResourcePropertySource ("step6" , new ClassPathResource ("step6.properties" )) ); Class<? extends SpringApplication > clazz = app.getClass(); Field bannerMode = clazz.getDeclaredField("bannerMode" ); bannerMode.setAccessible(true ); Field lazyInitialization = clazz.getDeclaredField("lazyInitialization" ); lazyInitialization.setAccessible(true ); System.out.println(bannerMode.get(app)); System.out.println(lazyInitialization.get(app)); Binder.get(env).bind("spring.main" , Bindable.ofInstance(app)); System.out.println(bannerMode.get(app)); System.out.println(lazyInitialization.get(app)); }
CONSOLE
false
OFF
true
第七步:打印 Banner
1 2 3 4 5 6 7 8 9 public static void main (String[] args) { ApplicationEnvironment env = new ApplicationEnvironment (); SpringApplicationBannerPrinter printer = new SpringApplicationBannerPrinter ( new DefaultResourceLoader (), new SpringBootBanner () ); printer.print(env, Step7.class, System.out); }
除此之外还可以自定义文字和图片 Banner,文字 Banner 的文件类型需要是 txt,图片 Banner 的文件类型需要是 gif。
文字 Banner:
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) { env.getPropertySources().addLast(new MapPropertySource ( "custom" , Collections.singletonMap("spring.banner.location" , "banner1.txt" ) )); printer.print(env, Step7.class, System.out); }
文字 Banner 可以从 网站 上自定义。
图片 Banner:
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) { env.getPropertySources().addLast(new MapPropertySource ( "custom" , Collections.singletonMap("spring.banner.image.location" , "banner2.gif" ) )); printer.print(env, Step7.class, System.out); }
获取 Spring 或 SpringBoot 的版本号可以使用:
1 2 System.out.println("SpringBoot: " + SpringBootVersion.getVersion()); System.out.println("Spring: " + SpringVersion.getVersion());
第八到十一步:完成 Spring 容器的创建
第八步:创建容器。在构造 SpringApplication
时已经推断出应用的类型,使用应用类型直接创建即可。
第九步:准备容器。回调在构造 SpringApplication
时添加的初始化器。
第十步:加载 Bean 定义。从配置类、XML 配置文件读取 BeanDefinition,或者扫描某一包路径下的 BeanDefinition。
第十一步:调用 ApplicationContext
的 refresh()
方法,完成 Spring 容器的创建。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @SneakyThrows @SuppressWarnings("all") public static void main (String[] args) { SpringApplication app = new SpringApplication (); app.addInitializers(applicationContext -> System.out.println("执行初始化器增强..." )); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 8. 创建容器" ); GenericApplicationContext context = createApplicationContext(WebApplicationType.SERVLET); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 9. 准备容器" ); for (ApplicationContextInitializer initializer : app.getInitializers()) { initializer.initialize(context); } System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 10. 加载 Bean 定义" ); DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory(); AnnotatedBeanDefinitionReader reader1 = new AnnotatedBeanDefinitionReader (beanFactory); XmlBeanDefinitionReader reader2 = new XmlBeanDefinitionReader (beanFactory); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner (beanFactory); reader1.register(Config.class); reader2.loadBeanDefinitions(new ClassPathResource ("b03.xml" )); scanner.scan("com.itheima.a39.sub" ); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 11. refresh 容器" ); context.refresh(); for (String name : context.getBeanDefinitionNames()) { System.out.println("name: " + name + " 来源: " + beanFactory.getBeanDefinition(name).getResourceDescription()); } } private static GenericApplicationContext createApplicationContext (WebApplicationType type) { GenericApplicationContext context = null ; switch (type) { case SERVLET: context = new AnnotationConfigServletWebServerApplicationContext (); break ; case REACTIVE: context = new AnnotationConfigReactiveWebServerApplicationContext (); break ; case NONE: context = new AnnotationConfigApplicationContext (); break ; } return context; }
涉及到的配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static class Bean4 {} static class Bean5 {} @Configuration static class Config { @Bean public Bean5 bean5 () { return new Bean5 (); } @Bean public ServletWebServerFactory servletWebServerFactory () { return new TomcatServletWebServerFactory (); } }
XML 配置文件:
1 2 3 4 5 6 7 8 <?xml version="1.0" encoding="UTF-8" ?> <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 ="bean4" class ="com.itheima.a39.A39_3.Bean4" /> </beans >
com.itheima.a39.sub 包下的 Bean 信息:
1 2 3 4 5 6 7 8 package com.itheima.a39.sub;import org.springframework.stereotype.Component;@Component public class Bean7 {}
运行 main() 方法后,控制台打印出的 Bean 信息:
name: org.springframework.context.annotation.internalConfigurationAnnotationProcessor 来源: null
name: org.springframework.context.annotation.internalAutowiredAnnotationProcessor 来源: null
name: org.springframework.context.annotation.internalCommonAnnotationProcessor 来源: null
name: org.springframework.context.event.internalEventListenerProcessor 来源: null
name: org.springframework.context.event.internalEventListenerFactory 来源: null
name: a39_3.Config 来源: null
name: bean4 来源: class path resource [b03.xml]
name: bean7 来源: file [D:\Code\IdeaCode\advanced-spring\boot\target\classes\com\itheima\a39\sub\Bean7.class]
name: org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory 来源: null
name: bean5 来源: com.itheima.a39.A39_3$Config
name: servletWebServerFactory 来源: com.itheima.a39.A39_3$Config
第十二步:执行 Runner
在 SpringBoot 启动成功后,可以执行一些 Runner
,进行一些预处理或测试。Runner
有两种,分别是 CommandLineRunner
和 ApplicationRunner
:
1 2 3 4 5 6 7 8 9 @FunctionalInterface public interface CommandLineRunner { void run (String... args) throws Exception; } @FunctionalInterface public interface ApplicationRunner { void run (ApplicationArguments args) throws Exception; }
它们都是函数式接口,内部的抽象方法长得也很像,只不过:
CommandLineRunner
直接接收启动参数;
ApplicationRunner
则是接收封装后的 ApplicationArguments
,即 第二步 封装的对象。
在配置类中添加这两种类型的 Bean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Bean public CommandLineRunner commandLineRunner () { return args -> System.out.println("commandLineRunner()..." + Arrays.toString(args)); } @Bean public ApplicationRunner applicationRunner () { return args -> { System.out.println("applicationRunner()..." + Arrays.toString(args.getSourceArgs())); System.out.println(args.getOptionNames()); System.out.println(args.getOptionValues("server.port" )); System.out.println(args.getNonOptionArgs()); }; }
执行 Runner
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @SneakyThrows @SuppressWarnings("all") public static void main (String[] args) { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 2. 封装启动 args" ); DefaultApplicationArguments arguments = new DefaultApplicationArguments (args); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 12. 执行 runner" ); for (CommandLineRunner runner : context.getBeansOfType(CommandLineRunner.class).values()) { runner.run(args); } for (ApplicationRunner runner : context.getBeansOfType(ApplicationRunner.class).values()) { runner.run(arguments); } }
运行 main() 方法时,需要添加程序参数 --server.port=8080 debug
:
>>>>>>>>>>>>>>>>>>>>>>>> 12. 执行 runner
commandLineRunner()...[--server.port=8080, debug]
applicationRunner()...[--server.port=8080, debug]
[server.port]
[8080]
[debug]
步骤总结
得到 SpringApplicationRunListeners
事件发布器
发布 Application Starting 事件 1️⃣
封装启动 args
准备 Environment
添加命令行参数
ConfigurationPropertySources
处理
发布 Application Environment
已准备事件 2️⃣
通过 EnvironmentPostProcessorApplicationListener
进行 env 后处理
绑定 spring.main
到 SpringApplication
对象
打印 Banner
创建容器
准备容器
发布 Application Context
已初始化事件 3️⃣
加载 Bean 定义
发布 Application Prepared
事件 4️⃣
refresh
容器
发布 Application Started
事件 5️⃣
执行 Runner
40. Tomcat 内嵌容器
Tomcat 基本结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Server └───Service ├───Connector (协议, 端口) └───Engine └───Host (虚拟主机 localhost) ├───Context1 (应用 1 , 可以设置虚拟路径, / 即 url 起始路径; 项目磁盘路径, 即 docBase) │ │ index.html │ └───WEB-INF │ │ web.xml (servlet, filter, listener) 3.0 │ ├───classes (servlet, controller, service ...) │ ├───jsp │ └───lib (第三方 jar 包) └───Context2 (应用 2 ) │ index.html └───WEB-INF web.xml
40.1 内嵌 Tomcat 的使用
内嵌 Tomcat 的使用分为 6 步:
创建 Tomcat
创建项目文件夹,即 docBase
文件夹
创建 Tomcat 项目,在 Tomcat 中称为 Context
编程添加 Servlet
启动 Tomcat
创建连接器,设置监听端口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @SneakyThrows public static void main (String[] args) { Tomcat tomcat = new Tomcat (); tomcat.setBaseDir("tomcat" ); File docBase = Files.createTempDirectory("boot." ).toFile(); docBase.deleteOnExit(); Context context = tomcat.addContext("" , docBase.getAbsolutePath()); context.addServletContainerInitializer((set, servletContext) -> { HelloServlet servlet = new HelloServlet (); servletContext.addServlet("hello" , servlet).addMapping("/hello" ); }, Collections.emptySet()); tomcat.start(); Connector connector = new Connector (new Http11Nio2Protocol ()); connector.setPort(8080 ); tomcat.setConnector(connector); }
自行实现 HelloServlet 继承 HttpServlet
,重写 doGet()
方法:
1 2 3 4 5 6 7 8 9 10 public class HelloServlet extends HttpServlet { private static final long serialVersionUID = 8117441197359625079L ; @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8" ); resp.getWriter().print("<h3>hello</h3>" ); } }
运行 main()
方法后,在浏览器访问 localhost:8080/hello
,页面显示 hello
。
40.2 与 Spring 整合
选择不支持内嵌 Tomcat 的 Spring 容器,使其使用前文中的 Tomcat:
1 2 3 4 5 6 7 public static WebApplicationContext getApplicationContext () { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext (); context.register(Config.class); context.refresh(); return context; }
容器中注册了 Config 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 26 27 28 29 30 31 @Configuration static class Config { @Bean public DispatcherServletRegistrationBean registrationBean (DispatcherServlet dispatcherServlet) { return new DispatcherServletRegistrationBean (dispatcherServlet, "/" ); } @Bean public DispatcherServlet dispatcherServlet (WebApplicationContext applicationContext) { return new DispatcherServlet (applicationContext); } @Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter () { RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter (); handlerAdapter.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter ())); return handlerAdapter; } @RestController static class MyController { @GetMapping("hello2") public Map<String,Object> hello () { return Collections.singletonMap("hello2" , "hello2, spring!" ); } } }
Tomcat 在添加 Servlet 时,添加 DispatcherServlet
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @SneakyThrows public static void main (String[] args) { WebApplicationContext springContext = getApplicationContext(); context.addServletContainerInitializer((set, servletContext) -> { HelloServlet servlet = new HelloServlet (); servletContext.addServlet("hello" , servlet).addMapping("/hello" ); DispatcherServlet dispatcherServlet = springContext.getBean(DispatcherServlet.class); servletContext.addServlet("dispatcherServlet" , dispatcherServlet).addMapping("/" ); }, Collections.emptySet()); }
运行 main()
方法,在浏览器中访问 localhost:8080/hello2
,页面上显示:
{"hello2":"hello2, spring!"}
添加 Servlet 时只添加了一个 DispatcherServlet
,但 Spring 容器中可能存在多个 Servlet,这些 Servlet 也应该被添加,因此可以获取 ServletRegistrationBean
类型的 Bean 并执行方法,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @SneakyThrows public static void main (String[] args) { WebApplicationContext springContext = getApplicationContext(); context.addServletContainerInitializer((set, servletContext) -> { HelloServlet servlet = new HelloServlet (); servletContext.addServlet("hello" , servlet).addMapping("/hello" ); for (ServletRegistrationBean registrationBean : springContext.getBeansOfType(ServletRegistrationBean.class).values()) { registrationBean.onStartup(servletContext); } }, Collections.emptySet()); }
运行 main()
方法,在浏览器中访问 localhost:8080/hello2
,页面显示同样的内容。
41. 自动配置
41.1 自动配置类原理
有以下 4 个类
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 26 27 28 29 30 static class AutoConfiguration1 { @Bean public Bean1 bean1 () { return new Bean1 (); } } @ToString @NoArgsConstructor @AllArgsConstructor static class Bean1 { private String name; } static class AutoConfiguration2 { @Bean public Bean2 bean2 () { return new Bean2 (); } } static class Bean2 {}
其中 AutoConfiguration1
和 AutoConfiguration2
用来模拟第三方配置类,注意它们并没有被 @Configuration
注解标记,因此在未进行其他操作时,不会被添加到 Spring 容器中。
然后编写自己的配置类,使用 @Import
注解将第三方配置类添加到 Spring 容器中:
1 2 3 4 @Configuration @Import({AutoConfiguration1.class, AutoConfiguration2.class}) static class Config {}
1 2 3 4 5 6 7 8 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("config" , Config.class); context.registerBean(ConfigurationClassPostProcessor.class); context.refresh(); Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println); }
运行 main() 方法后,控制台打印出:
config
org.springframework.context.annotation.ConfigurationClassPostProcessor
com.itheima.A41$AutoConfiguration1
bean1
com.itheima.a41.A41$AutoConfiguration2
bean2
如果有多个第三方配置类,难不成到一个个地导入?
可以使用导入选择器 ImportSelector
,重写 selectImports()
方法,返回需要自动装配的 Bean 的全限定类名数组:
1 2 3 4 5 6 7 8 9 10 11 12 @Configuration @Import(MyImportSelector.class) static class Config {} static class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String []{AutoConfiguration1.class.getName(), AutoConfiguration2.class.getName()}; } }
但这样的方式相比最初的方式并没有本质区别,如果 selectImports()
方法返回的全限定类名可以从文件中读取,就更方便了。
在当前项目的类路径下创建 META-INF/spring.factories
文件,约定一个 key,对应的 value 即为需要指定装配的 Bean:
1 2 3 4 com.itheima.a41.A41$MyImportSelector =\ com.itheima.a41.A41.AutoConfiguration1, \ com.itheima.a41.A41.AutoConfiguration2
修改 selectImports()
方法实现逻辑:
1 2 3 4 5 6 7 static class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { List<String> names = SpringFactoriesLoader.loadFactoryNames(MyImportSelector.class, null ); return names.toArray(new String [0 ]); } }
运行 main() 方法后,控制台打印出同样的结果。
SpringFactoriesLoader.loadFactoryNames()
不仅会扫描当前项目路径下的 META-INF/spring.factories
文件,还会扫描项目所关联的 Jar 包下的 META-INF/spring.factories
文件。
针对 SpringBoot 来说,自动装配的 Bean 使用如下语句加载:
1 SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, null );
SpringBoot 2.7.0 及其以后版本的自动装配
在 SpringBoot 2.7.0 及其以后的版本中,SpringBoot 不再通过读取 META-INF/spring.factories
文件中 key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration
的 values 来实现自动装配。
为了更贴合 SPI 机制,SpringBoot 将读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件中的内容,该文件中每一行都表示需要自动装配的 Bean 的全限定类名,可以使用 # 作为注释。其加载方式使用:
1 ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader());
其中 AutoConfiguration
是一个注解,它的全限定类名为 org.springframework.boot.autoconfigure.AutoConfiguration
。
也就是说可以自定义一个注解,创建 META-INF/spring/full-qualified-annotation-name.imports
文件,在文件里声明需要自动装配的类:
1 2 3 4 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MyAutoConfiguration {}
1 2 3 4 public class A41 { static class Bean3 { } }
创建 META-INF/spring/com.itheima.a41.MyAutoConfiguration.imports
文件:
1 com.itheima .a41 .A41$Bean3
修改 selectImports()
方法实现逻辑:
1 2 3 4 5 6 7 8 9 static class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { List<String> names = new ArrayList <>(SpringFactoriesLoader.loadFactoryNames(MyImportSelector.class, null )); ImportCandidates.load(MyAutoConfiguration.class, null ).forEach(names::add); return names.toArray(new String [0 ]); } }
运行 main() 方法后,Spring 容器中的 Bean 多了 一个:
com.itheima.a41.A41$Bean3
定义了冲突的 Bean
第三方装配了 Bean1
:
1 2 3 4 5 6 static class AutoConfiguration1 { @Bean public Bean1 bean1 () { return new Bean1 ("第三方" ); } }
用户又自行定义了 Bean1
:
1 2 3 4 5 6 7 8 @Configuration @Import(MyImportSelector.class) static class Config { @Bean public Bean1 bean1 () { return new Bean1 ("本项目" ); } }
修改测试的 main() 方法:
1 2 3 4 5 6 public static void main (String[] args) { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>" ); System.out.println(context.getBean(Bean1.class)); }
>>>>>>>>>>>>>>>>>>>>>>>>>>>
A41.Bean1(name=本项目)
用户自行定义的 Bean 生效了,这是因为:@Import
导入的 Bean 先于配置类中 @Bean
定义的 Bean 执行,后者覆盖前者,使得用户自定义的 Bean 生效。
但在 SpringBoot 中不是这样的,当后续添加的 Bean 想覆盖先前添加的 Bean,会出现错误。模拟 SpringBoot 的设置:
1 2 3 4 5 6 7 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.getDefaultListableBeanFactory().setAllowBeanDefinitionOverriding(false ); }
Exception in thread "main" org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'bean1' defined in com.itheima.a41.A41$Config: Cannot register bean definition [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=config; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in com.itheima.a41.A41$Config] for bean 'bean1': There is already [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=com.itheima.a41.A41$AutoConfiguration1; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [com/itheima/a41/A41$AutoConfiguration1.class]] bound.
那这样是合理的吗?
显然不是。比如 SpringBoot 默认的数据连接池是 Hikari,如果用户想换成 Druid,岂不是做不到?
实际情况下是能做到的,这又是怎么做到的呢?
首先需要使用户的配置类中定义的 Bean 先于 @Import
导入的 Bean 添加到 Spring 容器中,只需将选择器 MyImportSelector
实现的 ImportSelector
接口更换成其子接口 DeferredImportSelector
即可:
1 2 3 static class MyImportSelector implements DeferredImportSelector { }
再次运行 main() 方法:
Exception in thread "main" org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'bean1' defined in class path resource [com/itheima/a41/A41$AutoConfiguration1.class]: Cannot register bean definition [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=com.itheima.a41.A41$AutoConfiguration1; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [com/itheima/a41/A41$AutoConfiguration1.class]] for bean 'bean1': There is already [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=config; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in com.itheima.a41.A41$Config] bound.
尽管还是出现了异常,但异常信息中显示的是在配置类定义的 Bean 已存在,第三方装配的 Bean 无法再添加,这表明 Bean 的添加顺序修改成功。
最后在第三方定义的 Bean 上添加 @ConditionalOnMissingBean
注解,表示容器中存在同名的 Bean 时忽略该 Bean 的添加:
1 2 3 4 5 6 7 static class AutoConfiguration1 { @Bean @ConditionalOnMissingBean public Bean1 bean1 () { return new Bean1 ("第三方" ); } }
再次运行 main() 方法,不再出现异常:
>>>>>>>>>>>>>>>>>>>>>>>>>>>
A41.Bean1(name=本项目)
41.2 Aop 自动配置
确保当前模块下已导入:
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency >
使用 AopAutoConfiguration
自动装配与 AOP 相关的 Bean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class TestAopAuto { public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); AnnotationConfigUtils.registerAnnotationConfigProcessors(context.getDefaultListableBeanFactory()); context.registerBean(Config.class); context.refresh(); Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println); } @Configuration @Import(MyImportSelector.class) static class Config { } static class MyImportSelector implements DeferredImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String []{AopAutoConfiguration.class.getName()}; } } }
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
com.itheima.a41.TestAopAuto$Config
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration$CglibAutoProxyConfiguration
org.springframework.aop.config.internalAutoProxyCreator
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
以 com.itheima.a41.TestAopAuto$Config
为分割线,上方是添加的一些后置处理器,下方就是 AOP 自动装配添加的 Bean。
在配置类 AopAutoConfiguration
中,使用注解判断配置类是否生效。首先是最外层的 AopAutoConfiguration
:
1 2 3 4 5 @AutoConfiguration @ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true) public class AopAutoConfiguration { }
根据 @ConditionalOnProperty
注解配置的信息:如果配置文件中存在前缀 为 spring.aop
,名称 为 auto
的 key,并且其对应的 value 是 true
时,配置类 AopAutoConfiguration
生效;如果配置文件中未显式配置,该配置类也生效。
不使用配置文件,使用 StandardEnvironment
指定 spring.aop.auto
的值为 false
:
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); StandardEnvironment env = new StandardEnvironment (); env.getPropertySources().addLast( new SimpleCommandLinePropertySource ("--spring.aop.auto=false" ) ); context.setEnvironment(env); }
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
com.itheima.a41.TestAopAuto$Config
如果 spring.aop.auto
的值是 true
,又会成功添加上 AOP 自动装配的 Bean。
再看 AopAutoConfiguration
的内部类:
1 2 3 4 5 6 7 8 9 10 11 12 @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Advice.class) static class AspectJAutoProxyingConfiguration { } @Configuration(proxyBeanMethods = false) @ConditionalOnMissingClass("org.aspectj.weaver.Advice") @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true) static class ClassProxyingConfiguration { }
其内部存在两个类:AspectJAutoProxyingConfiguration
和 ClassProxyingConfiguration
。
使用了 @ConditionalOnClass
注解判断 Advice.class
存在时,AspectJAutoProxyingConfiguration
生效;使用 @ConditionalOnMissingClass
注解判断 org.aspectj.weaver.Advice
不存在时,ClassProxyingConfiguration
生效。
由于先前导入了 spring-boot-starter-aop
依赖,Advice.class
是存在的,AspectJAutoProxyingConfiguration
将生效。
AspectJAutoProxyingConfiguration
内部又有两个配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration(proxyBeanMethods = false) @EnableAspectJAutoProxy(proxyTargetClass = false) @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false") static class JdkDynamicAutoProxyConfiguration {} @Configuration(proxyBeanMethods = false) @EnableAspectJAutoProxy(proxyTargetClass = true) @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true) static class CglibAutoProxyConfiguration {}
这两个配置类通过使用 @ConditionalOnProperty
注解判断配置文件中是否存在 spring.aop.proxy-target-class
配置来让对应的配置类生效。
由于并未显式配置,因此 CglibAutoProxyConfiguration
将生效。
无论哪个配置类生效,它们都被 @EnableAspectJAutoProxy
标记,这个注解相当于是添加了些配置的 @Import
注解:
1 2 3 4 5 6 7 8 9 @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Import({AspectJAutoProxyRegistrar.class}) public @interface EnableAspectJAutoProxy { boolean proxyTargetClass () default false ; boolean exposeProxy () default false ; }
向 Spring 容器中添加 AspectJAutoProxyRegistrar
类型的 Bean。
AspectJAutoProxyRegistrar
实现了 ImportBeanDefinitionRegistrar
接口,可以使用编程的方式来注册一些 Bean:
1 2 3 4 5 6 class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar { public void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry); } }
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary()
方法是注册 Bean 的主要逻辑:
1 2 3 4 5 6 7 8 9 @Nullable public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary (BeanDefinitionRegistry registry) { return registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry, (Object)null ); } @Nullable public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary (BeanDefinitionRegistry registry, @Nullable Object source) { return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source); }
最终注册了 AnnotationAwareAspectJAutoProxyCreator
。
使用 org.springframework.aop.config.internalAutoProxyCreator
作为名称,获取 AnnotationAwareAspectJAutoProxyCreator
类型的 Bean,并查看其 proxyTargetClass
属性是否为 true:
1 2 3 4 5 6 7 8 public static void main (String[] args) { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>" ); AnnotationAwareAspectJAutoProxyCreator creator = context.getBean("org.springframework.aop.config.internalAutoProxyCreator" , AnnotationAwareAspectJAutoProxyCreator.class); System.out.println(creator.isProxyTargetClass()); }
【补充】ImportBeanDefinitionRegistrar
接口
将 Bean 注入到 Spring 的大致流程是:
利用 BeanDefinitionReader
读取配置文件或注解信息,为每一个 Bean 生成一个 BeanDefinition
将 BeanDefinition
注册到 BeanDefinitionRegistry
中
当需要创建 Bean 对象时,从 BeanDefinitionRegistry
中取出对应的 BeanDefinition
,利用这个 BeanDefinition
来创建 Bean
如果创建的 Bean 是单例的,Spring 会将这个 Bean 保存到 SingletonBeanRegistry
中,即三级缓存中的第一级缓存,需要时直接从这里获取,而不是重复创建
也就是说 Spring 是通过 BeanDefinition
去创建 Bean 的,而 BeanDefinition
会被注册到 BeanDefinitionRegistry
中,因此可以拿到 BeanDefinitionRegistry
直接向里面注册 BeanDefinition
达到将 Bean 注入到 Spring 的目标。
ImportBeanDefinitionRegistrar
接口就可以直接拿到 BeanDefinitionRegistry
:
1 2 3 4 5 6 7 8 public interface ImportBeanDefinitionRegistrar { default void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { this .registerBeanDefinitions(importingClassMetadata, registry); } default void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { } }
该接口需要搭配 @Import
注解使用。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean(ConfigurationClassPostProcessor.class); context.registerBean("config" , Config.class); context.refresh(); Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println); System.out.println(context.getBean(User.class)); } @Configuration @Import({MyImportBeanDefinitionRegistrar.class}) static class Config {} static class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(User.class) .addPropertyValue("name" , "lxd" ) .addPropertyValue("age" , 20 ) .getBeanDefinition(); registry.registerBeanDefinition("user" , beanDefinition); } } @Setter @ToString static class User { private String name; private int age; }
org.springframework.context.annotation.ConfigurationClassPostProcessor
config
user
TestImportBeanDefinitionRegistrar.User(name=lxd, age=20)
注意: 使用时一定要确保 Spring 容器中存在 ConfigurationClassPostProcessor
类型的 Bean。
除此之外,使用 BeanDefinitionRegistryPostProcessor
接口也能拿到 BeanDefinitionRegistry
:
1 2 3 public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor { void postProcessBeanDefinitionRegistry (BeanDefinitionRegistry var1) throws BeansException; }
41.3 数据库相关的自动配置
确保当前模块下已导入:
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 2.3.0</version > </dependency > <dependency > <groupId > com.mysql</groupId > <artifactId > mysql-connector-j</artifactId > </dependency >
DataSource 自动配置
自行实现导入选择器,并使用 @Import
注解进行导入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration @Import(MyImportSelector.class) static class Config {} static class MyImportSelector implements DeferredImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String []{ DataSourceAutoConfiguration.class.getName(), MybatisAutoConfiguration.class.getName(), DataSourceTransactionManagerAutoConfiguration.class.getName(), TransactionAutoConfiguration.class.getName() }; } }
在 main()方法中打印导入的 Bean 信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); StandardEnvironment env = new StandardEnvironment (); env.getPropertySources().addLast(new SimpleCommandLinePropertySource ( "--spring.datasource.url=jdbc:mysql://localhost:3306/advanced_spring" , "--spring.datasource.username=root" , "--spring.datasource.password=123456" )); context.setEnvironment(env); AnnotationConfigUtils.registerAnnotationConfigProcessors(context.getDefaultListableBeanFactory()); context.registerBean(Config.class); context.refresh(); for (String name : context.getBeanDefinitionNames()) { String resourceDescription = context.getBeanDefinition(name).getResourceDescription(); if (resourceDescription != null ) System.out.println(name + " 来源: " + resourceDescription); } }
未使用配置文件,而是使用 StandardEnvironment
设置了一些数据库连接信息。
最后只打印有明确来源的 Bean 信息,其中有一条:
dataSource 来源: class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]
名叫 dataSource
的 Bean 的来源为什么是 DataSourceConfiguration
,而不是 DataSourceAutoConfiguration
呢?
查看 DataSourceAutoConfiguration
的源码,实现与 AopAutoConfiguration
类似,都是通过注解来判断需要导入哪些 Bean,有两个关键的内部类 EmbeddedDatabaseConfiguration
和 PooledDataSourceConfiguration
:
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 @AutoConfiguration(before = SqlInitializationAutoConfiguration.class) @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) @ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory") @EnableConfigurationProperties(DataSourceProperties.class) @Import(DataSourcePoolMetadataProvidersConfiguration.class) public class DataSourceAutoConfiguration { @Configuration(proxyBeanMethods = false) @Conditional(EmbeddedDatabaseCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import(EmbeddedDataSourceConfiguration.class) protected static class EmbeddedDatabaseConfiguration { } @Configuration(proxyBeanMethods = false) @Conditional(PooledDataSourceCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class }) protected static class PooledDataSourceConfiguration { } }
它们都被 @Conditional
注解标记。当项目支持内嵌数据源时,EmbeddedDatabaseConfiguration
生效;当项目支持基于数据库连接池的数据源时,PooledDataSourceConfiguration
生效。
SpringBoot 默认的数据库连接池是 Hikari
,因此 PooledDataSourceConfiguration
生效,最终使用 @Import
导入一系列 Bean,导入的这些 Bean 都是 DataSourceConfiguration
的内部类,因此dataSource
的 Bean 的来源是 DataSourceConfiguration
。
在 DataSourceConfiguration
中,通过 @ConditionalOnClass
注解判断某些 Class 是否存在来使某种数据库连接池生效。
由于导入了 mybatis-spring-boot-starter
,其内部依赖 mybatis-spring-boot-jdbc
,而它又依赖了 HikariCP
,因此最终数据库连接池 Hikari
生效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration(proxyBeanMethods = false) @ConditionalOnClass(HikariDataSource.class) @ConditionalOnMissingBean(DataSource.class) @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true) static class Hikari { @Bean @ConfigurationProperties(prefix = "spring.datasource.hikari") HikariDataSource dataSource (DataSourceProperties properties) { HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class); if (StringUtils.hasText(properties.getName())) { dataSource.setPoolName(properties.getName()); } return dataSource; } }
在 Hikari#dataSource()
方法中,接受一个 DataSourceProperties
类型的参数,这要求 Spring 容器中存在 DataSourceProperties
类型的 Bean。
在最初的 DataSourceAutoConfiguration
自动配置类上有个 @EnableConfigurationProperties
注解,它将 DataSourceProperties
添加到容器中:
1 2 3 4 @EnableConfigurationProperties(DataSourceProperties.class) public class DataSourceAutoConfiguration { }
在 DataSourceProperties
中会绑定配置文件中以 spring.datasource
为前缀的配置:
1 2 3 4 @ConfigurationProperties(prefix = "spring.datasource") public class DataSourceProperties implements BeanClassLoaderAware , InitializingBean { }
获取 DataSourceProperties
类型的 Bean,并打印其 url
、username
和 password
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); StandardEnvironment env = new StandardEnvironment (); env.getPropertySources().addLast(new SimpleCommandLinePropertySource ( "--spring.datasource.url=jdbc:mysql://localhost:3306/advanced_spring" , "--spring.datasource.username=root" , "--spring.datasource.password=123456" )); context.setEnvironment(env); DataSourceProperties properties = context.getBean(DataSourceProperties.class); System.out.println(properties.getUrl()); System.out.println(properties.getUsername()); System.out.println(properties.getPassword()); }
jdbc:mysql://localhost:3306/advanced_spring
root
123456
MyBatis 自动配置
接下来看看 MyBatis 的自动配置类:
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 26 27 28 29 30 31 @Configuration @ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class}) @ConditionalOnSingleCandidate(DataSource.class) @EnableConfigurationProperties({MybatisProperties.class}) @AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class}) public class MybatisAutoConfiguration implements InitializingBean { @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory (DataSource dataSource) throws Exception { } @Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate (SqlSessionFactory sqlSessionFactory) { } @Configuration @Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class}) @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class}) public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean { } }
MybatisAutoConfiguration
生效的条件有两个:
类路径下存在 SqlSessionFactory
和 SqlSessionFactoryBean
Spring 容器中有且仅有一个 DataSource
类型的 Bean
它还添加了 MybatisProperties
类型的 Bean 到 Spring 容器中,并与配置文件中以 mybatis
为前缀的信息绑定。
@AutoConfigureAfter
注解指定了当前自动配置类在 DataSourceAutoConfiguration
和 MybatisLanguageDriverAutoConfiguration
两个自动配置类解析完成之后再解析。
接下来遇到 sqlSessionFactory()
方法:
1 2 3 4 5 @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory (DataSource dataSource) throws Exception { }
依赖 Spring 容器中的 DataSource
,当容器中不存在 SqlSessionFactory
时,将其添加到 Spring 容器中。
然后是 sqlSessionTemplate()
方法,它与添加 SqlSessionFactory
到 Spring 容器的逻辑一样:
1 2 3 4 5 @Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate (SqlSessionFactory sqlSessionFactory) { }
SqlSessionTemplate
也是 SqlSession
的实现,提供了与当前线程绑定的 SqlSession
。针对多个方法调用,如果它们来自同一个线程,那么获取到的 SqlSession
对象是同一个。这也是为什么有了 DefaultSqlSession
作为 SqlSession
的实现了,还需要 SqlSessionTemplate
。
在 MyBatis 中,使用 MapperFactoryBean
将接口转换为对象,其核心是 getObject()
方法:
1 2 3 public T getObject () throws Exception { return this .getSqlSession().getMapper(this .mapperInterface); }
方法中获取了 sqlSession
对象,而获取的就是 SqlSessionTemplate
对象:
1 2 3 public SqlSession getSqlSession () { return this .sqlSessionTemplate; }
最后来到 MapperScannerRegistrarNotFoundConfiguration
内部类:
1 2 3 4 5 6 @Configuration @Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class}) @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class}) public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean { }
利用 @ConditionalOnMissingBean
判断 Spring 容器中缺失 MapperFactoryBean
和 MapperScannerConfigurer
时,该配置类生效。生效时利用 @Import
导入 AutoConfiguredMapperScannerRegistrar
:
1 2 3 public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware , EnvironmentAware, ImportBeanDefinitionRegistrar { }
AutoConfiguredMapperScannerRegistrar
实现了 ImportBeanDefinitionRegistrar
接口,允许通过编程的方式将 Bean 添加到 Spring 容器中,而这里是去扫描 Mapper 接口,将其转换为对象添加到 Spring 容器中。
在 main()所在类的包路径下创建 mapper
包,并新建三个接口,其中两个被 @Mapper
注解标记:
1 2 3 4 5 6 7 8 9 10 @Mapper public interface Mapper1 {} @Mapper public interface Mapper2 {} public interface Mapper3 {}
运行 main()方法,查看 Mapper1
和 Mapper2
是否被添加到 Spring 容器中。
结果是否定的。因为 没有设置要扫描的包路径 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void main (String[] args) { String packageName = TestDataSourceAuto.class.getPackage().getName(); System.out.println("当前包名: " + packageName); AutoConfigurationPackages.register(context.getDefaultListableBeanFactory(), packageName); context.refresh(); for (String name : context.getBeanDefinitionNames()) { String resourceDescription = context.getBeanDefinition(name).getResourceDescription(); if (resourceDescription != null ) System.out.println(name + " 来源: " + resourceDescription); } }
当前包名: com.itheima.a41
mapper1 来源: file [D:\Code\IdeaCode\advanced-spring\boot\target\classes\com\itheima\a41\mapper\Mapper1.class]
mapper2 来源: file [D:\Code\IdeaCode\advanced-spring\boot\target\classes\com\itheima\a41\mapper\Mapper2.class]
@MapperScan
注解与 MybatisAutoConfiguration
在功能上很类似,只不过:
@MapperScan
可以指定具体的扫描路径,未指定时会把引导类范围内的所有接口当做 Mapper
接口;
MybatisAutoConfiguration
关注所有被 @Mapper
注解标记的接口,忽略未被 @Mapper
标记的接口。
事务自动配置
事务自动配置与 DataSourceTransactionManagerAutoConfiguration
、TransactionAutoConfiguration
有关。
DataSourceTransactionManagerAutoConfiguration
配置了 DataSourceTransactionManager
用来执行事务的提交、回滚操作。
TransactionAutoConfiguration
在功能上对标 @EnableTransactionManagement
,包含以下三个 Bean:
BeanFactoryTransactionAttributeSourceAdvisor
:事务切面类,包含通知和切点
TransactionInterceptor
:事务通知类,由它在目标方法调用前后加入事务操作
AnnotationTransactionAttributeSource
:解析 @Transactional
及事务属性,还包含了切点功能
如果自定义了 DataSourceTransactionManager
或是在引导类加了 @EnableTransactionManagement
,则以自定义为准。
41.4 MVC 自动配置
MVC 的自动配置需要用到四个类:
配置内嵌 Tomcat 服务器工厂:ServletWebServerFactoryAutoConfiguration
配置 DispatcherServlet:DispatcherServletAutoConfiguration
配置 WebMVC 各种组件:WebMvcAutoConfiguration
配置 MVC 的错误处理:ErrorMvcAutoConfiguration
查看自动配置与 MVC 相关的 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 26 27 28 29 30 31 32 33 34 35 36 public class TestMvcAuto { public static void main (String[] args) { AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext (); context.registerBean(Config.class); context.refresh(); for (String name : context.getBeanDefinitionNames()) { String source = context.getBeanDefinition(name).getResourceDescription(); if (source != null ) { System.out.println(name + " 来源:" + source); } } context.close(); } @Configuration @Import(MyImportSelector.class) static class Config { } static class MyImportSelector implements DeferredImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String []{ ServletWebServerFactoryAutoConfiguration.class.getName(), DispatcherServletAutoConfiguration.class.getName(), WebMvcAutoConfiguration.class.getName(), ErrorMvcAutoConfiguration.class.getName() }; } } }
41.5 自定义自动配置类
在 SpringBoot 自动装配时添加自定义组件分为两步:
在类路径下自定义 META-INF/spring.factories
文件,以 org.springframework.boot.autoconfigure.EnableAutoConfiguration
为 key,设置需要自动装配的自定义组件的全限定类名为 value
编写配置类,在配置类上使用 @EnableAutoConfiguration
注解,并将其添加到 Spring 容器中
在实际项目开发中,省略第二步,SpringBoot 的会自动扫描。
SpringBoot 2.7.0 及其以后版本
在类路径下自定义 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件,文件中 每一行 表示需要进行自动装配的类的全限定类名,因此不能随意换行。
在这个文件中,以 #
开头的行表示注释。
42. 条件装配底层
42.1 @Conditional
在 SpringBoot 的自动配置中,经常看到 @Conditional
注解的使用,使用该注解可以按条件加载配置类。
@Conditional
注解并不具备条件判断功能,而是通过指定的 Class
列表来进行判断,指定的 Class
需要实现 Condition
接口。
假设有这样一个需求:通过判断类路径下是否存在 com.alibaba.druid.pool.DruidDataSource
类来加载不同的配置类,当存在 DruidDataSource
时,加载 AutoConfiguration1,反之加载 AutoConfiguration2。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 public static void main (String[] args) throws IOException { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("config" , Config.class); context.registerBean(ConfigurationClassPostProcessor.class); context.refresh(); for (String name : context.getBeanDefinitionNames()) { System.out.println(name); } } @Configuration @Import(MyImportSelector.class) static class Config {} static class MyImportSelector implements DeferredImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String []{AutoConfiguration1.class.getName(), AutoConfiguration2.class.getName()}; } } static class MyCondition1 implements Condition { @Override public boolean matches (ConditionContext context, AnnotatedTypeMetadata metadata) { return ClassUtils.isPresent("com.alibaba.druid.pool.DruidDataSource" , null ); } } static class MyCondition2 implements Condition { @Override public boolean matches (ConditionContext context, AnnotatedTypeMetadata metadata) { return !ClassUtils.isPresent("com.alibaba.druid.pool.DruidDataSource" , null ); } } @Configuration @Conditional(MyCondition1.class) static class AutoConfiguration1 { @Bean public Bean1 bean1 () { return new Bean1 (); } } @Configuration @Conditional(MyCondition2.class) static class AutoConfiguration2 { @Bean public Bean2 bean2 () { return new Bean2 (); } } static class Bean1 {} static class Bean2 {}
此时并未导入 druid
依赖,AutoConfiguration2
应该生效,运行 main() 方法后,控制台打印出:
config
org.springframework.context.annotation.ConfigurationClassPostProcessor
com.itheima.a42.A42$AutoConfiguration2
bean2
导入 druid 依赖
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.2.15</version > </dependency >
再次运行 main()方法:
config
org.springframework.context.annotation.ConfigurationClassPostProcessor
com.itheima.a42.A42$AutoConfiguration1
bean1
42.2 @ConditionalOnXxx
在 SpringBoot 的自动配置中,经常看到 @ConditionalOnXxx
注解的使用,这种注解是将某个 @Conditional
的判断进行了封装,比如 ConditionalOnClass
就是用于判断某个 Class
是否存在。
因此针对上文中的代码可以做出修改:
自定义 @ConditionalOnClass
注解,填入需要判断的全限定类名和判断条件;
移除模拟的第三方配置上的 @Conditional
注解,而是使用自定义的 @ConditionalOnClass
;
Condition
接口的使用类重写的 matches()
方法利用 @ConditionalOnClass
注解进行条件判断。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 public static void main (String[] args) throws IOException { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("config" , Config.class); context.registerBean(ConfigurationClassPostProcessor.class); context.refresh(); for (String name : context.getBeanDefinitionNames()) { System.out.println(name); } } @Configuration @Import(MyImportSelector.class) static class Config {} static class MyImportSelector implements DeferredImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String []{AutoConfiguration1.class.getName(), AutoConfiguration2.class.getName()}; } } static class MyCondition implements Condition { @Override public boolean matches (ConditionContext context, AnnotatedTypeMetadata metadata) { Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnClass.class.getName()); Optional<Map<String, Object>> optional = Optional.ofNullable(attributes); String className = optional.map(i -> String.valueOf(i.get("className" ))).orElse("" ); boolean exists = optional.map(i -> i.get("exists" )) .map(String::valueOf) .map(Boolean::parseBoolean).orElse(false ); boolean present = ClassUtils.isPresent(className, null ); return exists == present; } } @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) @Conditional(MyCondition.class) private @interface ConditionalOnClass { boolean exists () ; String className () ; } @Configuration @ConditionalOnClass(className = "com.alibaba.druid.pool.DruidDataSource", exists = true) static class AutoConfiguration1 { @Bean public Bean1 bean1 () { return new Bean1 (); } } @Configuration @ConditionalOnClass(className = "com.alibaba.druid.pool.DruidDataSource", exists = false) static class AutoConfiguration2 { @Bean public Bean2 bean2 () { return new Bean2 (); } } static class Bean1 {} static class Bean2 {}
在导入 druid
依赖或未导入 druid
依赖的情况下运行 main() 方法,控制台打印结果与【42.1 @Conditional】一样。
43. FactoryBean
FactoryBean
是一个接口,可以实现该接口,并指定一个泛型,在重写的方法指定泛型类型对象的创建,然后将实现类交由 Spring 管理,最后 Spring 容器中会增加泛型类型的 Bean。这个 Bean 并不是完全受 Spring 管理,或者说部分受 Spring 管理。
为什么这么说呢?
首先定义一个 Bean2
,交由 Spring 管理,但它不是重点:
1 2 3 @Component public class Bean2 {}
然后定义 Bean1
,它未交由 Spring 管理,但是在其内部注入了 Bean2
、定义初始化方法、实现 Aware
接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Slf4j public class Bean1 implements BeanFactoryAware { private Bean2 bean2; @Autowired public void setBean2 (Bean2 bean2) { this .bean2 = bean2; } public Bean2 getBean2 () { return this .bean2; } @PostConstruct public void init () { log.debug("init" ); } @Override public void setBeanFactory (BeanFactory beanFactory) throws BeansException { log.debug("setBeanFactory({})" , beanFactory); } }
定义 Bean1FactoryBean
,实现 FactoryBean
接口,指定泛型为 Bean1
,将其交由 Spring 管理,Bean 的名称是 bean1
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Slf4j @Component("bean1") public class Bean1FactoryBean implements FactoryBean <Bean1> { @Override public Bean1 getObject () throws Exception { Bean1 bean1 = new Bean1 (); log.debug("create bean: {}" , bean1); return bean1; } @Override public Class<?> getObjectType() { return Bean1.class; } @Override public boolean isSingleton () { return true ; } }
使用这种方式添加到 Spring 容器中的 Bean 的名称是 bean1
,但 Bean 的类型不是 Bean1FactoryBean
,或者 FactoryBean
,而是 Bean1
。
1 2 3 4 5 6 7 8 9 10 @ComponentScan public class A43 { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (A43.class); Bean1 bean1 = context.getBean("bean1" , Bean1.class); System.out.println(bean1); context.close(); } }
运行 main()方法后,控制台打印出:
com.itheima.a43.Bean1FactoryBean - create bean: com.itheima.a43.Bean1@2667f029
com.itheima.a43.Bean1@2667f029
Bean1
类型的 Bean 被成功添加到 Spring 容器中,但根据打印的日志信息可以看出这个 Bean 没有经历依赖注入阶段、没有回调 Aware 接口、没有经历初始化阶段,其创建是由重写的 getObject()
方法完成的。
这个 Bean 就真的没有经历 Spring Bean 的生命周期中的任何阶段吗?
定义 Bean1PostProcessor
,实现 BeanPostProcessor
接口,在 bean1
初始化前后打印日志信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Slf4j @Component public class Bean1PostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization (Object bean, String beanName) throws BeansException { if ("bean1" .equals(beanName) && bean instanceof Bean1) { log.debug("before [{}] init" , beanName); } return bean; } @Override public Object postProcessAfterInitialization (Object bean, String beanName) throws BeansException { if ("bean1" .equals(beanName) && bean instanceof Bean1) { log.debug("after [{}] init" , beanName); } return bean; } }
运行 main()方法后,控制台打印出:
com.itheima.a43.Bean1FactoryBean - create bean: com.itheima.a43.Bean1@6a28ffa4
com.itheima.a43.Bean1PostProcessor - after [bean1] init
com.itheima.a43.Bean1@6a28ffa4
bean1
进行了初始化后的增强逻辑,但未进行初始化前的增强逻辑。
创建代理对象的时机就是在初始化后,因此由 FactoryBean
创建的 Bean 可以进行代理增强 。
FactoryBean
接口
FactoryBean
接口中有三个可以被重写的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 public interface FactoryBean <T> { String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType" ; @Nullable T getObject () throws Exception; @Nullable Class<?> getObjectType(); default boolean isSingleton () { return true ; } }
其中:
getObject()
用于构造 Bean 对象
getObjectType()
用于返回 Bean 对象的类型,以便可以通过类型从容器中获取 Bean
isSingleton()
每次获取的 Bean 对象是否是单例的
从容器中获取 Bean 时可以通过名称获取、可以通过类型获取、也可以通过名称和类型一起获取。如果重写的 getObjectType()
方法返回了 null,那么通过类型从容器中获取 Bean 时,将抛出 NoSuchBeanDefinitionException
异常,并提示没有指定类型的 Bean。
如果重写的 isSingleton()
方法返回 true,那么每次充容器中获取 Bean 对象都是同一个,反之则不是。
注意: 由 FactoryBean
构造的单例 Bean 不会存放在 DefaultSingletonBeanRegistry
的 singletonFactories
中,而是在 AbstractAutowireCapableBeanFactory
的 factoryBeanInstanceCache
中。
获取FactoryBean
类型的 Bean
肯定不能简单地通过名称获取,那会返回其泛型参数类型的 Bean,那通过类型获取呢?比如:
1 context.getBean(Bean1FactoryBean.class)
答案是可行的。
除此之外,还可以在名称前添加 &
,然后通过名称来获取:
1 context.getBean("&bean1" )
44. @Indexed
Spring 在进行组件扫描时,会遍历项目中依赖的所有 Jar 包中类路径下所有的文件,找到被 @Component
及其衍生注解标记的类,然后把它们组装成 BeanDefinition
添加到 Spring 容器中。
如果扫描的返回过大,势必会大大地影响项目启动速度。
为了优化扫描速度,引入以下依赖,Spring 将扫描过程提前到编译期:
1 2 3 4 5 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context-indexer</artifactId > <optional > true</optional > </dependency >
现有如下类信息:
1 2 3 4 5 6 7 8 9 10 11 @Component public class Bean1 {} @Component public class Bean2 {} @Component public class Bean3 {}
这几个类都与 A44 存放于同一包路径下:
1 2 3 4 5 6 7 8 9 10 public class A44 { public static void main (String[] args) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory (); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner (beanFactory); scanner.scan(A44.class.getPackage().getName()); Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println); } }
运行 main()方法后,控制台打印出:
bean2
bean3
bean1
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
bean1
、bean2
和 bean3
都被添加到 Spring 容器中。
在编译生成的 target
目录下的 classes/META-INF/spring.components
文件里有以下信息:
1 2 3 com.itheima .a44 .Bean1=org.springframework .stereotype .Component com.itheima .a44 .Bean2=org.springframework .stereotype .Component com.itheima .a44 .Bean3=org.springframework .stereotype .Component
删除最后两条信息,再次运行 main() 方法
bean1
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
此时只有 bean1
被添加到 Spring 容器中,也就是说会先以 spring.components
文件中的信息为主。
那 spring.components
是怎么实现的?
它是在引入 spring-context-indexer
依赖后,在编译期根据类是否被 @Indexed
注解标记,生成 spring.components
文件及内容。
到目前为止,虽然都没显式使用 @Indexed
注解,但它包含在 @Component
注解中:
1 2 3 4 5 6 7 @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Indexed public @interface Component { String value () default "" ; }
总结
导入 spring-context-indexer
依赖后,在编译期根据 @Indexed
生成 META-INF/spring.components
文件。
Spring 在扫描组件时,如果发现 META-INF/spring.components
文件存在,以它为准加载 BeanDefinition
,反之遍历包含 Jar 包类路径下所有 class 信息。
45. Spring 代理的特点
在 Spring 的代理中,依赖注入和初始化针对的是目标对象,代理对象和目标对象是两个对象,两者的成员变量不会共享。
确保项目中已导入以下依赖:
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency >
依赖注入和初始化针对的是目标对象
现有如下类信息:
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 26 27 28 29 30 @Slf4j @Component public class Bean1 { protected Bean2 bean2; protected boolean initialized; @Autowired public void setBean2 (Bean2 bean2) { log.debug("setBean2(Bean2 bean2)" ); this .bean2 = bean2; } @PostConstruct public void init () { log.debug("init" ); initialized = true ; } public Bean2 getBean2 () { log.debug("getBean2()" ); return bean2; } public boolean isInitialized () { log.debug("isInitialized()" ); return initialized; } }
1 2 3 @Component public class Bean2 {}
为 Bean1
中的每个方法定制一个前置通知:
1 2 3 4 5 6 7 8 9 10 11 @Aspect @Component public class MyAspect { @Before("execution(* com.itheima.a45.Bean1.*(..))") public void before () { System.out.println("before" ); } }
有一 SpringBoot 主启动类,它与 Bean1
、Bean2
和 MyAspect
在同一包路径下,确保它们能被自动添加到 Spring 容器中:
1 2 3 4 5 6 7 @SpringBootApplication public class A45 { public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A45.class, args); context.close(); } }
运行 main()方法后,控制台打印出:
com.itheima.a45.Bean1 - setBean2(Bean2 bean2)
com.itheima.a45.Bean1 - init
Bean1
中的依赖注入和初始化被成功执行,但 并没有被增强。
由于 Bean1
被增强了,从 Spring 容器中获取的对象将是代理对象:
1 2 3 4 5 6 7 8 9 public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A45.class, args); Bean1 proxy = context.getBean(Bean1.class); proxy.setBean2(new Bean2 ()); proxy.init(); context.close(); }
before
com.itheima.a45.Bean1 - setBean2(Bean2 bean2)
before
com.itheima.a45.Bean1 - init
主动调用的 setBean2()
和 init()
方法 都被增强。
代理对象与目标对象的成员变量不共享
尝试打印代理对象和目标对象的成员变量信息(直接访问,不使用方法):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A45.class, args); Bean1 proxy = context.getBean(Bean1.class); showProxyAndTarget(proxy); context.close(); } @SneakyThrows public static void showProxyAndTarget (Bean1 proxy) { System.out.println(">>>>> 代理中的成员变量" ); System.out.println("\tinitialized = " + proxy.initialized); System.out.println("\tbean2 = " + proxy.bean2); if (proxy instanceof Advised) { Advised advised = (Advised) proxy; System.out.println(">>>>> 目标中的成员变量" ); Bean1 target = (Bean1) advised.getTargetSource().getTarget(); System.out.println("\tinitialized = " + target.initialized); System.out.println("\tbean2 = " + target.bean2); } }
>>>>> 代理中的成员变量
initialized = false
bean2 = null
>>>>> 目标中的成员变量
initialized = true
bean2 = com.itheima.45.Bean2@771db12c
由于依赖注入和初始化只针对目标对象,因此代理对象中的成员变量的值都是初始值。
在实际应用过程中,不会直接去访问成员变量,而是通过方法去访问:
1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A45.class, args); Bean1 proxy = context.getBean(Bean1.class); showProxyAndTarget(proxy); System.out.println(">>>>>>>>>>>>>>>>>>>" ); System.out.println(proxy.getBean2()); System.out.println(proxy.isInitialized()); context.close(); }
before
com.itheima.a45.Bean1 - getBean2()
com.itheima.a45.Bean2@771db12c
before
com.itheima.a45.Bean1 - isInitialized()
true
通过方法访问代理对象的成员变量时,这些方法会被增强,同时代理对象中的方法又会去调用目标对象的方法,从而读取出正确的值。
只会对可以被重写的方法进行增强
在 Bean1 中增加几个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Component public class Bean1 { public void m1 () { System.out.println("m1() 成员方法" ); } final public void m2 () { System.out.println("m2() final 方法" ); } static public void m3 () { System.out.println("m3() static 方法" ); } private void m4 () { System.out.println("m4() private 方法" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @SneakyThrows public static void main (String[] args) { proxy.m1(); proxy.m2(); Bean1.m3(); Method m4 = Bean1.class.getDeclaredMethod("m4" ); m4.setAccessible(true ); m4.invoke(proxy); context.close(); }
before
m1() 成员方法
m2() final 方法
m3() static 方法
m4() private 方法
能被重写的成员方法成功被增强,但被 final
修饰的、被 static
修饰的方法和private
方法由于无法被重写,因此它们不能被增强。如果想增强这些方法,可以使用 AspectJ 编译器增强或者 Agent 类加载。
46. @Value 注入底层
1 2 3 4 5 6 public class Bean1 { @Value("${JAVA_HOME}") private String home; @Value("18") private int age; }
需要解析 @Value("${JAVA_HOME}")
和 @Value("18")
的值,其中 JAVA_HOME
以系统环境变量填充,18 为整型。
解析分为两步:
获取 @Value
注解中 value
属性值;
解析属性值
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 26 27 28 29 30 @Configuration @SuppressWarnings("all") public class A46 { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (A46.class); DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory(); ContextAnnotationAutowireCandidateResolver resolver = new ContextAnnotationAutowireCandidateResolver (); resolver.setBeanFactory(beanFactory); test1(context, resolver); test2(context, resolver); } @SneakyThrows private static void test1 (AnnotationConfigApplicationContext context, ContextAnnotationAutowireCandidateResolver resolver) { DependencyDescriptor dd1 = new DependencyDescriptor (Bean1.class.getDeclaredField("home" ), false ); String value = resolver.getSuggestedValue(dd1).toString(); System.out.println(value); value = context.getEnvironment().resolvePlaceholders(value); System.out.println(value); } }
${JAVA_HOME}
D:\environment\JDK1.8
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @SneakyThrows private static void test2 (AnnotationConfigApplicationContext context, ContextAnnotationAutowireCandidateResolver resolver) { DependencyDescriptor dd1 = new DependencyDescriptor (Bean1.class.getDeclaredField("age" ), false ); String value = resolver.getSuggestedValue(dd1).toString(); System.out.println("@Value 的 value 属性值: " + value); value = context.getEnvironment().resolvePlaceholders(value); System.out.println("解析得到的值: " + value); System.out.println("解析得到的值的类型: " + value.getClass()); Object age = context.getBeanFactory() .getTypeConverter() .convertIfNecessary(value, dd1.getDependencyType()); System.out.println("转换后的类型: " + age.getClass()); }
@Value 的 value 属性值: 18
解析得到的值: 18
解析得到的值的类型: class java.lang.String
转换后的类型: class java.lang.Integer
EL 表达式的解析
有如下几个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Bean2 { @Value("#{@bean3}") private Bean3 bean3; } @Component("bean3") public class Bean3 {} static class Bean4 { @Value("#{'hello, ' + '${JAVA_HOME}'}") private String value; }
同样要求解析 @Value
中的 value
属性值。
如果沿用 test2()
方法进行解析,控制台打印出:
@Value 的 value 属性值: #{@bean3}
解析得到的值: #{@bean3}
解析得到的值的类型: class java.lang.String
Exception in thread "main" org.springframework.beans.ConversionNotSupportedException: Failed to convert value of type 'java.lang.String' to required type 'com.itheima.a46.A46$Bean3'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'com.itheima.a46.A46$Bean3': no matching editors or conversion strategy found
最后一步数据转换出了问题,无法将 String
转换成 A46$Bean3
类型,也就是说解析 @bean3
失败了,程序仍然把它当成字符串,而不是注入的 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 26 27 28 29 30 31 32 33 @SneakyThrows public static void main (String[] args) { test3(context, resolver, Bean2.class.getDeclaredField("bean3" )); System.out.println(">>>>>>>>>>>>>>>>>>>" ); test3(context, resolver, Bean4.class.getDeclaredField("value" )); } private static void test3 (AnnotationConfigApplicationContext context, ContextAnnotationAutowireCandidateResolver resolver, Field field) { DependencyDescriptor dd1 = new DependencyDescriptor (field, false ); String value = resolver.getSuggestedValue(dd1).toString(); System.out.println("@Value 的 value 属性值: " + value); value = context.getEnvironment().resolvePlaceholders(value); System.out.println("解析得到的值: " + value); System.out.println("解析得到的值的类型: " + value.getClass()); Object bean3 = context.getBeanFactory() .getBeanExpressionResolver() .evaluate(value, new BeanExpressionContext (context.getBeanFactory(), null )); Object result = context.getBeanFactory() .getTypeConverter() .convertIfNecessary(bean3, dd1.getDependencyType()); System.out.println("转换后的类型: " + result.getClass()); }
@Value 的 value 属性值: #{@bean3}
解析得到的值: #{@bean3}
解析得到的值的类型: class java.lang.String
转换后的类型: class com.itheima.a46.A46$Bean3
>>>>>>>>>>>>>>>>>>>
@Value 的 value 属性值: #{'hello, ' + '${JAVA_HOME}'}
解析得到的值: #{'hello, ' + 'D:\environment\JDK1.8'}
解析得到的值的类型: class java.lang.String
转换后的类型: class java.lang.String
47. @Autowired 注入底层
47.1 注入方式
按成员变量类型注入
现有Bean1
类如下:
1 2 3 4 static class Bean1 { @Autowired private Bean2 bean2; }
需要被注入的对象所在类:
1 2 3 @Component("bean2") static class Bean2 {}
从容器中获取需要被注入的 Bean 对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration public class A47_1 { @SneakyThrows public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (A47_1.class); DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory(); DependencyDescriptor dd1 = new DependencyDescriptor (Bean1.class.getDeclaredField("bean2" ), false ); System.out.println(beanFactory.doResolveDependency(dd1, "bean1" , null , null )); } }
com.itheima.a47.A47_1$Bean2@222545dc
按参数类型注入
对 Bean1
进行修改:
1 2 3 4 5 6 7 8 static class Bean1 { @Autowired public void setBean2 (Bean2 bean2) { this .bean2 = bean2; } }
根据 setBean2()
方法的 Bean2
类型参数进行注入:
1 2 3 4 5 6 7 8 9 @SneakyThrows public static void main (String[] args) { Method setBean2 = Bean1.class.getDeclaredMethod("setBean2" , Bean2.class); DependencyDescriptor dd2 = new DependencyDescriptor (new MethodParameter (setBean2, 0 ), false ); System.out.println(beanFactory.doResolveDependency(dd2, "bean1" , null , null )); }
com.itheima.a47.A47_1$Bean2@222545dc
包装为 Optional<Bean2>
对 Bean1 进行修改:
1 2 3 4 5 6 static class Bean1 { @Autowired private Optional<Bean2> bean3; }
如果直接按照以下方式获取 DependencyDescriptor
对象:
1 2 DependencyDescriptor dd3 = new DependencyDescriptor (Bean1.class.getDeclaredField("bean3" ), false );
其 dd3.getDependencyType()
方法将返回 Optional
的 Class
对象,这显然是不对的。
Spring 为 DependencyDescriptor 提供了解决这个问题的方法,即“增加嵌套等级”来获取内嵌类型:
1 dd3.increaseNestingLevel();
执行 increaseNestingLevel()
方法后,dd3.getDependencyType()
方法返回的 Bean2
的 Class
对象。
因此注入 Optional<Bean2>
类型的成员变量应该按照:
1 2 3 4 5 6 7 8 9 10 11 12 @SneakyThrows public static void main (String[] args) { DependencyDescriptor dd3 = new DependencyDescriptor (Bean1.class.getDeclaredField("bean3" ), false ); if (Optional.class.equals(dd3.getDependencyType())) { dd3.increaseNestingLevel(); Object result = beanFactory.doResolveDependency(dd3, "bean1" , null , null ); System.out.println(Optional.ofNullable(result)); } }
Optional[com.itheima.a47.A47_1$Bean2@222545dc]
注入 Optional
对象和使用 @Autowired(required = false)
的作用是一样的,当容器中不存在目标 Bean 时,不会抛出 NoSuchBeanDefinitionException
异常。
包装为 ObjectFactory<Bean2>
对 Bean1
进行修改:
1 2 3 4 5 6 static class Bean1 { @Autowired private ObjectFactory<Bean2> bean4; }
注入 ObjectFactory<Bean2>
类型的对象与注入 Optional<Bean2>
类型的对象类似,只不过 ObjectFactory
提供了 延迟注入 的能力,也就是说 Bean2
对象不会立即被注入,而是在需要时才被注入。
ObjectFactory
是一个函数式接口:
1 2 3 4 @FunctionalInterface public interface ObjectFactory <T> { T getObject () throws BeansException; }
注入的应该是 ObjectFactory
对象,在调用该对象的 getObject()
方法时,Bean2
对象才被注入:
1 2 3 4 5 6 7 8 9 10 11 12 13 @SneakyThrows public static void main (String[] args) { DependencyDescriptor dd4 = new DependencyDescriptor (Bean1.class.getDeclaredField("bean4" ), false ); if (ObjectFactory.class.equals(dd4.getDependencyType())) { dd4.increaseNestingLevel(); ObjectFactory<Bean2> objectFactory = () -> (Bean2) beanFactory.doResolveDependency(dd4, "bean1" , null , null ); System.out.println(objectFactory.getObject()); } }
com.itheima.a47.A47_1$Bean2@222545dc
与 ObjectFactory
类似的还有个名为 ObjectProvider
的接口,后者继承了前者。
与 ObjectFactory
相比,ObjectProvider
提供了类似于 Optional
的安全注入功能,当容器中不存在目标 Bean 时, 不会抛出 NoSuchBeanDefinitionException
异常。ObjectProvider
提供的 getIfAvailable()
在获取不存在的 Bean 时,不会抛出异常,而是返回 null
。
对 @Lazy
的处理
对 Bean1
进行修改,在成员变量 bean2
上使用 @Lazy
注解:
1 2 3 4 5 6 7 static class Bean1 { @Autowired @Lazy private Bean2 bean2; }
对于 @Lazy
注解标记的成员变量,注入的对象不再是目标对象,而是其代理对象,因此不能使用 DefaultListableBeanFactory
对象的 doResolveDependency()
方法来获取注入的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @SneakyThrows public static void main (String[] args) { DependencyDescriptor dd5 = new DependencyDescriptor (Bean1.class.getDeclaredField("bean2" ), false ); ContextAnnotationAutowireCandidateResolver resolver = new ContextAnnotationAutowireCandidateResolver (); resolver.setBeanFactory(beanFactory); Object proxy = resolver.getLazyResolutionProxyIfNecessary(dd5, "bean1" ); System.out.println(proxy); System.out.println(proxy.getClass()); }
com.itheima.a47.A47_1$Bean2@222545dc
class com.itheima.a47.A47_1$Bean2$$EnhancerBySpringCGLIB$$d631a20c
@Lazy
实现的 延迟注入 (前面讲的 ObjectFactory
和 ObjectProvider
也有延迟注入功能,但与 @Lazy
的实现不一样)不是不注入,而是注入目标对象的代理对象,当使用到代理对象中的方法时,代理对象就会去 Spring 容器中寻找真正的目标对象,然后调用目标对象对应的方法。
@Lazy
的实现细节可以在 ContextAnnotationAutowireCandidateResolver
中看到:
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 26 27 28 29 30 31 32 33 34 35 public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotationAutowireCandidateResolver { @Override @Nullable public Object getLazyResolutionProxyIfNecessary (DependencyDescriptor descriptor, @Nullable String beanName) { return (isLazy(descriptor) ? buildLazyResolutionProxy(descriptor, beanName) : null ); } protected boolean isLazy (DependencyDescriptor descriptor) { for (Annotation ann : descriptor.getAnnotations()) { Lazy lazy = AnnotationUtils.getAnnotation(ann, Lazy.class); if (lazy != null && lazy.value()) { return true ; } } MethodParameter methodParam = descriptor.getMethodParameter(); if (methodParam != null ) { } return false ; } protected Object buildLazyResolutionProxy (final DependencyDescriptor descriptor, final @Nullable String beanName) { return pf.getProxy(dlbf.getBeanClassLoader()); } }
补充:包装为 Provider<Bean2>
Provider
接口是由 JSR-330 提出,要想使用此接口,需要导入以下依赖:
1 2 3 4 5 6 <dependency > <groupId > javax.inject</groupId > <artifactId > javax.inject</artifactId > <version > 1</version > </dependency >
对 Bean1 进行修改:
1 2 3 4 5 6 static class Bean1 { @Autowired private Provider<Bean2> bean5; }
注入 Provider
类型的对象与注入 ObjectFactory<Bean2>
类型的对象极其相似,Provider
也提供了 延迟注入 的能力,注入的是 Provider
对象,在调用该对象的 get()
方法时,Bean2
对象才被注入:
1 2 3 4 5 6 7 8 9 10 11 12 @SneakyThrows public static void main (String[] args) { DependencyDescriptor dd6 = new DependencyDescriptor (Bean1.class.getDeclaredField("bean5" ), false ); if (Provider.class.equals(dd6.getDependencyType())) { dd6.increaseNestingLevel(); Provider<Bean2> provider = () -> (Bean2) beanFactory.doResolveDependency(dd6, "bean1" , null , null ); System.out.println(provider.get()); } }
com.itheima.a47.A47_1$Bean2@222545dc
Optional
类型、ObjectFactory
类型、ObjectProvider
类型、JSR-330
提供的类型的注入逻辑可在 DefaultListableBeanFactory#resolveDependency()
方法中看到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Override @Nullable public Object resolveDependency (DependencyDescriptor descriptor, @Nullable String requestingBeanName, @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { descriptor.initParameterNameDiscovery(getParameterNameDiscoverer()); if (Optional.class == descriptor.getDependencyType()) { } else if (ObjectFactory.class == descriptor.getDependencyType() || ObjectProvider.class == descriptor.getDependencyType()) { } else if (javaxInjectProviderClass == descriptor.getDependencyType()) { } else { } }
47.2 类型匹配细节
无论是 @Value
注入,还是 @Autowired
注入,最终都会调用 DefaultListableBeanFactory#doResolveDependency()
方法。
现有如下几个类:
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 26 27 28 29 30 31 32 interface Dao <T> {} @Component("dao1") static class Dao1 implements Dao <Student> {} @Component("dao2") static class Dao2 implements Dao <Teacher> {} static class Student {} static class Teacher {} interface Service {} @Component("service1") static class Service1 implements Service {} @Component("service2") static class Service2 implements Service {} @Component("service3") static class Service3 implements Service {}
有一目标类 Target
,对其进行依赖注入:
1 2 3 4 5 6 7 8 9 10 11 12 13 static class Target { @Autowired private Service[] serviceArray; @Autowired private List<Service> serviceList; @Autowired private ConfigurableApplicationContext applicationContext; @Autowired private Dao<Teacher> dao; @Autowired @Qualifier("service2") private Service service; }
数据类型
Spring 容器中肯定不存在数组类型且元素类型为 Service
的 Bean 对象,因此注入的 Service
数组应当是容器中 Service
类型的 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 26 27 28 29 30 31 32 33 34 35 @Configuration @SuppressWarnings("all") public class A47_2 { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (A47_2.class); DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory(); testArray(beanFactory); } @SneakyThrows private static void testArray (DefaultListableBeanFactory beanFactory) { DependencyDescriptor dd1 = new DependencyDescriptor (Target.class.getDeclaredField("serviceArray" ), true ); if (dd1.getDependencyType().isArray()) { Class<?> componentType = dd1.getDependencyType().getComponentType(); System.out.println(componentType); String[] names = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( beanFactory, componentType ); List<Object> beans = new ArrayList <>(); for (String name : names) { System.out.println(name); Object bean = dd1.resolveCandidate(name, componentType, beanFactory); beans.add(bean); } Object array = beanFactory.getTypeConverter() .convertIfNecessary(beans, dd1.getDependencyType()); System.out.println(array); } } }
interface com.itheima.a47.A47_2$Service
service3
service2
service1
com.itheima.a47.A47_2$Service;@49139829
相关源码可在 DefaultListableBeanFactory#resolveMultipleBeans()
方法中看到。
List
类型
注入 List<Service>
类型数据的逻辑与注入 Service[]
类型数据的逻辑类似,只不过在容器中寻找目标 Bean 时不再通过数组元素类型,而是通过 List
的泛型类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @SneakyThrows private static void testList (DefaultListableBeanFactory beanFactory) { DependencyDescriptor dd2 = new DependencyDescriptor (Target.class.getDeclaredField("serviceList" ), true ); if (List.class.equals(dd2.getDependencyType())) { Class<?> resolve = dd2.getResolvableType().getGeneric().resolve(); System.out.println(resolve); List<Object> list = new ArrayList <>(); String[] names = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, resolve); for (String name : names) { Object bean = dd2.resolveCandidate(name, resolve, beanFactory); list.add(bean); } System.out.println(list); } }
interface com.itheima.a47.A47_2$Service
[com.itheima.a47.A47_2$Service3@35e2d654, com.itheima.a47.A47_2$Service2@1bd4fdd, com.itheima.a47.A47_2$Service1@55183b20]
注意: 对于注入的集合类型数据,注入的类型必须是 Collection
及其 子接口,比如不支持直接注入 ArrayList
类型的数据。
相关源码可在 DefaultListableBeanFactory#resolveMultipleBeans()
方法中看到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Nullable private Object resolveMultipleBeans (DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) { Class<?> type = descriptor.getDependencyType(); if (descriptor instanceof StreamDependencyDescriptor) { } else if (type.isArray()) { } else if (Collection.class.isAssignableFrom(type) && type.isInterface()) { } else if (Map.class == type) { } else { return null ; } }
从源码中可以看到,@Autowired
还支持 Map
类型数据的注入,此时注入的 Map 的 key 是 Bean 的名称,value 是 Bean 对象,这种方式常常配合策略模式使用。需要注意的是,只支持注入 Map
接口,不支持其子类。
特殊类型 ConfigurableApplicationContext
ConfigurableApplicationContext
是 ApplicationContext
接口的子接口。
需要注意的是,在 Spring 容器中并不存在 ConfigurableApplicationContext
类型、或 ApplicationContext
类型的 Bean。
Spring 容器中的所有单例 Bean 对象存放在 DefaultListableBeanFactory
中,在 DefaultListableBeanFactory
父类 DefaultSingletonBeanRegistry
中有一成员变量:
1 2 3 4 5 6 7 public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry { private final Map<String, Object> singletonObjects = new ConcurrentHashMap <>(256 ); }
singletonObjects
用于存放 Spring 容器中的所有单例 Bean 对象。
类似 ApplicationContext
、BeanFactory
类型的对象则是放在 DefaultListableBeanFactory
中的 resolvableDependencies
中:
1 2 3 4 5 6 7 8 public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory , BeanDefinitionRegistry, Serializable { private final Map<Class<?>, Object> resolvableDependencies = new ConcurrentHashMap <>(16 ); }
这些特殊对象是在调用 ApplicationContext
的 refresh()
方法时添加到 resolvableDependencies
中的。可在 AbstractApplicationContext
的 refresh()
方法中看到:
1 2 3 4 5 6 7 8 9 10 public void refresh () throws BeansException, IllegalStateException { synchronized (this .startupShutdownMonitor) { prepareBeanFactory(beanFactory); } }
1 2 3 4 5 6 7 8 9 10 11 12 protected void prepareBeanFactory (ConfigurableListableBeanFactory beanFactory) { beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory); beanFactory.registerResolvableDependency(ResourceLoader.class, this ); beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this ); beanFactory.registerResolvableDependency(ApplicationContext.class, this ); }
因此在注入诸如 ConfigurableApplicationContext
特殊类型的对象时,不能直接使用 getBean()
方法获取,而是应该从 resolvableDependencies
集合中获取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @SneakyThrows private static void testApplicationContext (DefaultListableBeanFactory beanFactory) { DependencyDescriptor dd3 = new DependencyDescriptor ( Target.class.getDeclaredField("applicationContext" ), true ); Field resolvableDependencies = DefaultListableBeanFactory.class.getDeclaredField("resolvableDependencies" ); resolvableDependencies.setAccessible(true ); Map<Class<?>, Object> dependencies = (Map<Class<?>, Object>) resolvableDependencies.get(beanFactory); for (Map.Entry<Class<?>, Object> entry : dependencies.entrySet()) { if (entry.getKey().isAssignableFrom(dd3.getDependencyType())) { System.out.println(entry.getValue()); break ; } } }
org.springframework.beans.factory.support.DefaultListableBeanFactory@7364985f: defining beans
[org.springframework.context.annotation.internalConfigurationAnnotationProcessor,
org.springframework.context.annotation.internalAutowiredAnnotationProcessor,
org.springframework.context.annotation.internalCommonAnnotationProcessor,
org.springframework.context.event.internalEventListenerProcessor,
org.springframework.context.event.internalEventListenerFactory,
a47_2,service3,service2,service1,dao2,dao1]; root of factory hierarchy
泛型类型
容器中 Dao
类型的 Bean 有多个,而依赖注入的是 Dao<Teacher>
类型的对象,因此需要判断容器中的 Bean 对象泛型类型是否为指定类型。判断逻辑可以使用 ContextAnnotationAutowireCandidateResolver
的 isAutowireCandidate()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @SneakyThrows private static void testGeneric (DefaultListableBeanFactory beanFactory) { DependencyDescriptor dd4 = new DependencyDescriptor (Target.class.getDeclaredField("dao" ), true ); Class<?> type = dd4.getDependencyType(); ContextAnnotationAutowireCandidateResolver resolver = new ContextAnnotationAutowireCandidateResolver (); resolver.setBeanFactory(beanFactory); for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type)) { BeanDefinition bd = beanFactory.getMergedBeanDefinition(name); if (resolver.isAutowireCandidate(new BeanDefinitionHolder (bd, name), dd4)) { System.out.println(name); System.out.println(dd4.resolveCandidate(name, type, beanFactory)); } } }
dao2
com.itheima.a47.A47_2$Dao2@74f0ea28
@Qualifier
当容器中存在多个相同类型的 Bean 对象,在执行依赖注入时可以使用 @Qualifier
注解来指定需要注入的 Bean 对象的名称。判断逻辑同样使用 ContextAnnotationAutowireCandidateResolver
的 isAutowireCandidate()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @SneakyThrows private static void testQualifier (DefaultListableBeanFactory beanFactory) { DependencyDescriptor dd5 = new DependencyDescriptor (Target.class.getDeclaredField("service" ), true ); Class<?> type = dd5.getDependencyType(); ContextAnnotationAutowireCandidateResolver resolver = new ContextAnnotationAutowireCandidateResolver (); resolver.setBeanFactory(beanFactory); for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type)) { BeanDefinition bd = beanFactory.getMergedBeanDefinition(name); if (resolver.isAutowireCandidate(new BeanDefinitionHolder (bd, name), dd5)) { System.out.println(name); System.out.println(dd5.resolveCandidate(name, type, beanFactory)); } } }
service2
com.itheima.a47.A47_2$Service2@1bd4fdd
@Primary
当容器中存在多个相同类型的 Bean 对象时,在执行依赖注入时除了可以使用 @Qualifier
注解外,还可以在被注入的 Bean 对象所在类上使用 @Primary
注解,指定执行依赖注入时使用的主要 Bean 对象。
如果 Bean 对象的所在类被 @Primary
注解标记,那么在构造 BeanDefinition
时就会记录这个信息。
通常情况下,@Primary
注解只有一个作用在同种类型的 Bean 上,存在多个时,Spring 依旧无法区分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static class Target1 { @Autowired private Service service; } interface Service {} @Component("service1") static class Service1 implements Service {} @Primary @Component("service2") static class Service2 implements Service {} @Component("service3") static class Service3 implements Service {}
1 2 3 4 5 6 7 8 9 10 11 @SneakyThrows private static void testPrimary (DefaultListableBeanFactory beanFactory) { DependencyDescriptor dd = new DependencyDescriptor (Target1.class.getDeclaredField("service" ), false ); Class<?> type = dd.getDependencyType(); for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type)) { if (beanFactory.getMergedBeanDefinition(name).isPrimary()) { System.out.println("primary: " + name); } } }
primary: service2
默认规则
当容器中存在多个相同类型的 Bean 对象时,除了使用 @Qualifier
或 @Primary
注解外,@Autowired
注解还支持按照成员变量名称进行匹配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static class Target2 { @Autowired private Service service3; } interface Service {} @Component("service1") static class Service1 implements Service {} @Primary @Component("service2") static class Service2 implements Service {} @Component("service3") static class Service3 implements Service {}
1 2 3 4 5 6 7 8 9 10 11 @SneakyThrows private static void testDefault (DefaultListableBeanFactory beanFactory) { DependencyDescriptor dd = new DependencyDescriptor (Target2.class.getDeclaredField("service3" ), false ); Class<?> type = dd.getDependencyType(); for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type)) { if (name.equals(dd.getDependencyName())) { System.out.println("default: " + name); } } }
default: service3
48. 事件 - 监听器
Spring 提供了事件发布 - 监听(订阅)机制,利用该机制可以令主要业务与附加业务解耦,是观察者模式的一种体现。
本节介绍事件的监听,事件的发布将在下一节进行介绍。
48.1 监听器的实现
实现 ApplicationListener
接口
需求模拟:假设现有一个【主线业务】,要求在【主线业务】执行完成之后,执行【发送短信】、【发送邮件】。
常规写法是将这三种业务依次写在一个方法里,程序顺序执行它们,但这将增加代码的耦合性。
利用 Spring 提供的事件发布 - 监听(订阅)机制来解决这个问题可以大大地降低代码的耦合性。
首先定义事件的类型,用于后续的发布和监听,该事件类必须继承 ApplicationEvent
抽象类:
1 2 3 4 5 6 7 static class MyEvent extends ApplicationEvent { private static final long serialVersionUID = -1541319641201302606L ; public MyEvent (Object source) { super (source); } }
【主线业务】执行完成后,发送事件:
1 2 3 4 5 6 7 8 9 10 11 12 @Slf4j @Component static class MyService { @Autowired private ApplicationEventPublisher publisher; public void doBusiness () { log.debug("主线业务" ); publisher.publishEvent(new MyEvent ("MyService.doBusiness()" )); } }
之后定义两个监听器,监听发布的事件,执行【发送短信】、【发送邮件】,定义的监听器需要实现 ApplicationListener
接口,并交由 Spring 管理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Slf4j @Component static class SmsApplicationListener implements ApplicationListener <MyEvent> { @Override public void onApplicationEvent (MyEvent event) { log.debug("发送短信" ); } } @Slf4j @Component static class EmailApplicationListener implements ApplicationListener <MyEvent> { @Override public void onApplicationEvent (MyEvent event) { log.debug("发送邮件" ); } }
最后调用 doBusiness()
串联整个逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 @Slf4j @Configuration public class A48_1 { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (A48_1.class); context.getBean(MyService.class).doBusiness(); context.close(); } }
运行 main()方法后,控制台打印出:
[main] com.itheima.a48.A48_1$MyService - 主线业务
[main] i.m.a.A48_1$EmailApplicationListener - 发送邮件
[main] i.m.a.A48_1$SmsApplicationListener - 发送短信
使用 @EventListener
注解
监听器的实现除了实现 ApplicationListener
接口外,还可以使用 @EventListener
注解。比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Slf4j @Component static class SmsService { @EventListener public void listener (MyEvent myEvent) { log.debug("发送短信" ); } } @Slf4j @Component static class EmailService { @EventListener public void listener (MyEvent myEvent) { log.debug("发送邮件" ); } }
运行 main() 方法后,控制台输出相同的内容。
48.2 异步事件
从上文控制台输出的信息可知,事件都是在主线程 main
中被监听,都是同步执行的。
那怎么发送异步事件呢?
ApplicationEventPublisher
底层利用了 SimpleApplicationEventMulticaster
来发布事件,SimpleApplicationEventMulticaster
在发布事件时可以指定是否使用线程池,如果实现了线程池,那么就是异步事件,反之为同步。
因此可以先添加一个线程池 Bean,然后使 SimpleApplicationEventMulticaster
利用这个线程池 Bean 来发送事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Bean public ThreadPoolTaskExecutor executor () { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor (); executor.setCorePoolSize(3 ); executor.setMaxPoolSize(10 ); executor.setQueueCapacity(100 ); return executor; } @Bean public SimpleApplicationEventMulticaster applicationEventMulticaster (ThreadPoolTaskExecutor executor) { SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster (); multicaster.setTaskExecutor(executor); return multicaster; }
注意: SimpleApplicationEventMulticaster
类型的 Bean 的名称必须是 applicationEventMulticaster
,这样才能覆盖原本未使用线程池的 Bean。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class A48_2 { @SneakyThrows public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (A48_2.class); context.getBean(MyService.class).doBusiness(); TimeUnit.SECONDS.sleep(2 ); context.close(); } }
运行 main()方法后,控制台打印出:
[main] com.itheima.a48.A48_2$MyService - 主线业务
[executor-1] com.itheima.a48.A48_2$EmailService - 发送邮件
[executor-2] com.itheima.a48.A48_2$SmsService - 发送短信
【主线业务】、【发送短信】和【发送邮件】都在不同线程中被执行。
【补充】@Async
实现异步事件
实际开发中,在监听器方法或监听器类上添加 @Async
注解即可实现异步事件。比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Slf4j @Component static class SmsService { @Async @EventListener public void listener (MyEvent myEvent) { log.debug("发送短信" ); } } @Slf4j @Component static class EmailService { @Async @EventListener public void listener (MyEvent myEvent) { log.debug("发送邮件" ); } }
运行 main() 方法后,根据控制台打印出的信息可知仍是同步消息,这是因为 没有开启异步支持。
在配置类上使用 @EnableAsync
注解,开启异步支持:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Slf4j @Configuration @EnableAsync(proxyTargetClass = true) public class TestAsync { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (TestAsync.class); context.getBean(MyService.class).doBusiness(); context.close(); } @Bean public ThreadPoolTaskExecutor executor () { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor (); executor.setCorePoolSize(3 ); executor.setMaxPoolSize(10 ); executor.setQueueCapacity(100 ); return executor; } }
也可以注入自行实现的线程池 Bean,使用这个 Bean 来发布异步事件。当然也可以不注入,Spring 提供了默认的线程池。
建议: 使用 @EnableAsync
注解时,尽量指定 proxyTargetClass
的属性值为 true
,采用 CGLib 动态代理,避免监听器类实现接口而监听器方法又未在基类中声明时,导致使用默认 JDK 动态代理失败。
运行 main() 方法,控制台打印出:
[main] com.itheima.a48.TestAsync$MyService - 主线业务
[executor-2] com.itheima.a48.TestAsync$SmsService - 发送短信
[executor-1] com.itheima.a48.TestAsync$EmailService - 发送邮件
48.3 自定义事件监听注解
无论是实现 ApplicationListener
接口,还是使用 @EventListener
注解,监听器类都需要交由 Spring 管理,那 Spring 是怎么实现事件的监听的呢?
以自定义事件监听注解为例,简单了解 Spring 的事件监听机制。
自定义 @MyListener
注解:
1 2 3 4 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface MyListener {}
定义事件类型 MyEvent
:
1 2 3 4 5 6 7 static class MyEvent extends ApplicationEvent { private static final long serialVersionUID = -6388410688691384516L ; public MyEvent (Object source) { super (source); } }
定义两个监听器类,监听 MyEvent
事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Slf4j @Component static class SmsService { @MyListener public void listener (MyEvent myEvent) { log.debug("发送短信" ); } } @Slf4j @Component static class EmailService { @MyListener public void listener (MyEvent myEvent) { log.debug("发送邮件" ); } }
再定义 MyService
,在该业务类中的 doBusiness()
方法发送 MyEvent
事件:
1 2 3 4 5 6 7 8 9 10 11 @Slf4j @Component static class MyService { @Autowired private ApplicationEventPublisher publisher; public void doBusiness () { log.debug("主线业务" ); publisher.publishEvent(new MyEvent ("MyService.doBusiness()" )); } }
接下来就是解析 @MyListener
注解了:
首先需要获取容器中的所有 Bean 对象
查看这些 Bean 对象中是否存在被 @MyListener
注解标记的方法
将被 @MyListener
注解标记的方法转换成事件监听器添加到 Spring 容器中(适配器模式),这些事件监听器需要判断监听的事件类型是否与原始方法的参数类型一致,一致的情况下才执行方法
在第一步中需要拿到容器中的所有 Bean 对象,因此前面的逻辑要在 Spring 中所有单例 Bean 初始化完成后才执行,可以使用 SmartInitializingSingleton
接口实现
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 @Bean public SmartInitializingSingleton smartInitializingSingleton (ConfigurableApplicationContext context) { return () -> { for (String name : context.getBeanDefinitionNames()) { Object bean = context.getBean(name); for (Method method : bean.getClass().getMethods()) { if (method.isAnnotationPresent(MyListener.class)) { context.addApplicationListener((event) -> { Class<?> eventType = method.getParameterTypes()[0 ]; if (eventType.isAssignableFrom(event.getClass())) { try { method.invoke(bean, event); } catch (Exception e) { throw new RuntimeException (e); } } }); } } } }; }
1 2 3 4 5 6 7 8 9 10 11 @Configuration public class A48_3 { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (A48_3.class); context.getBean(MyService.class).doBusiness(); context.close(); } }
[main] com.itheima.a48.A48_3$MyService - 主线业务
[main] com.itheima.a48.A48_3$EmailService - 发送邮件
[main] com.itheima.a48.A48_3$SmsService - 发送短信
48.4 【补充】监听器执行顺序
当一个事件有多个对应的监听器时,这些监听器的执行顺序是不确定的。
如果需要监听器按指定的顺序执行,可以使监听器类实现 SmartApplicationListener
接口,重写 getOrder()
方法,指定监听器优先级。除此之外,使用 @Order
注解、在实现 ApplicationListener
接口的基础上再实现 Ordered
接口也能实现相同的功能。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 @Slf4j @Component static class SmsService1 { @Order(1) @EventListener public void listener (MyEvent myEvent) { log.debug("发送短信-1" ); } } @Slf4j @Component static class EmailService1 { @Order(2) @EventListener public void listener (MyEvent myEvent) { log.debug("发送邮件-1" ); } } @Slf4j @Component static class SmsService2 implements ApplicationListener <MyEvent>, Ordered { @Override public int getOrder () { return 3 ; } @Override public void onApplicationEvent (MyEvent event) { log.debug("发送短信-2" ); } } @Slf4j @Component static class EmailService2 implements SmartApplicationListener { @Override public boolean supportsEventType (Class<? extends ApplicationEvent> eventType) { return MyEvent.class.equals(eventType); } @Override public int getOrder () { return 4 ; } @Override public void onApplicationEvent (ApplicationEvent event) { log.debug("发送邮件-2" ); } }
[main] i.m.a.TestSmartApplicationListener$MyService - 主线业务
[main] i.m.a.TestSmartApplicationListener$SmsService1 - 发送短信-1
[main] i.m.a.TestSmartApplicationListener$EmailService1 - 发送邮件-1
[main] i.m.a.TestSmartApplicationListener$SmsService2 - 发送短信-2
[main] i.m.a.TestSmartApplicationListener$EmailService2 - 发送邮件-2
48.5 【补充】事件的传播
当 Spring 容器嵌套 Spring 容器时,通过子容器发布事件,能够在父容器监听到。
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 26 27 28 public static void main (String[] args) { AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext (); parent.registerBean(MyListener.class); parent.refresh(); AnnotationConfigApplicationContext child = new AnnotationConfigApplicationContext (); child.setParent(parent); child.refresh(); child.publishEvent(new MyEvent ("子容器发送的事件..." )); } static class MyEvent extends ApplicationEvent { private static final long serialVersionUID = -7002403082731659626L ; public MyEvent (String source) { super (source); } } @Slf4j static class MyListener { @EventListener public void listener (MyEvent myEvent) { log.debug(String.valueOf(myEvent.getSource())); } }
[main] i.m.a.EventPropagationTest$MyListener - 子容器发送的事件...
48.6 【补充】带泛型的事件
现有一个事件类 MutationEvent
,接收一个泛型参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Getter static class MutationEvent <T> extends ApplicationEvent { private static final long serialVersionUID = -2718823625228147843L ; private final T source; private final String type; public MutationEvent (T data, String type) { super (data); this .source = data; this .type = type; } }
事件对应的监听器 MutationEventListener
:
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 26 27 28 29 30 31 32 33 @Getter @AllArgsConstructor static class Pizza { private final String name; private final double price; } @Getter @AllArgsConstructor static class ChineseHamburger { private final double price; private final String size; } static class MutationEventListener { @EventListener public void handlePizza (MutationEvent<Pizza> event) { System.out.println("监听到 Pizza..." ); System.out.println("类型是: " + event.getType()); Pizza pizza = event.getSource(); System.out.println("Pizza 名称为: " + pizza.getName() + ", 价格为: " + pizza.getPrice()); } @EventListener public void handleChineseHamburger (MutationEvent<ChineseHamburger> event) { System.out.println("监听到肉夹馍..." ); System.out.println("类型是: " + event.getType()); ChineseHamburger hamburger = event.getSource(); System.out.println("肉夹馍的价格是: " + hamburger.getPrice() + ", 大小是: " + hamburger.getSize()); } }
尝试发布 MutationEvent<Pizza>
类型的事件,看看监听器是否能监听到:
1 2 3 4 5 6 7 8 public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (); context.registerBean(MutationEventListener.class); context.refresh(); Pizza pizza = new Pizza ("NewYorkPizza" , 25 ); context.publishEvent(new MutationEvent <>(pizza, "ONE" )); }
运行 main() 方法后,抛出 ClassCastException
异常,提示无法将 Pizza
转换成 ChineseHamburger
,事件监听失败。
由于泛型擦除,无法通过事件真正的内部对象类型来分发事件,为了解决这个问题,需要使类实现 ResolvableTypeProvider
接口。
如果未实现 ResolvableTypeProvider
接口:
但实现了 ApplicationEvent
接口,尽管在监听器方法和发布事件时都指定了泛型参数信息,但所有的监听器方法都会被执行,由此可能产生 ClassCastException
;
也未实现 ApplicationEvent
接口,就算发送的泛型事件的内部对象类型与监听器指定的泛型事件的内部对象类型一样,也不会监听成功。
1 2 3 4 5 6 7 8 9 10 @Getter static class MutationEvent <T> extends ApplicationEvent implements ResolvableTypeProvider { @Override public ResolvableType getResolvableType () { return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(this .source)); } }
再次运行 main() 方法后,不再抛出异常,控制台打印出:
监听到 Pizza...
类型是: ONE
Pizza 名称为: NewYorkPizza, 价格为: 25.0
再发布泛型参数类型为 ChineseHamburger
的事件:
1 2 3 4 5 6 public static void main (String[] args) { ChineseHamburger hamburger = new ChineseHamburger (18 , "M" ); context.publishEvent(new MutationEvent <>(hamburger, "TWO" )); }
监听到 Pizza...
类型是: ONE
Pizza 名称为: NewYorkPizza, 价格为: 25.0
监听到肉夹馍...
类型是: TWO
肉夹馍的价格是: 18.0, 大小是: M
49. 事件 - 发布器
49.1 自定义事件发布
前文说到,事件的发布使用了 SimpleApplicationEventMulticaster
,它的顶层接口是 ApplicationEventMulticaster
,尝试自定义 ApplicationEventMulticaster
的实现类,实现事件的发布。
ApplicationEventMulticaster
接口的抽象方法有很多,本节只实现重要方法,采用默认适配器处理:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 abstract static class AbstractApplicationEventMulticaster implements ApplicationEventMulticaster { @Override public void addApplicationListener (ApplicationListener<?> listener) { } @Override public void addApplicationListenerBean (String listenerBeanName) { } @Override public void removeApplicationListener (ApplicationListener<?> listener) { } @Override public void removeApplicationListenerBean (String listenerBeanName) { } @Override public void removeApplicationListeners (Predicate<ApplicationListener<?>> predicate) { } @Override public void removeApplicationListenerBeans (Predicate<String> predicate) { } @Override public void removeAllListeners () { } @Override public void multicastEvent (ApplicationEvent event) { } @Override public void multicastEvent (ApplicationEvent event, ResolvableType eventType) { } }
需要实现的方法有两个:
addApplicationListenerBean()
:收集容器中所有的监听器
multicastEvent()
:发布事件
收集监听器时,需要获取监听器支持的事件类型,将原始的监听器封装为支持事件类型检查的监听器,这种监听器在发布事件时,使用线程池支持发布异步事件。
发布事件时,遍历容器中所有监听器,当监听器支持的事件类型与发布的事件类型一致时才发布事件。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Bean @SuppressWarnings("all") public ApplicationEventMulticaster applicationEventMulticaster (ConfigurableApplicationContext context, ThreadPoolTaskExecutor executor) { return new AbstractApplicationEventMulticaster () { private List<GenericApplicationListener> listeners = new ArrayList <>(); public void addApplicationListenerBean (String name) { ApplicationListener listener = context.getBean(name, ApplicationListener.class); ResolvableType type = ResolvableType.forClass(listener.getClass()).getInterfaces()[0 ].getGeneric(); GenericApplicationListener genericApplicationListener = new GenericApplicationListener () { public boolean supportsEventType (ResolvableType eventType) { return type.isAssignableFrom(eventType); } public void onApplicationEvent (ApplicationEvent event) { executor.submit(() -> listener.onApplicationEvent(event)); } }; listeners.add(genericApplicationListener); } public void multicastEvent (ApplicationEvent event, ResolvableType eventType) { for (GenericApplicationListener listener : listeners) { if (listener.supportsEventType(ResolvableType.forClass(event.getClass()))) { listener.onApplicationEvent(event); } } } }; }
49.2 非 ApplicationEvent 事件
如果发送的事件不是 ApplicationEvent
类型时,Spring 会将其包装为 PayloadApplicationEvent
并用泛型技术解析事件对象的原始类型。
包装为 PayloadApplicationEvent
类型的逻辑无需实现,直接使用即可。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 interface Inter {} static class Bean1 implements Inter {} @Bean @SuppressWarnings("all") public ApplicationEventMulticaster applicationEventMulticaster (ConfigurableApplicationContext context, ThreadPoolTaskExecutor executor) { return new A49 .AbstractApplicationEventMulticaster() { private List<GenericApplicationListener> listeners = new ArrayList <>(); { listeners.add(new GenericApplicationListener () { @Override public void onApplicationEvent (ApplicationEvent event) { if (event instanceof PayloadApplicationEvent) { PayloadApplicationEvent<?> payloadApplicationEvent = (PayloadApplicationEvent<?>) event; System.out.println(payloadApplicationEvent.getPayload()); } } @Override public boolean supportsEventType (ResolvableType eventType) { System.out.println(eventType); return (Inter.class.isAssignableFrom(eventType.getGeneric().toClass())); } }); } @Override public void multicastEvent (ApplicationEvent event) { multicastEvent(event, null ); } @SuppressWarnings("all") @Override public void multicastEvent (ApplicationEvent event, ResolvableType eventType) { listeners.stream().filter(applicationListener -> { if (eventType == null ) { return false ; } if (applicationListener instanceof GenericApplicationListener) { GenericApplicationListener listener = (GenericApplicationListener) applicationListener; return listener.supportsEventType(eventType); } return false ; }).forEach(listener -> listener.onApplicationEvent(event)); } }; }
1 2 3 4 5 6 7 8 9 10 11 @Configuration public class TestEventPublisher { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (TestEventPublisher.class); context.publishEvent(new Object ()); context.publishEvent("aaaa" ); context.publishEvent(new Bean1 ()); } }
运行 main()方法后,控制台打印出:
org.springframework.context.PayloadApplicationEvent
org.springframework.context.PayloadApplicationEvent
org.springframework.context.PayloadApplicationEvent
com.itheima.a49.TestEventPublisher$Bean1@7ba18f1b
49.3 【补充】Spring 内置事件
Spring 的内置事件有很多,在此列举几个与 Spring 容器启动相关的事件,如果需要在 Spring 容器启动的某个时刻进行一些操作,就可以监听这些事件:
事件类型
触发时机
ContextRefreshedEvent
在调用 ConfigurableApplicationContext
接口中的 refresh()
方法时触发
ContextStartedEvent
在调用 ConfigurableApplicationContext
的 start()
方法时触发
ContextStoppedEvent
在调用 ConfigurableApplicationContext
的 stop()
方法时触发
ContextClosedEvent
当 ApplicationContext
被关闭时触发该事件,也就是调用 close()
方法触发