因为Spring源码这个东西实在是太难学了,学了忘,忘了学,反反复复,还有很多晦涩难懂的东西,甚至现在我感觉仍然是入门,只能说是把每节课学到的东西给记录一下,将来能多次回顾。笔记参考视频-黑马程序员
这几篇老文章都是我之前用飞书云文档写的,因转换格式问题,本文之前已写完的文档图片太多了,不想一一转化了,大家可以访问我的飞书云文档来查看这些图片–https://miu7shl031o.feishu.cn/drive/folder/WJiBfqX9klvKNEdpTHccfrEQn3e
容器接口 以 SpringBoot 的启动类为例:
@Slf4j @SpringBootApplication public class A01Application { public static void main (String[] args) { SpringApplication.run(A01Application.class, args); } }
其中的 run() 方法是有返回值的:
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
ConfigurableApplicationContext
ConfigurableApplicationContext 接口继承了 ApplicationContext 接口,而 ApplicationContext 接口又间接地继承了 BeanFactory 接口,除此之外还继承了其他很多接口,相当于对 BeanFactory 进行了拓展。
1.1 BeanFactory介绍 它是 ApplicationContext 的父接口 它才是 Spring 的核心容器,主要的 ApplicationContext 实现组合了它的功能,也就是说,BeanFactory 是 ApplicationContext 中的一个成员变量。 常用的 context.getBean(“xxx”) 方法,其实是调用了 BeanFactory 的 getBean() 方法。 1.2 BeanFactory 作用 IDEA中查看BeanFactory的相关方法
通过这些方法定义可知,BeanFactory 表面上只有 getBean() 方法,但实际上 Spring 中的控制反转、基本的依赖注入、乃至 Bean 的生命周期中的各个功能都是由它的实现类提供。
1.3DefaultListableBeanFactory 1.3.1DefaultListableBeanFactory简单介绍 DefaultListableBeanFactory 实现了 BeanFactory 接口,它能管理 Spring 中所有的 Bean,当然也包含 Spring 容器中的那些单例对象。 DefaultListableBeanFactory 还继承了DefaultSingletonBeanRegistry 类,这个类就是用来管理 Spring 容器中的单例对象。 1.3.2源码分析 进入DefaultSingletonBeanRegistry它有一个 Map 类型的成员变量 singletonObjects
Map 的 key 就是 Bean 的名字,而 value 是对应的 Bean,即单例对象。
private final Map<String, Object> singletonObjects = new ConcurrentHashMap <>(256 );
演示
现有如下两个 Bean:
@Component public class Component1 {} @Component public class Component2 {}
编写程序查看 singletonObjects 中是否存在这两个 Bean 的信息:
@Slf4j @SpringBootApplication public class A01Application { @SneakyThrows @SuppressWarnings("unchecked") public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args); Field singletonObjects = DefaultSingletonBeanRegistry.class.getDeclaredField("singletonObjects" ); singletonObjects.setAccessible(true ); ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); Map<String, Object> map = (Map<String, Object>) singletonObjects.get(beanFactory); map.entrySet().stream().filter(e -> e.getKey().startsWith("component" )) .forEach(e -> System.out.println(e.getKey() + "=" + e.getValue())); context.close(); } }
输出结果:
component1=indi.lcp.bean.a01.Component1@25a5c7db component2=indi.lcp.bean.a01.Component2@4d27d9d
1.4ApplicationContext 附一张ConfigurableApplicationContext的继承类图
可以看出ApplicationContext 除了继承 BeanFactory 外,还继承了:
MessageSource:使其具备处理国际化资源的能力 ResourcePatternResolver:使其具备使用通配符进行资源匹配的能力 EnvironmentCapable:使其具备读取 Spring 环境信息、配置文件信息的能力 ApplicationEventPublisher:使其具备发布事件的能力 依次深入看一下这四个类
1.4.1MessageSource 在 SpringBoot 项目的 resources 目录下创建 messages.properties、messages_en.properties、messages_zh_CN.properties、messages_zh_TW.properties 四个国际化文件,除 messages.properties 外,其余三个文件内容如下:
lcp=Li congpu lcp=李从浦 lcp=李從浦
测试
@SneakyThrows @SuppressWarnings("unchecked") 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)); System.out.println(context.getMessage("thanks" , null , Locale.TRADITIONAL_CHINESE)); context.close(); }
控制台打印出:
1.4.2ResourcePatternResolver @SneakyThrows @SuppressWarnings("unchecked") public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args); Resource[] resources = context.getResources("classpath:application.properties" ); Assert.isTrue(resources.length > 0 , "加载类路径下的 application.properties 文件失败" ); resources = context.getResources("classpath*:META-INF/spring.factories" ); Assert.isTrue(resources.length > 0 , "加载类路径下的 META-INF/spring.factories 文件失败" ); context.close(); }
1.4.3EnvironmentCapable @SneakyThrows @SuppressWarnings("unchecked") public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args); System.out.println(context.getEnvironment().getProperty("java_home" )); System.out.println(context.getEnvironment().getProperty("properties.name" )); context.close(); }
java_home 是从环境变量中读取,properties.name 则是从 application.yml 配置文件中读取。
1.4.4ApplicationEventPublisher 定义事件类 UserRegisteredEvent:
public class UserRegisteredEvent extends ApplicationEvent { private static final long serialVersionUID = 6319117283222183184L ; public UserRegisteredEvent (Object source) { super (source); } }
将 Component1 作为发送事件的 Bean:
@Slf4j @Component public class Component1 { @Autowired private ApplicationEventPublisher context; public void register () { log.debug("事件" ); context.publishEvent(new UserRegisteredEvent (this )); } }
将 Component2 作为事件监听器:
@Slf4j @Component public class Component2 { @EventListener public void aaa (UserRegisteredEvent event) { log.debug("{}" , event); log.debug("监听到事件" ); } }
在 main() 方法中使用 Component1 发送事件:
@SneakyThrows @SuppressWarnings("unchecked") public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args); context.getBean(Component1.class).register(); context.close(); }
控制台输出
indi.lcp.bean.a01.Component1 - 事件 indi.lcp.bean.a01.Component2 - indi.lcp.bean.a01.UserRegisteredEvent[source=indi.lcp.bean.a01.Component1@25a5c7db] indi.lcp.bean.a01.Component2 - 监听到事件
容器实现 2.1 BeanFactory 的实现 有如下类,尝试将 Config 添加到 Bean 工厂中:
@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()" ); } }
需要使用到 BeanFactory 的一个实现类: DefaultListableBeanFactory。有了 Bean 工厂,还需要定义 Bean,之后再把定义的 Bean 注册到工厂即可。
public static void main (String[] args) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory (); AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(Config.class) .setScope("singleton" ) .getBeanDefinition(); beanFactory.registerBeanDefinition("config" , beanDefinition); Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println); } config
现在 Bean 工厂中 有且仅有一个 名为 config 的 Bean。
根据对 @Configuration 和 @Bean 两个注解的认识可知,Bean 工厂中应该还存在 bean1 和 bean2,那为什么现在没有呢?
很明显是现在的 BeanFactory 缺少了解析 @Configuration 和 @Bean 两个注解的能力。
public static void main (String[] args) { AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory); Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println); }
根据打印出的信息,可以看到有一个名为 org.springframework.context.annotation.internalConfigurationAnnotationProcessor 的 Bean,根据其所含的 ConfigurationAnnotationProcessor 字样,可以知道这个 Bean 就是用来处理 @Configuration 和 @Bean 注解的,将配置类中定义的 Bean 信息补充到 BeanFactory 中。
那为什么在 Bean 工厂中依旧没有 bean1 和 bean2 呢?
现在仅仅是将处理器添加到了 Bean 工厂,还没有使用处理器。
使用处理器很简单,先获取到处理器,然后再使用即可。像internalConfigurationAnnotationProcessor 这样的 Bean,都有一个共同的类型,名为 BeanFactoryPostProcessor,因此可以:
public static void main (String[] args) { beanFactory.getBeansOfType(BeanFactoryPostProcessor.class).values().forEach(i -> i.postProcessBeanFactory(beanFactory)); Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println); } 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
bean1 和 bean2 已经被添加到 Bean 工厂中,尝试获取 bean1 中的 bean2,查看 bean2 是否成功注入到 bean1 中:
public static void main (String[] args) { System.out.println(beanFactory.getBean(Bean1.class).getBean2()); } null
bean2 没有成功被注入到 bean1 中。
在先前添加到 BeanFactory 中的后置处理器里,有名为 internalAutowiredAnnotationProcessor 和 internalCommonAnnotationProcessor 的两个后置处理器。前者用于解析 @Autowired 注解,后者用于解析 @Resource 注解,它们都有一个共同的类型 BeanPostProcessor,因此可以:
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()); } ---------------------------------------- [main] DEBUG indi.lcp.bean.a02.TestBeanFactory$Bean1 - 构造 Bean1() [main] DEBUG indi.lcp.bean.a02.TestBeanFactory$Bean2 - 构造 Bean2() indi.lcp.bean.a02.TestBeanFactory$Bean2@6ee12bac
建立 BeanPostProcessor 和 BeanFactory 的关系后,bean2 被成功注入到 bean1 中了。
除此之外还可以发现:当需要使用 Bean 时,Bean 才会被创建,即按需加载。那有没有什么办法预先就初始化好单例对象呢?
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()); } [main] DEBUG indi.lcp.bean.a02.TestBeanFactory$Bean1 - 构造 Bean1() [main] DEBUG indi.lcp.bean.a02.TestBeanFactory$Bean2 - 构造 Bean2() ---------------------------------------- indi.lcp.bean.a02.TestBeanFactory$Bean2@6ee12bac
学到了什么->BeanFactory 不会做的事 :
主动调用 BeanFactory 后置处理器;
主动添加 Bean 后置处理器;
主动初始化单例对象;
解析 ${} 和 #{}
扩展:后置处理器的排序
在最初给出的类信息中进行补充:
@Configuration static class Config { @Bean public Bean3 bean3 () { return new Bean3 (); } @Bean public Bean4 bean4 () { return new Bean4 (); } } interface Inter {} @Slf4j static class Bean3 implements Inter { public Bean3 () { log.debug("构造 Bean3()" ); } } @Slf4j static class Bean4 implements Inter { public Bean4 () { log.debug("构造 Bean4()" ); } } @Slf4j static class Bean1 { @Autowired @Resource(name = "bean4") private Inter bean3; private Inter getInter () { return bean3; } }
向 Bean 工厂中添加了 bean3 和 bean4,并且计划在 bean1 中注入 Inter 类型的 Bean。
现在 Bean 工厂中 Inter 类型的 Bean 有两个,分别是 bean3、bean4,那么会注入哪一个呢?
如果只使用 @Autowired,首先会按照类型注入,如果同种类型的 Bean 有多个,再按照变量名称注入,如果再注入失败,就报错;如果只使用 @Resource,也会采取与 @Autowired 一样的注入策略,只不过 @Resource 注解还可以指定需要注入 Bean 的 id(使用 name 属性进行指定),如果指定了需要注入 Bean 的 id,就直接按照指定的 id 进行注入,如果失败就报错。
那如果即使用 @Autowired 又使用 @Resource(name = “bean4”) 呢?
public static void main (String[] args) { System.out.println(beanFactory.getBean(Bean1.class).getInter()); } indi.lcp.bean.a02.TestBeanFactory$Bean3@8e0379d
根据打印的结果可知,@Autowired 先生效了,这是因为 internalAutowiredAnnotationProcessor 排在 internalCommonAnnotationProcessor 之前。可以查看它们的先后关系
public static void main (String[] args) { beanFactory.getBeansOfType(BeanPostProcessor.class).values().forEach(i -> { System.out.println(">>>> " + i); beanFactory.addBeanPostProcessor(i); }); }
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@6385cb26 org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@38364841
也可以改变它们的顺序,然后再查看注入的是 bean3 还是 bean4:
public static void main (String[] args) { beanFactory.getBeansOfType(BeanPostProcessor.class).values().stream() .sorted(Objects.requireNonNull(beanFactory.getDependencyComparator())) .forEach(i -> { System.out.println(">>>> " + i); beanFactory.addBeanPostProcessor(i); }); System.out.println(beanFactory.getBean(Bean1.class).getInter()); } >>>> org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@6385cb26 >>>> org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@38364841 indi.lcp.bean.a02.TestBeanFactory$Bean4@52e677af
改变 BeanPostProcessor 的先后顺序后,@Resource(name = “bean4”) 生效了,成功注入了 bean4。
为什么使用 beanFactory.getDependencyComparator() 后就改变了 BeanPostProcessor 的先后顺序呢?
在调用的 AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory); 方法源码中有:
public static void registerAnnotationConfigProcessors (BeanDefinitionRegistry registry) { registerAnnotationConfigProcessors(registry, null ); } 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 ()); } } }
设置的 AnnotationAwareOrderComparator 比较器会根据设置的 order 信息进行比较。
AutowiredAnnotationBeanPostProcessor 设置的 order 是:
private int order = Ordered.LOWEST_PRECEDENCE - 2 ;
CommonAnnotationBeanPostProcessor 设置的 order 是:
public CommonAnnotationBeanPostProcessor () { setOrder(Ordered.LOWEST_PRECEDENCE - 3 ); }
值越小,优先级越大,就排在更前面,因此当设置了 AnnotationAwareOrderComparator 比较器后,CommonAnnotationBeanPostProcessor 排在更前面,@Resource 就先生效。
2.2 ApplicationContext 的实现 @Slf4j public class A02Application { static class Bean1 { } static class Bean2 { @Getter @Setter private Bean1 bean1; } }
2.2.1 ClassPathXmlApplicationContext 较为经典的容器,基于 classpath 下的 xml 格式的配置文件来创建。
<?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 ="indi.lcp.bean.a02.A02Application.Bean1" /> <bean id ="bean2" class ="indi.lcp.bean.a02.A02Application.Bean2" > <property name ="bean1" ref ="bean1" /> </bean > </beans > private static void testClassPathXmlApplicationContext() { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("b01.xml"); Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println); System.out.println(context.getBean(Bean2.class).getBean1()); } bean1 bean2 indi.lcp.bean.a02.A02Application$Bean1@2db7a79b
2.2.2 FileSystemXmlApplicationContext 与 ClassPathXmlApplicationContext 相比,FileSystemXmlApplicationContext 是基于磁盘路径下 xml 格式的配置文件来创建。
private static void testFileSystemXmlApplicationContext () { FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext ("bean\\src\\main\\resources\\b01.xml" ); Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println); System.out.println(context.getBean(Bean2.class).getBean1()); } bean1 bean2 indi.lcp.bean.a02.A02Application$Bean1@2db7a79b
ClassPathXmlApplicationContext 和 FileSystemXmlApplicationContext 都依赖于从 XML 文件中读取 Bean 的信息,而这都利用了 XmlBeanDefinitionReader 进行读取。
private static void testXmlBeanDefinitionReader () { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory (); System.out.println("读取之前..." ); Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println); System.out.println("读取之后..." ); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader (beanFactory); reader.loadBeanDefinitions(new FileSystemResource ("bean\\src\\main\\resources\\b01.xml" )); Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println); } 读取之前... 读取之后... bean1 bean2
2.2.3 AnnotationConfigApplicationContext 基于 Java 配置类来创建。首先定义配置类:
@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; } } private static void testAnnotationConfigApplicationContext () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (Config.class); Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println); System.out.println(context.getBean(Bean2.class).getBean1()); } 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 indi.lcp.bean.a02.A02Application$Bean1@1f0f1111
与前面两种基于 XML 创建 ApplicationContext 的方式相比,使用 AnnotationConfigApplicationContext 后,使得容器中多了一些后置处理器相关的 Bean。
如果要在先前的两种方式中也添加上这些 Bean,可以在 XML 进行配置:
<?xml version="1.0" encoding="UTF-8" ?> <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 ="indi.lcp.bean.a02.A02Application.Bean1" /> <bean id ="bean2" class ="indi.lcp.bean.a02.A02Application.Bean2" > <property name ="bean1" ref ="bean1" /> </bean > <context:annotation-config /> </beans >
2.2.4 AnnotationConfigServletWebServerApplicationContext 基于 Java 配置类来创建,用于 web 环境。首先定义配置类:
@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 ; }; } } 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 可以通过下列两种类完成:
该接口中对 loadBeanDefinitions() 方法进行了多种重载,支持传入一个或多个 Resource 对象、资源位置来加载 BeanDefinition。
它有一系列相关实现,比如:
XmlBeanDefinitionReader:通过读取 XML 文件来加载; PropertiesBeanDefinitionReader:通过读取 properties 文件来加载,此类已经被 @Deprecated 注解标记; 除此之外,还有一个 AnnotatedBeanDefinitionReader,尽管它并不是 BeanDefinition 的子类,但它们俩长得很像,根据其类注释可知:它能够通过编程的方式对 Bean 进行注册,是 ClassPathBeanDefinitionScanner 的替代方案,能读取通过注解定义的 Bean。
ClassPathBeanDefinitionScanner 通过扫描指定包路径下的 @Component 及其派生注解来注册 Bean,是 @ComponentScan 注解的底层实现。
比如 MyBatis 通过继承 ClassPathBeanDefinitionScanner 实现通过 @MapperScan 注解来扫描指定包下的 Mapper 接口。
AnnotatedBeanDefinitionReader 和 ClassPathBeanDefinitionScanner 中都有一个 BeanDefinitionRegistry 类型的成员变量,它是一个接口,提供了 BeanDefinition 的增加、删除和查找功能。
根据前面的补充,现在可以这样注册并获取 Bean:
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 接口体系 public interface ApplicationContext extends EnvironmentCapable , ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver { }
ApplicationContext 接口继承了许多接口,其中:
EnvironmentCapable:提供获取 Environment 的能力 ListableBeanFactory:提供了获取某种类型的 Bean 集合的能力 HierarchicalBeanFactory:提供了获取父容器的能力 MessageSource:提供了对国际化信息进行处理的能力 ApplicationEventPublisher:提供了事件发布能力 ResourcePatternResolver:提供了通过通配符获取多个资源的能力 虽然 ApplicationContext 继承了很多接口,但这些能力的实现是通过一种委派(Delegate)的方式实现的,这种方式也被叫做委派模式,但它并不属于 GoF 的 23 种设计模式中的一种,是一种面向对象的设计模式。什么是委派呢?
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()。
Bean 的生命周期 自定义一个 SpringBoot 的主启动类:
@SpringBootApplication public class A03Application { public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A03Application.class, args); context.close(); } }
在启动类所在路径下再定义一个类,使其能够被自动装配:
@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("销毁" ); } } indi.lcp.bean.a03.LifeCycleBean : 构造 indi.lcp.bean.a03.LifeCycleBean : 依赖注入: D:\environment\JDK1.8 indi.lcp.bean.a03.LifeCycleBean : 初始化 indi.lcp.bean.a03.LifeCycleBean : 销毁
除此之外,Spring 还提供了一些对 Bean 生命周期的各个阶段进行拓展的 BeanPostProcessor,比如 InstantiationAwareBeanPostProcessor 和 DestructionAwareBeanPostProcessor。
实现这两个接口,并使用 @Component 注解标记实现类:
@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; } }
再运行主启动类,查看控制台的日志信息(只列举主要信息):
indi.lcp.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 实例化之前执行,这里返回的对象会替换掉原本的 bean indi.lcp.bean.a03.LifeCycleBean : 构造 indi.lcp.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 实例化之后执行,如果返回 false 会跳过依赖注入节点 indi.lcp.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 依赖注入阶段执行,如 @Autowired 、@Value 、@Resource indi.lcp.bean.a03.LifeCycleBean : 依赖注入: D:\environment\JDK1.8 indi.lcp.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 初始化执行之前,这里返回的对象会替换掉原本的 bean,如 @PostConstruct 、@ConfigurationProperties indi.lcp.bean.a03.LifeCycleBean : 初始化 indi.lcp.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 初始化之后执行,这里返回的对象会替换掉原本的 bean,如代理增强 indi.lcp.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 销毁执行之前,如 @PreDestroy indi.lcp.bean.a03.LifeCycleBean : 销毁
为什么实现了 BeanPostProcessor 接口后就能够在 Bean 生命周期的各个阶段进行拓展呢?
这使用了模板方法设计模式。
如下代码,模拟 BeanFactory 构造 Bean:
static class MyBeanFactory { public Object getBean () { Object bean = new Object (); System.out.println("构造 " + bean); System.out.println("依赖注入 " + bean); System.out.println("初始化 " + bean); return bean; } }
假设现在需要在依赖注入之后,初始化之前进行其他的操作,那首先能想到的就是在这个位置直接书写相关操作的代码,但这会使代码更加臃肿、增加耦合性,显然不是一种好方式。
可以定义一个接口:
interface BeanPostProcessor { void inject (Object bean) ; }
然后对 MyBeanFactory 进行修改:
static class MyBeanFactory { 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; } private List<BeanPostProcessor> processors = new ArrayList <>(); public void addProcessor (BeanPostProcessor processor) { processors.add(processor); } }
之后如果需要拓展,调用 MyBeanFactory 实例的 addProcessor() 方法添加拓展逻辑即可:
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
Bean的生命周期
Bean 后置处理器 4.1 常见的 Bean 后置处理器 现有如下三个类:
@Slf4j @ToString 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 生效" ); } } public class Bean2 {} public class Bean3 {}
Bean2 和 Bean3 很简单,而在 Bean1 中使用了多个注解以实现 Bean 注入和值注入。
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 后置处理器相关的 Bean,使得 Bean1 中使用的注解能够生效。
public static void main (String[] args) { context.registerBean("bean3" , Bean3.class); context.getDefaultListableBeanFactory().setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver ()); context.registerBean(AutowiredAnnotationBeanPostProcessor.class); context.refresh(); } indi.lcp.bean.a04.Bean1 - @Autowired 生效: indi.lcp.bean.a04.Bean2@29b5cd00 indi.lcp.bean.a04.Bean1 - @Value 生效: D:\environment\JDK1.8
@Autowired 和 @Value 注解成功生效,但 @Resource、@PostConstruct 和 @PreDestroy 依旧没有生效,因此还需要添加解析它们的 Bean 后置处理器。
public static void main (String[] args) { context.registerBean(CommonAnnotationBeanPostProcessor.class); } indi.lcp.bean.a04.Bean1 - @Resource 生效: indi.lcp.bean.a04.Bean3@12cdcf4 indi.lcp.bean.a04.Bean1 - @Autowired 生效: indi.lcp.bean.a04.Bean2@6121c9d6 indi.lcp.bean.a04.Bean1 - @Value 生效: D:\environment\JDK1.8 indi.lcp.bean.a04.Bean1 - @PostConstruct 生效 INFO indi.lcp.bean.a04.Bean1 - @PreDestroy 生效
解析 @ConfigurationProperties
使用 @ConfigurationProperties 可以指定配置信息的前缀,使得配置信息的读取更加简单。比如:
@Getter @Setter @ToString @ConfigurationProperties(prefix = "java") public class Bean4 { private String home; private String version; }
上述代码用于获取环境变量中 java.home 和 java.version 的信息。
对先前的 main() 方法进行补充:
public static void main (String[] args) { context.registerBean("bean4" , Bean4.class); System.out.println(context.getBean(Bean4.class)); }
Bean4 成功添加到容器中,但值注入失败了,显然也是因为缺少解析 @ConfigurationProperties 注解的后置处理器。
public static void main (String[] args) { context.registerBean("bean4" , Bean4.class); ConfigurationPropertiesBindingPostProcessor.register(context.getDefaultListableBeanFactory()); System.out.println(context.getBean(Bean4.class)); } Bean4(home=D:\environment\JDK1.8 \jre, version=1.8 .0_251 )
4.2 AutowiredAnnotationBeanPostProcessor 通过前文可知 AutowiredAnnotationBeanPostProcessor 用于解析 @Autowired 和 @Value 注解,那它究竟是怎么工作的呢?
public static void main (String[] args) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory (); beanFactory.registerSingleton("bean2" , new Bean2 ()); beanFactory.registerSingleton("bean3" , new Bean3 ()); beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver ()); AutowiredAnnotationBeanPostProcessor postProcessor = new AutowiredAnnotationBeanPostProcessor (); postProcessor.setBeanFactory(beanFactory); Bean1 bean1 = new Bean1 (); System.out.println(bean1); postProcessor.postProcessProperties(null , bean1, "bean1" ); System.out.println(bean1); } Bean1(bean2=null , bean3=null , home=null ) 21 :31 :27.409 [main] INFO indi.lcp.bean.a04.Bean1 - @Value 生效: ${JAVA_HOME}21 :31 :27.419 [main] INFO indi.lcp.bean.a04.Bean1 - @Autowired 生效: indi.lcp.bean.a04.Bean2@5bcab519Bean1 (bean2=indi.lcp.bean.a04.Bean2@5bcab519, bean3=null , home=${JAVA_HOME})
在未调用 AutowiredAnnotationBeanPostProcessor#postProcessProperties() 方法时,Bean1 中的 bean2、bean3 和 home 都没有注入成功,而在调用之后,成功注入了 bean2 和 home,但 home 的值似乎有点奇怪,并没有打印出前文中相同的值,可能是因为没有成功解析 #{}?
至于 bean3 为什么没注入成功,是因为 bean3
的注入是利用 @Resource,而不是 @Autowired。如果对 Bean1 进行修改:
public class Bean1 { @Autowired private Bean3 bean3; @Resource public void setBean3 (Bean3 bean3) { log.info("@Resource 生效: {}" , bean3); this .bean3 = bean3; } } Bean1(bean2=null , bean3=null , home=null ) 21 :36 :36.402 [main] INFO indi.lcp.bean.a04.Bean1 - @Value 生效: ${JAVA_HOME}21 :36 :36.406 [main] INFO indi.lcp.bean.a04.Bean1 - @Autowired 生效: indi.lcp.bean.a04.Bean2@490ab905Bean1 (bean2=indi.lcp.bean.a04.Bean2@490ab905, bean3=indi.lcp.bean.a04.Bean3@56ac3a89, home=${JAVA_HOME})
成功注入了 bean3。如果想要成功注入 home,则需要在 BeanFactory 中添加 #{} 的解析器:
public static void main (String[] args) { beanFactory.addEmbeddedValueResolver(new StandardEnvironment ()::resolvePlaceholders); postProcessor.postProcessProperties(null , bean1, "bean1" ); System.out.println(bean1); } Bean1(bean2=null , bean3=null , home=null ) indi.lcp.bean.a04.Bean1 - @Value 生效: D:\environment\JDK1.8 indi.lcp.bean.a04.Bean1 - @Autowired 生效: indi.lcp.bean.a04.Bean2@4fe3c938 Bean1 (bean2=indi.lcp.bean.a04.Bean2@4fe3c938, bean3=indi.lcp.bean.a04.Bean3@5383967b, home=D:\environment\JDK1.8 )
AutowiredAnnotationBeanPostProcessor#postProcessProperties()
源码如下:
@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() 用于查找指定的 bean 对象中哪些地方使用了 @Autowired、@Value 等与注入相关的注解,并将这些信息封装在 InjectionMetadata 对象中,之后调用其 inject() 方法利用反射完成注入。
findAutowiringMetadata() 方法是一个私有方法,尝试利用反射进行调用并进行断点查看 InjectionMetadata 对象中的信息:
@SneakyThrows public static void main (String[] args) { AutowiredAnnotationBeanPostProcessor postProcessor = new AutowiredAnnotationBeanPostProcessor (); postProcessor.setBeanFactory(beanFactory); Bean1 bean1 = new Bean1 (); Method method = AutowiredAnnotationBeanPostProcessor.class.getDeclaredMethod("findAutowiringMetadata" , String.class, Class.class, PropertyValues.class); method.setAccessible(true ); InjectionMetadata metadata = (InjectionMetadata) method.invoke(postProcessor, "bean1" , Bean1.class, null ); System.out.println(metadata); }
InjectionMetadata 中有一个名为 injectedElements 的集合类型成员变量,injectedElements 存储了被相关注解标记的成员变量、方法的信息,因为 Bean1 中的 bean3 成员变量、setBean2() ,setHome() 方法恰好被 @Autowired 注解标记。
然后按照源码一样,调用 InjectionMetadata#inject() 方法进行依赖注入:
@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); } indi.lcp.bean.a04.Bean1 - @Value 生效: D:\environment\JDK1.8 indi.lcp.bean.a04.Bean1 - @Autowired 生效: indi.lcp.bean.a04.Bean2@5383967b Bean1 (bean2=indi.lcp.bean.a04.Bean2@5383967b, bean3=indi.lcp.bean.a04.Bean3@2ac273d3, home=D:\environment\JDK1.8 )
调用 inject() 方法后会利用反射进行依赖注入,但在反射之前,肯定得先拿到被注入的对象或值,那这些对象或值是怎么取到的呢?
可以通过以下代码概括:
@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); } indi.lcp.bean.a04.Bean3@2ac273d3 indi.lcp.bean.a04.Bean2@192b07fd D:\environment\JDK1.8
BeanFactory 后置处理器 5.1 常见的 BeanFactory 后置处理器 先引入要用到的依赖:
<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 >
需要用到的类信息:
package indi.lcp.bean.a05;@Configuration @ComponentScan("indi.lcp.bean.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; } } package indi.lcp.bean.a05;@Slf4j public class Bean1 { public Bean1 () { System.out.println("我被 Spring 管理啦" ); } } package indi.lcp.bean.a05.component;@Slf4j @Component public class Bean2 { public Bean2 () { log.info("我被 Spring 管理啦" ); } }
继续使用 GenericApplicationContext 作为容器,向容器中注册 config:
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 信息,也就是说 Config 类中的 @ComponentScan 和 @Bean 注解都没有生效。
根据经验,显然是因为缺少某个后置处理器。
public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("config" , Config.class); context.registerBean(ConfigurationClassPostProcessor.class); } indi.lcp.bean.a05.component.Bean2 - 我被 Spring 管理啦 indi.lcp.bean.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 后置处理器。
以下两个接口被 @Mapper 注解标记:
package indi.lcp.bean.a05.mapper;@Mapper public interface Mapper1 {} @Mapper public interface Mapper2 {}
然后添加解析 @Mapper 注解的后置处理器:
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" , "indi.lcp.bean.a05.mapper" )); }
其中的 basePackage 是 MapperScannerConfigurer 中的一个成员变量,表示需要扫描的包路径,设置的值恰好是被 @Mapper 注解标记的接口所在的包路径。
控制台打印的信息中增加了:
5.2 模拟实现 移除向容器中添加的 ConfigurationClassPostProcessor 和 MapperScannerConfigurer 两个后置处理器,自行编码模拟它们功能的实现。
在 Bean2 所在包路径下再增加两个类,用于后续测试:
package indi.lcp.bean.a05.component;@Slf4j @Controller public class Bean3 { public Bean3 () { log.info("我被 Spring 管理啦" ); } } @Slf4j public class Bean4 { public Bean4 () { log.info("我被 Spring 管理啦" ); } }
编写 ComponentScanPostProcessor 用于实现 @ComponentScan 注解的解析:
public class ComponentScanPostProcessor implements BeanDefinitionRegistryPostProcessor { @Override @SneakyThrows public void postProcessBeanDefinitionRegistry (BeanDefinitionRegistry registry) throws BeansException { ComponentScan componentScan = AnnotationUtils.findAnnotation(Config.class, ComponentScan.class); if (componentScan != null ) { for (String packageName : componentScan.basePackages()) { System.out.println(packageName); String path = "classpath*:" + packageName.replace("." , "/" ) + "/**/**.class" ; Resource[] resources = new PathMatchingResourcePatternResolver ().getResources(path); CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory (); 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(); String name = generator.generateBeanName(beanDefinition, registry); registry.registerBeanDefinition(name, beanDefinition); } } } } } @Override public void postProcessBeanFactory (ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { } } public static void main (String[] args) { 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(); } indi.lcp.bean.a05.component indi.lcp.bean.a05.component.Bean2 : 我被 Spring 管理啦 indi.lcp.bean.a05.component.Bean3 : 我被 Spring 管理啦 config indi.lcp.bean.a05.ComponentScanPostProcessor bean2 bean3
没使用 ConfigurationClassPostProcessor 也实现了 @ComponentScan 注解的解析!
Config 类中再增加一个方法作为干扰项:
@Configuration @ComponentScan("indi.lcp.bean.a05.component") public class Config { public Bean2 bean2 () { return new Bean2 (); } }
与解析 @ComponentScan 一样,自行编写一个 BeanFactoryPostProcessor 的实现类用于解析 @Bean 注解:
public class AtBeanPostProcessor implements BeanDefinitionRegistryPostProcessor { @Override @SneakyThrows public void postProcessBeanDefinitionRegistry (BeanDefinitionRegistry registry) throws BeansException { CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory (); MetadataReader reader = factory.getMetadataReader(new ClassPathResource ("indi/lcp/bean/a05/Config.class" )); Set<MethodMetadata> methods = reader.getAnnotationMetadata().getAnnotatedMethods(Bean.class.getName()); for (MethodMetadata method : methods) { System.out.println(method); String initMethod = method.getAnnotationAttributes(Bean.class.getName()).get("initMethod" ).toString(); String methodName = method.getMethodName(); BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition() .setFactoryMethodOnBean(methodName, "config" ) .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); if (StringUtils.hasLength(initMethod)) { builder.setInitMethodName(initMethod); } AbstractBeanDefinition beanDefinition = builder.getBeanDefinition(); registry.registerBeanDefinition(methodName, beanDefinition); } } @Override public void postProcessBeanFactory (ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { } }
在构造 BeanDefinition 时调用了 setAutowireMode() 方法设置注入模式,这是因为在 Config
类中有一特殊的被 @Bean 标记的方法:
@Bean public SqlSessionFactoryBean sqlSessionFactoryBean (DataSource dataSource) { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean (); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean; }
接收一个 DataSource 类型的参数,需要将容器中这个类型的 Bean 进行注入,设置的 AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR 注入模式则能完成这个功能。
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(); } indi.lcp.bean.a05.Config.bean1() indi.lcp.bean.a05.Config.sqlSessionFactoryBean(javax.sql.DataSource) indi.lcp.bean.a05.Config.dataSource() indi.lcp.bean.a05.Bean1 : 我被 Spring 管理啦 com.alibaba.druid.pool.DruidDataSource : {dataSource-1 ,root} inited config indi.lcp.bean.a05.AtBeanPostProcessor bean1 sqlSessionFactoryBean dataSource
@Mapper 注解是在接口上使用的,但根据前文内容可知,@Mapper 被解析后在 Spring 容器中也存在与被标记的接口相关的 Bean。
难道 Spring 能管理接口?
那肯定是不行的,Spring 只能管理对象这是毋庸置疑的。那这些接口是怎么变成对象被 Spring 管理的呢?
这依赖于 MapperFactoryBean 将接口转换为对象。
在 Config 添加注册 Mapper1 和 Mapper2 的方法:
@Bean public MapperFactoryBean<Mapper1> mapper1 (SqlSessionFactory sqlSessionFactory) { MapperFactoryBean<Mapper1> factoryBean = new MapperFactoryBean <>(Mapper1.class); factoryBean.setSqlSessionFactory(sqlSessionFactory); return factoryBean; } @Bean public MapperFactoryBean<Mapper2> mapper2 (SqlSessionFactory sqlSessionFactory) { MapperFactoryBean<Mapper2> factoryBean = new MapperFactoryBean <>(Mapper2.class); factoryBean.setSqlSessionFactory(sqlSessionFactory); return factoryBean; }
再运行 main() 方法可以看到容器中存在名为 mapper1 和 mapper2 的 Bean。
这种方式虽然可以完成 Mapper 接口的注册,但每次只能单个注册,不能批量注册。
移除 Config 类中的 mapper1() 和 mapper2() 方法,自行编写 BeanDefinitionRegistryPostProcessor 接口的实现类完成 @Mapper 注解的解析:
public class MapperPostProcessor implements BeanDefinitionRegistryPostProcessor { @Override @SneakyThrows public void postProcessBeanDefinitionRegistry (BeanDefinitionRegistry registry) throws BeansException { PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver (); Resource[] resources = resolver.getResources("classpath:indi/lcp/bean/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 beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(MapperFactoryBean.class) .addConstructorArgValue(classMetadata.getClassName()) .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE) .getBeanDefinition(); AbstractBeanDefinition bd = BeanDefinitionBuilder.genericBeanDefinition(classMetadata.getClassName()) .getBeanDefinition(); String name = generator.generateBeanName(bd, registry); registry.registerBeanDefinition(name, beanDefinition); } } } @Override public void postProcessBeanFactory (ConfigurableListableBeanFactory beanFactory) throws BeansException { } } @SneakyThrows 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(); } indi.lcp.bean.a05.Config.bean1() indi.lcp.bean.a05.Config.sqlSessionFactoryBean(javax.sql.DataSource) indi.lcp.bean.a05.Config.dataSource() indi.lcp.bean.a05.Bean1 : 我被 Spring 管理啦 com.alibaba.druid.pool.DruidDataSource : {dataSource-1 ,root} inited config indi.lcp.bean.a05.AtBeanPostProcessor indi.lcp.bean.a05.MapperPostProcessor bean1 sqlSessionFactoryBean dataSource mapper1 mapper2
容器中存在 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 接口中抽象方法的参数:
@FunctionalInterface public interface BeanFactoryPostProcessor { void postProcessBeanFactory (ConfigurableListableBeanFactory beanFactory) throws BeansException; }
尝试使用 BeanFactoryPostProcessor 注册创建完成的 Bean:
@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("lcp" ); 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=lcp, bean2=null )
BeanDefinition 的名称数组中不包含 bean1,也没有输出任何与经过 Spring 生命周期相关的日志信息,容器中 bean1 里注入的 bean2 也是 null。这表明通过这种方式注册的 Bean 不会注册 BeanDefinition,也不会经过 Spring 生命周期。
Aware 接口 6.1. Aware 接口 Aware 接口用于注入一些与容器相关的信息,比如:
BeanNameAware 注入 Bean 的名字 BeanFactoryAware 注入 BeanFactory 容器 ApplicationContextAware 注入 ApplicationContext 容器 EmbeddedValueResolverAware 解析 ${} @Slf4j public class MyBean implements BeanNameAware , ApplicationContextAware { @Override public void setBeanName (String name) { log.info("当前 Bean: " + this + "名字叫: " + name); } @Override public void setApplicationContext (ApplicationContext applicationContext) throws BeansException { log.info("当前 Bean: " + this + "容器是: " + applicationContext); } } public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("myBean" , MyBean.class); context.refresh(); context.close(); } 当前 Bean: indi.lcp.bean.a06.MyBean@16f7c8c1名字叫: myBean 当前 Bean: indi.lcp.bean.a06.MyBean@16f7c8c1容器是: org.springframework.context.support.GenericApplicationContext@2669b199
6.2. InitializingBean @Slf4j public class MyBean implements BeanNameAware , ApplicationContextAware, InitializingBean { @Override public void afterPropertiesSet () throws Exception { log.info("当前 Bean: " + this + " 初始化" ); } }
再次运行 main() 方法有:
当前 Bean: indi.lcp.bean.a06.MyBean@16f7c8c1名字叫: myBean 当前 Bean: indi.lcp.bean.a06.MyBean@16f7c8c1容器是: org.springframework.context.support.GenericApplicationContext@2669b199 当前 Bean: indi.lcp.bean.a06.MyBean@df27fae 初始化
当同时实现 Aware 接口和 InitializingBean 接口时,会先执行 Aware 接口。
BeanFactoryAware 、ApplicationContextAware 和 EmbeddedValueResolverAware 三个接口的功能可以使用 @Autowired 注解实现,InitializingBean 接口的功能也可以使用 @PostConstruct 注解实现,为什么还要使用接口呢?
@Autowired 和 @PostConstruct 注解的解析需要使用 Bean 后置处理器,属于拓展功能,而这些接口属于内置功能,不加任何拓展 Spring 就能识别。在某些情况下,拓展功能会失效,而内容功能不会失效。
@Slf4j public class MyBean implements BeanNameAware , ApplicationContextAware, InitializingBean { @Autowired public void setApplicationContextWithAutowired (ApplicationContext applicationContext) { log.info("当前 Bean: " + this + " 使用 @Autowired 注解,容器是: " + applicationContext); } @PostConstruct public void init () { log.info("当前 Bean: " + this + " 使用 @PostConstruct 注解初始化" ); } }
再运行 main() 方法会发现使用的注解没有被成功解析,原因很简单,GenericApplicationContext 是一个干净的容器,其内部没有用于解析这些注解的后置处理器。如果想要这些注解生效,则需要像前文一样添加必要的后置处理器:
context.registerBean(AutowiredAnnotationBeanPostProcessor.class); context.registerBean(CommonAnnotationBeanPostProcessor.class);
6.3. 失效的 @Autowired 注解 在某些情况下,尽管容器中存在必要的后置处理器,但 @Autowired 和 @PostConstruct 注解也会失效。
@Slf4j @Configuration public class MyConfig1 { @Autowired public void setApplicationContext (ApplicationContext applicationContext) { log.info("注入 ApplicationContext" ); } @PostConstruct public void init () { log.info("初始化" ); } } 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(); } indi.lcp.bean.a06.MyConfig1 : 注入 ApplicationContext indi.lcp.bean.a06.MyConfig1 : 初始化
@Autowired 和 @PostConstruct 注解成功被解析。
如果再对 Config1 进行一点小小的修改呢?
@Slf4j @Configuration public class MyConfig1 { @Bean public BeanFactoryPostProcessor processor1 () { return processor -> log.info("执行 processor1" ); } }
在 Config1 中添加了一个被 @Bean 注解标记的 processor1() 方法,用于向容器中添加 BeanFactoryPostProcessor。
如果再运行 main() 方法:
indi.lcp.bean.a06.MyConfig1 : 执行 processor1
processor1() 方法成功生效,但 @Autowired 和 @PostConstruct 注解的解析失败了。
对于 context.refresh(); 方法来说,它主要按照以下顺序干了三件事:
执行 BeanFactory 后置处理器; 添加 Bean 后置处理器; 创建和初始化单例对象。 比如当 Java 配置类不包括 BeanFactoryPostProcessor 时:
BeanFactoryPostProcessor 会在 Java 配置类初始化之前执行。
当 Java 配置类中定义了BeanFactoryPostProcessor 时,如果要创建配置类中的 BeanFactoryPostProcessor 就必须 提前 创建和初始化 Java 配置类。
在创建和初始化 Java 配置类时,由于 BeanPostProcessor 还未准备好,无法解析配置类中的 @Autowired 等注解,导致 @Autowired 等注解失效:
要解决这个问题也很简单,使用相关接口的功能实现注入和初始化:
@Slf4j @Configuration public class MyConfig2 implements InitializingBean , ApplicationContextAware { @Override public void afterPropertiesSet () throws Exception { log.info("初始化" ); } @Override public void setApplicationContext (ApplicationContext applicationContext) throws BeansException { log.info("注入 ApplicationContext" ); } @Bean public BeanFactoryPostProcessor processor2 () { return processor -> log.info("执行 processor2" ); } }
修改下 main() 方法:
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(); } indi.lcp.bean.a06.MyConfig2 : 注入 ApplicationContext indi.lcp.bean.a06.MyConfig2 : 初始化 indi.lcp.bean.a06.MyConfig2 : 执行 processor2
总结 Aware 接口提供了一种 内置 的注入手段,可以注入 BeanFactory、ApplicationContext;
InitializingBean 接口提供了一种 内置 的初始化手段;
内置的注入和初始化不受拓展功能的影响,总会被执行,因此 Spring 框架内部的类总是使用这些接口。
初始化与销毁 初始化和销毁 Bean 的实现有三种:
依赖于后置处理器提供的拓展功能 相关接口的功能 使用 @Bean 注解中的属性进行指定 当同时存在以上三种方式时,它们的执行顺序也将按照上述顺序进行执行。
包含三种初始化方式的 Bean:
@Slf4j public class Bean1 implements InitializingBean { @PostConstruct public void init () { log.info("初始化1" ); } @Override public void afterPropertiesSet () throws Exception { log.info("初始化2" ); } public void init3 () { log.info("初始化3" ); } }
包含三种销毁方式的 Bean:
@Slf4j public class Bean2 implements DisposableBean { @PreDestroy public void destroy1 () { log.info("销毁1" ); } @Override public void destroy () throws Exception { log.info("销毁2" ); } public void destroy3 () { log.info("销毁3" ); } }
测试:
@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 (); } } indi.lcp.bean.a07.Bean1 : 初始化1 indi.lcp.bean.a07.Bean1 : 初始化2 indi.lcp.bean.a07.Bean1 : 初始化3 indi.lcp.bean.a07.Bean2 : 销毁1 indi.lcp.bean.a07.Bean2 : 销毁2 indi.lcp.bean.a07.Bean2 : 销毁3
Scope 8.1 Scope 的类型与销毁 Scope 用于指定 Bean 的作用范围,有如下五个取值:
singleton:单例(默认值)。容器启动时创建(未设置延迟),容器关闭时销毁 prototype:多例。每次使用时创建,不会自动销毁,需要调用 DefaultListableBeanFactory#destroyBean() 进行销毁 request:作用于 Web 应用的请求范围。每次请求用到此 Bean 时创建,请求结束时销毁 session:作用于 Web 应用的会话范围。每个会话用到此 Bean 时创建,会话结束时销毁 application:作用于 Web 应用的 ServletContext。Web 容器用到此 Bean 时创建,容器关闭时销毁 前两个取值不再赘述,重点看下后三个取值。
@Slf4j @Component @Scope(WebApplicationContext.SCOPE_REQUEST) public class BeanForRequest { @PreDestroy public void destroy () { log.info("destroy" ); } } @Slf4j @Component @Scope(WebApplicationContext.SCOPE_SESSION) public class BeanForSession { @PreDestroy public void destroy () { log.info("destroy" ); } } @Slf4j @Component @Scope(WebApplicationContext.SCOPE_APPLICATION) public class BeanForApplication { @PreDestroy public void destroy () { log.info("destroy" ); } }
编写一个 Controller 进行测试:
@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>" ; } }
主启动类:
@SpringBootApplication public class A08Application { public static void main (String[] args) { SpringApplication.run(A08Application.class, args); } }
如果使用的 JDK 版本大于 8,需要要启动参数中添加如下信息避免报错:
--add-opens java.base/java.lang=ALL-UNNAMED
但更建议在 pom.xml 中添加以下配置,一劳永逸:
<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: indi.lcp.bean.a08.BeanForRequest@34d37122 session scope: indi.lcp.bean.a08.BeanForSession@75ee7b19 application scope: indi.lcp.bean.a08.BeanForApplication@68b50897
刷新页面,页面上的信息变化为:
request scope: indi.lcp.bean.a08.BeanForRequest@2db4ac39 session scope: indi.lcp.bean.a08.BeanForSession@75ee7b19 application scope: indi.lcp.bean.a08.BeanForApplication@68b50897
可以看到 request scope 发生了变化,session scope 和 application scope 没有变化。
这是因为刷新页面后就产生了一个新的请求,而 request 的作用范围只在一个请求内,因此每一个新请求就对应一个新的对象。
那要怎么改变 session scope 呢?
换一个浏览器访问 http://localhost:8080/test,两个浏览器中的会话肯定不是同一个,此时 session scope 应该会发生变化:
request scope: indi.lcp.bean.a08.BeanForRequest@2286f290 session scope: indi.lcp.bean.a08.BeanForSession@4f025f73 application scope: indi.lcp.bean.a08.BeanForApplication@68b50897
application 的作用范围是 ServletContext,要想 application scope 发生变化可以重启程序。
当刷新页面后,除了 request scope 的值发生变化外,在 IDEA 的控制台能看到以下信息:
indi.lcp.bean.a08.BeanForRequest : destroy
这表示 request 作用范围的 Bean 进行了销毁,执行了销毁方法。
如果想看到 session 作用范围的 Bean 执行销毁方法,可以等 session 过期时在控制台上看到对应的信息。默认情况下,session 的过期时间是 30 分钟,为了更好地测试,可以在配置文件中添加:
# 修改 session 过期时间为 10s server.servlet.session.timeout=10s
这个配置是全局的,如果只想针对某个请求进行配置,则可以:
@GetMapping(value = "/test", produces = "text/html") public String test (HttpServletRequest request, HttpSession session) { session.setMaxInactiveInterval(10 ); }
设置 session 过期时间为 10 秒后,并不表示不进行任何操作 10 秒后就能在控制台上看到执行销毁方法的信息,经过测试,大概会等 1 分钟,静静等待 1 分钟左右,控制台上显示:
indi.lcp.bean.a08.BeanForSession : destroy
很遗憾没有办法看到 application 作用范围的 Bean 执行销毁方法,因为 Spring 似乎并没有对 application 作用范围的 Bean 进行正确的销毁处理,因此在 Servlet 容器销毁时看不到 application 作用范围的 Bean 执行销毁方法。
8.2. Scope 失效分析 现有两个类:
@Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class F1 {} @Getter @Component public class E { @Autowired private F1 f1; }
之后进行测试:
@Slf4j @ComponentScan("indi.lcp.bean.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(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 标记,之后向 e 中注入了 f1,那么 log.info(“{}”, e.getF1()); 打印出的 f1 应该都不是同一个对象吗?
indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F1@5fdcaa40 indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F1@5fdcaa40 indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F1@5fdcaa40
获取到的 f1 居然都是同一个,也就是说向单例对象中注入多例对象失败了。
对于单例对象来说,依赖注入仅发生了一次,后续不会再注入其他的 f1,因此 e 始终使用的是第一次注入的 f1:
为了解决这个问题,可以使用 @Lazy 生成代理对象,虽然代理对象依旧是同一个,但每次使用代理对象中的方法时,会由代理对象创建新的目标对象:
解决方式一
@Getter @Component public class E { @Lazy @Autowired private F1 f1; }
再修改下 main() 方法,打印下 f1 的 Class 信息,查看是否是代理对象:
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(); } indi.lcp.bean.a09.A09Application : class indi .lcp.bean.a09.F1$$EnhancerBySpringCGLIB$$ea96cbb5 indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F1@37271612 indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F1@4c309d4d indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F1@37883b97
使用 @Lazy 注解后,注入的是代理对象,每次获取到的 f1 不再是同一个。
解决方式二
除了使用 @Lazy 注解外,可以使用 @Scope 注解的 proxyMode 属性指定代理模式:
@Component @Scope( value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS ) public class F2 {} @Getter @Component public class E { @Autowired private F2 f2; }
之后再测试:
@Slf4j @ComponentScan("indi.lcp.bean.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(); } } indi.lcp.bean.a09.A09Application : class indi .lcp.bean.a09.F2$$EnhancerBySpringCGLIB$$f28665e2 indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F2@2525ff7e indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F2@524d6d96 indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F2@152aa092
解决方式三
@Component @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class F3 {} @Component public class E { @Autowired private ObjectFactory<F3> f3; public F3 getF3 () { return f3.getObject(); } } @Slf4j @ComponentScan("indi.lcp.bean.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()); log.info("{}" , e.getF3()); context.close(); } } indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F3@76f2bbc1 indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F3@306cf3ea
解决方式四
@Component @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class F4 {} @Component public class E { @Autowired private ApplicationContext applicationContext; public F4 getF4 () { return applicationContext.getBean(F4.class); } } @Slf4j @ComponentScan("indi.lcp.bean.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(); } } indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F4@2beee7ff indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F4@5136d012
如果对性能要求较高,则推荐使用后两种方式,前两种使用代理会有一定的性能损耗;如果不在乎那点性能损耗,则可以使用第一种方式,这种方式最简单。
四种解决方式虽然不同,但在理念上殊途同归,都是推迟了其他 Scope Bean 的获取,或者说按需加载。
AspectJ 编译器增强 创建一个 SpringBoot 项目,除了常见的依赖外,记得导入 AOP 相关的依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
Service 类:
@Service public class MyService { private static final Logger log = LoggerFactory.getLogger(MyService.class); public void foo () { log.info("foo()" ); } }
切面类,注意这个切面类没有被 Spring 管理:
@Aspect public class MyAspect { private static final Logger log = LoggerFactory.getLogger(MyAspect.class); @Before("execution(* indi.lcp.service.MyService.foo())") public void before () { log.info("before()" ); } }
用于测试的主启动类:
@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(); } }
运行主启动类后,控制台会显示:
indi.lcp.A10Application : service class: class indi .lcp.service.MyService indi.lcp.aop.MyAspect : before() indi.lcp.service.MyService : foo()
如果完全按照上述步骤进行,会发现 输出结果和给出的结果不一样。
在揭晓答案前,查看 service.getClass() 打印出的信息,它打印出的是原始类的 Class 信息,而非代理类的 Class 信息。
如果要问到 Spring AOP 的实现原理是什么,一下就能想到的是使用了代理,但这里并没有使用代理,依旧实现了增强。
这是因为在 pom.xml 中还引入了一个插件:
<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> <!-- use this goal to weave all your main classes --> <goal>compile</goal> <!-- use this goal to weave all your test classes --> <goal>test-compile</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
之后不再使用 IDEA 自带的编译器进行编译,而是使用 Maven 编译
编译之后查看生成的 target 文件夹下的 MyService.class 文件:
@Service public class MyService { private static final Logger log = LoggerFactory.getLogger(MyService.class); public MyService () { } public void foo () { MyAspect.aspectOf().before(); log.info("foo()" ); } }
可以看到在 foo() 方法中增加了一行代码:MyAspect.aspectOf().before();,也就是这行代码对 foo() 方法实现了增强。
这种方式属于编译时增强,和 Lombok 类似。
既然如此,那岂不是说使用这种方式时,没有 Spring 容器也能实现方法的增强?
确实如此。
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(); } } indi.lcp.A10Application - service class: class indi .lcp.service.MyService indi.lcp.aop.MyAspect - before() indi.lcp.service.MyService - foo()
除此之外,使用这种方式,就算 foo() 方法是静态方法,也能够成功增强。
public class MyService { private static final Logger log = LoggerFactory.getLogger(MyService.class); public static void foo () { log.info("foo()" ); } } public class A10Application { public static void main (String[] args) { MyService.foo(); } } indi.lcp.aop.MyAspect - before() indi.lcp.service.MyService - foo()
Agent 类加载 重新创建一个 SpringBoot 项目,同样需要导入 AOP 相关的依赖。
一个 Service 类:
@Slf4j @Service public class MyService { final public void foo () { log.info("foo()" ); bar(); } public void bar () { log.info("bar()" ); } }
一个切面类,注意这个切面类没有被 Spring 管理:
@Slf4j @Aspect public class MyAspect { @Before("execution(* indi.lcp.service.MyService.*())") public void before () { log.info("before()" ); } }
一个用于测试的主启动类:
@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(); } }
运行主启动类后,控制台会显示:
indi.lcp.A11Application : service class: class indi .lcp.service.MyService indi.lcp.aop.MyAspect : before() indi.lcp.service.MyService : foo() indi.lcp.aop.MyAspect : before() indi.lcp.service.MyService : bar()
如果完全按照上述步骤进行,会发现输出结果和给出的结果不一样。
那是怎么达到增强的效果呢?
首先得在 resources 目录下新建 META-INF 文件夹,并在 META-INF 目录下新建 aop.xml 文件,其内容如下:
<aspectj> <aspects> <!-- 切面类全限定类名 --> <aspect name="indi.lcp.aop.MyAspect" /> <weaver options="-verbose -showWeaveInfo" > <!-- 被增强方法所在类的全限定类名 --> <include within="indi.lcp.service.MyService" /> <!-- 切面类全限定类名 --> <include within="indi.lcp.aop.MyAspect" /> </weaver> </aspects> </aspectj>
在运行 main() 方法前添加 VM options:
-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 文件,会发现其内容并没有被修改,可以断定不是编译时增强,这里是在类加载时增强
动态代理 JDK 动态代理 JDK 动态代理 只能 针对接口进行代理。
public class JdkProxyDemo { interface Foo { void foo () ; } static final class Target implements Foo { @Override public void foo () { System.out.println("target foo" ); } } public static void main (String[] args) { Target target = new Target (); ClassLoader classLoader = JdkProxyDemo.class.getClassLoader(); Foo proxy = (Foo) Proxy.newProxyInstance(classLoader, new Class []{Foo.class}, (p, method, params) -> { System.out.println("before..." ); Object result = method.invoke(target, params); System.out.println("after..." ); return result; }); proxy.foo(); } }
运行 main() 方法后控制台打印出:
before... target foo after...
代理对象和目标对象是兄弟关系,都实现了相同的接口,因此不能将代理对象强转成目标对象类型;
代理类与目标类之间没有继承关系,因此目标类可以被 final 修饰。
11.2. CGLib 动态代理 CGLib 动态代理与 JDK 动态代理不一样,无需目标类实现某个特定的接口:
public class CglibProxyDemo { static class Target { public void foo () { System.out.println("target foo" ); } } public static void main (String[] args) { Target target = new Target (); Target proxy = (Target) Enhancer.create(Target.class, (MethodInterceptor) (obj, method, params, methodProxy) -> { System.out.println("before..." ); Object result = method.invoke(target, params); System.out.println("after..." ); return result; }); proxy.foo(); } }
运行 main() 方法后控制台打印出:
before... target foo after...
调用目标方法的方式有三种,上文使用的是:
Object result = method.invoke(target, params);
使用这种方式,将利用反射对目标方法进行调用。
还可以使用 methodProxy 不利用反射对目标方法进行调用:
Object result = methodProxy.invoke(target, args); Object result = methodProxy.invokeSuper(obj, args);
与 JDK 动态代理相比,CGLib 动态代理无需实现接口 代理对象和目标对象是父子关系,也就是说代理类继承了目标类 由于代理类继承了目标类,因此目标类不能被 final 修饰,否则将出现以下异常信息: java.lang.IllegalArgumentException: Cannot subclass final class indi .lcp.a12.CglibProxyDemo$Target
代理类继承目标类后,还会重写目标类中要求被增强的方法,因此被增强的方法不能被 final 修饰,否则将无法被增强,但不会抛出异常 JDK 动态代理原理 12.1 JDK 动态代理的模拟 模拟 JDK 动态代理的实现:
interface Foo { void foo () ; } static class Target implements Foo { @Override public void foo () { System.out.println("target foo" ); } } public class $Proxy0 implements A13 .Foo{ @Override public void foo () { System.out.println("before..." ); new A13 .Target().foo(); } } public static void main (String[] args) { $Proxy0 proxy = new $Proxy0 (); proxy.foo(); }
运行 main() 方法,控制台打印出:
代码的实现很简单,但仔细想一下,如果是 JDK 中的实现:
“功能增强”的代码实现会直接硬编码吗?直接打印?
“调用目标”的代码就这样直接写上去?存不存在满足某些条件才调用目标的场景呢?
也就是说,“功能增强”和“调用目标”这两部分的代码都是不确定的。
针对这种“不确定”的实现,可以提供一个抽象类,等到用户具体使用时才实现抽象类,重写抽象方法。
interface Foo { void foo () ; } static class Target implements Foo { @Override public void foo () { System.out.println("target foo" ); } } interface InvocationHandler { void invoke () ; } public class $Proxy0 implements A13 .Foo{ private final A13.InvocationHandler h; public $Proxy0(A13.InvocationHandler h) { this .h = h; } @Override public void foo () { h.invoke(); } } public static void main (String[] args) { $Proxy0 proxy = new $Proxy0 (new InvocationHandler () { @Override public void invoke () { System.out.println("before..." ); new A13 .Target().foo(); } }); proxy.foo(); }
运行 main() 方法,控制台依旧成功打印出:
多个抽象方法的接口
这样的实现依旧有问题,如果接口中提供了两个抽象方法呢?比如:
interface Foo { void foo () ; void bar () ; }
此时无论是目标类,还是代理类都要重写这个方法:
static class Target implements Foo { @Override public void foo () { System.out.println("target foo" ); } @Override public void bar () { System.out.println("target bar" ); } } public class $Proxy0 implements A13 .Foo{ private final A13.InvocationHandler h; public $Proxy0(A13.InvocationHandler h) { this .h = h; } @Override public void foo () { h.invoke(); } @Override public void bar () { h.invoke(); } } public static void main (String[] args) { $Proxy0 proxy = new $Proxy0 (new InvocationHandler () { @Override public void invoke () { System.out.println("before..." ); new A13 .Target().foo(); } }); proxy.foo(); proxy.bar(); }
此时再执行 main() 方法,控制台上打印出:
before... target foo before... target foo
打印结果有点问题。当调用代理对象的 bar() 方法时,输出了 target foo,而不是 bar() 方法应该打印的 target bar。
原因就出在实现 InvocationHandler 的 invoke() 方法时,依旧只调用了目标类的 foo() 方法,而不是 bar() 方法。
也就是说,在调用代理对象中的某个方法时,增强的应该是目标对象中对应的方法,希望在调用目标方法时能够动态编码。
那么可以在 invoke() 方法中添加两个入参,分别表示需要调用的目标方法和目标方法的参数:
interface InvocationHandler { void invoke (Method method, Object[] params) throws Throwable; }
增加参数之后需要修改代理类,并将实现的抽象方法的 Method 对象与参数传递给 invoke() 方法:
public class $Proxy0 implements A13 .Foo{ private final A13.InvocationHandler h; public $Proxy0(A13.InvocationHandler h) { this .h = h; } @Override @SneakyThrows public void foo () { Method method = A13.Foo.class.getMethod("foo" ); h.invoke(method, new Object [0 ]); } @Override @SneakyThrows public void bar () { Method method = A13.Foo.class.getMethod("bar" ); h.invoke(method, new Object [0 ]); } }
还需要修改下 main() 方法中 InvocationHandler 的实现,利用传递的 Method 对象和参数信息反射调用目标方法:
public static void main (String[] args) { $Proxy0 proxy = new $Proxy0 (new InvocationHandler () { @Override public void invoke (Method method, Object[] params) throws Throwable { System.out.println("before..." ); Object invoke = method.invoke(new Target (), params); } }); proxy.foo(); proxy.bar(); }
再执行 main() 方法,控制台上打印出:
before... target foo before... target bar
有返回值的抽象方法
优化还在继续,如果抽象方法有返回值呢?比如:
interface Foo { void foo () ; int bar () ; }
实现了这个接口的目标类和代理类重写的方法都需要有具体的返回值:
static class Target implements Foo { @Override public void foo () { System.out.println("target foo" ); } @Override public int bar () { System.out.println("target bar" ); return 100 ; } }
目标类很简单,直接返回,那代理类返回什么?
InvocationHandler 的 invoke() 方法是对“功能增强”和“调用目标”的抽象,因此可以使 invoke() 方法也返回一个值,返回的值即为目标方法的返回值,这样就可以使得代理类中的方法有值可返。
interface InvocationHandler { Object invoke (Method method, Object[] params) throws Throwable; } public class $Proxy0 implements A13 .Foo { private final A13.InvocationHandler h; public $Proxy0(A13.InvocationHandler h) { this .h = h; } @Override public void foo () { try { Method foo = A13.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 = A13.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() 方法的返回值:
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 100
在静态代码块里创建 Method 实例
每调用一次代理对象中的方法都会创建一个 Method 实例,这些实例是可以复用的,因此可以将这些实例的创建移动到静态代码块中:
public class $Proxy0 implements A13 .Foo { private final A13.InvocationHandler h; public $Proxy0(A13.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 { return (int ) h.invoke(bar, new Object [0 ]); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException (e); } } static Method foo; static Method bar; static { try { foo = A13.Foo.class.getMethod("foo" ); bar = A13.Foo.class.getMethod("bar" ); } catch (NoSuchMethodException e) { throw new NoSuchMethodError (e.getMessage()); } } }
invoke() 方法增加代理对象作为参数
在 JDK 提供的 InvocationHandler 接口的 invoke() 方法还将代理对象作为方法的参数,以便用户根据实际情况使用。继续修改自定义的 InvocationHandler 接口:
interface InvocationHandler { Object invoke (Object proxy, Method method, Object[] params) throws Throwable; }
修改代理类中对 invoke() 方法的调用,第一个参数为当前类的实例,即 this:
public class $Proxy0 implements A13 .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 参数:
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 接口无异,注释自定义的 InvocationHandler,更换为 JDK 提供的 InvocationHandler 接口。
在 JDK 提供的 InvocationHandler 接口的注释中有一句:@see Proxy,在 Proxy 类的代码中有:
public class Proxy implements java .io.Serializable { protected InvocationHandler h; protected Proxy (InvocationHandler h) { Objects.requireNonNull(h); this .h = h; } }
Proxy 类中有一个 InvocationHandler 对象的成员变量。
因此还可以使代理类 $Proxy0 继承 Proxy 来进一步减少代码:
import java.lang.reflect.InvocationHandler;public class $Proxy0 extends Proxy implements A13 .Foo { private static final long serialVersionUID = -6909541593982979501L ; public $Proxy0(InvocationHandler h) { super (h); } }
12.2 代理类的源码 JDK 动态代理生成的代理类是以字节码的形式存在的,并不存在所谓的 .java 文件,但也不是说就没办法看到生成的代理类信息了。
以【11.1 JDK 动态代理】中的程序为例,查看 JDK 生成的代理类信息。
利用 Arthas 反编译代理类字节码文件
如果要使用 Arthas 的反编译功能需要满足两个条件:
知道被反编译文件的全限定类名
程序不能中断,需要存在 Java 进程
为了满足这个条件,可以在控制台打印出生成的代理类的全限定类名,然后利用阻塞 IO 使程序不中断:
@SneakyThrows public static void main (String[] args) { Target target = new Target (); ClassLoader classLoader = JdkProxyDemo.class.getClassLoader(); Foo proxy = (Foo) Proxy.newProxyInstance(classLoader, new Class []{Foo.class}, (p, method, params) -> { System.out.println("before..." ); Object result = method.invoke(target, params); System.out.println("after..." ); return result; }); System.out.println(proxy.getClass()); proxy.foo(); System.in.read(); }
运行 main() 方法后,控制台打印出:
class indi .lcp.a12.$Proxy0before... target foo after...
其中的 indi.lcp.a12.$Proxy0 就是生成的代理类的全限定类名,可以把它复制下来,之后按照【10. Agent 类加载】中使用 Arthas 的方式 indi.lcp.a12.$Proxy0 进行反编译即可。
将生成的代理类字节码文件保存在磁盘上
除了借助外部工具外,还可以直接将 JDK 生成的代理类字节码文件保存在磁盘上,其做法与【Lambda 与序列化】一文中将函数式接口动态生成的 Class 保存到磁盘上类似。
可以在 main() 方法开头加一句:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles" , "true" ); System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles" , "true" );
不同版本的 JDK 添加的配置信息不同,至于具体是哪一个可以查看 JDK 中 ProxyGenerator 类中的 saveGeneratedFiles 成员变量,比如:
private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction ("sun.misc.ProxyGenerator.saveGeneratedFiles" ));
显然,此处应该使用第一种方式来设置系统属性。最终的 main() 方法如下:
public static void main (String[] args) { System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles" , "true" ); Target target = new Target (); ClassLoader classLoader = JdkProxyDemo.class.getClassLoader(); Foo proxy = (Foo) Proxy.newProxyInstance(classLoader, new Class []{Foo.class}, (p, method, params) -> { }); proxy.foo(); }
除此之外,还可以通过在运行前添加 VM options:
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
如果既在代码里设置了系统属性,又配置了 VM options,最终以代码中的配置为主。
运行 main() 方法,在当前项目目录下生成 indi.lcp.a12.$Proxy0.class 文件,查看其内容:
final class $Proxy0 extends Proxy implements Foo { private static Method m1; private static Method m2; private static Method m3; private static Method m0; public $Proxy0(InvocationHandler var1) throws { super (var1); } public final boolean equals (Object var1) throws { try { return (Boolean)super .h.invoke(this , m1, new Object []{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException (var4); } } public final String toString () throws { try { return (String)super .h.invoke(this , m2, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException (var3); } } public final void foo () throws { try { super .h.invoke(this , m3, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException (var3); } } public final int hashCode () throws { try { return (Integer)super .h.invoke(this , m0, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException (var3); } } static { try { m1 = Class.forName("java.lang.Object" ).getMethod("equals" , Class.forName("java.lang.Object" )); m2 = Class.forName("java.lang.Object" ).getMethod("toString" ); m3 = Class.forName("indi.lcp.a12.JdkProxyDemo$Foo" ).getMethod("foo" ); m0 = Class.forName("java.lang.Object" ).getMethod("hashCode" ); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError (var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError (var3.getMessage()); } } }
其内容与自定义的 $Proxy0 几乎无异,只不过 JDK 生成的代理类信息还生成 equals()、toString() 和 hashCode() 三个方法对应的 Method 对象,并对它们也进行了相同的增强。
12.3 JDK 代理类字节码生成 JDK 在生成代理类时,没有经历源码阶段、编译阶段,而是直接到字节码阶段,使用了 ASM 来完成。
ASM 的学习成本较高,在此不做过多介绍,本节将采用一直“曲线求国”的方式,使用 IDEA 的 ASM Bytecode outline 插件将 Java 源码转换成使用 ASM 编写的代码。
ASM Bytecode outline 插件在高版本 Java 中可能无法很好地工作,建议在 Java8 环境下使用。
自行编写一个接口和代理类:
public interface Foo { void foo () ; } public class $Proxy0 extends Proxy implements Foo { private static final long serialVersionUID = 6059465134835974286L ; static Method foo; static { try { foo = 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 , foo, null ); } catch (Throwable throwable) { throw new UndeclaredThrowableException (throwable); } } }
将上述代码进行编译,编译成功后在 $Proxy0 文件中右击,选择 Show Bytecode outline 浏览当前类对应的字节码信息
查看 ASMified,并拷贝其内容,复制到 $Proxy0Dump 中:
import jdk.internal.org.objectweb.asm.*;public class $Proxy0Dump implements Opcodes { public static byte [] dump() throws Exception { ClassWriter cw = new ClassWriter (0 ); FieldVisitor fv; MethodVisitor mv; AnnotationVisitor av0; cw.visit(52 , ACC_PUBLIC + ACC_SUPER, "indi/lcp/$Proxy0" , null , "java/lang/reflect/Proxy" , new String []{"indi/lcp/Foo" }); cw.visitSource("$Proxy0.java" , null ); { fv = cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, "serialVersionUID" , "J" , null , new Long (6059465134835974286L )); fv.visitEnd(); } { fv = cw.visitField(ACC_STATIC, "foo" , "Ljava/lang/reflect/Method;" , null , null ); fv.visitEnd(); } { mv = cw.visitMethod(ACC_PROTECTED, "<init>" , "(Ljava/lang/reflect/InvocationHandler;)V" , null , null ); mv.visitParameter("h" , 0 ); mv.visitCode(); Label l0 = new Label (); mv.visitLabel(l0); mv.visitLineNumber(26 , l0); mv.visitVarInsn(ALOAD, 0 ); mv.visitVarInsn(ALOAD, 1 ); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/reflect/Proxy" , "<init>" , "(Ljava/lang/reflect/InvocationHandler;)V" , false ); Label l1 = new Label (); mv.visitLabel(l1); mv.visitLineNumber(27 , l1); mv.visitInsn(RETURN); Label l2 = new Label (); mv.visitLabel(l2); mv.visitLocalVariable("this" , "Lindi/lcp/$Proxy0;" , null , l0, l2, 0 ); mv.visitLocalVariable("h" , "Ljava/lang/reflect/InvocationHandler;" , null , l0, l2, 1 ); mv.visitMaxs(2 , 2 ); mv.visitEnd(); } { mv = cw.visitMethod(ACC_PUBLIC, "foo" , "()V" , null , null ); mv.visitCode(); Label l0 = new Label (); Label l1 = new Label (); Label l2 = new Label (); mv.visitTryCatchBlock(l0, l1, l2, "java/lang/Throwable" ); mv.visitLabel(l0); mv.visitLineNumber(32 , l0); mv.visitVarInsn(ALOAD, 0 ); mv.visitFieldInsn(GETFIELD, "indi/lcp/$Proxy0" , "h" , "Ljava/lang/reflect/InvocationHandler;" ); mv.visitVarInsn(ALOAD, 0 ); mv.visitFieldInsn(GETSTATIC, "indi/lcp/$Proxy0" , "foo" , "Ljava/lang/reflect/Method;" ); mv.visitInsn(ACONST_NULL); mv.visitMethodInsn(INVOKEINTERFACE, "java/lang/reflect/InvocationHandler" , "invoke" , "(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;" , true ); mv.visitInsn(POP); mv.visitLabel(l1); mv.visitLineNumber(35 , l1); Label l3 = new Label (); mv.visitJumpInsn(GOTO, l3); mv.visitLabel(l2); mv.visitLineNumber(33 , l2); mv.visitFrame(Opcodes.F_SAME1, 0 , null , 1 , new Object []{"java/lang/Throwable" }); mv.visitVarInsn(ASTORE, 1 ); Label l4 = new Label (); mv.visitLabel(l4); mv.visitLineNumber(34 , l4); mv.visitTypeInsn(NEW, "java/lang/reflect/UndeclaredThrowableException" ); mv.visitInsn(DUP); mv.visitVarInsn(ALOAD, 1 ); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/reflect/UndeclaredThrowableException" , "<init>" , "(Ljava/lang/Throwable;)V" , false ); mv.visitInsn(ATHROW); mv.visitLabel(l3); mv.visitLineNumber(36 , l3); mv.visitFrame(Opcodes.F_SAME, 0 , null , 0 , null ); mv.visitInsn(RETURN); Label l5 = new Label (); mv.visitLabel(l5); mv.visitLocalVariable("throwable" , "Ljava/lang/Throwable;" , null , l4, l3, 1 ); mv.visitLocalVariable("this" , "Lindi/lcp/$Proxy0;" , null , l0, l5, 0 ); mv.visitMaxs(4 , 2 ); mv.visitEnd(); } { mv = cw.visitMethod(ACC_STATIC, "<clinit>" , "()V" , null , null ); mv.visitCode(); Label l0 = new Label (); Label l1 = new Label (); Label l2 = new Label (); mv.visitTryCatchBlock(l0, l1, l2, "java/lang/NoSuchMethodException" ); mv.visitLabel(l0); mv.visitLineNumber(19 , l0); mv.visitLdcInsn(Type.getType("Lindi/lcp/Foo;" )); mv.visitLdcInsn("foo" ); mv.visitInsn(ICONST_0); mv.visitTypeInsn(ANEWARRAY, "java/lang/Class" ); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class" , "getMethod" , "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;" , false ); mv.visitFieldInsn(PUTSTATIC, "indi/lcp/$Proxy0" , "foo" , "Ljava/lang/reflect/Method;" ); mv.visitLabel(l1); mv.visitLineNumber(22 , l1); Label l3 = new Label (); mv.visitJumpInsn(GOTO, l3); mv.visitLabel(l2); mv.visitLineNumber(20 , l2); mv.visitFrame(Opcodes.F_SAME1, 0 , null , 1 , new Object []{"java/lang/NoSuchMethodException" }); mv.visitVarInsn(ASTORE, 0 ); Label l4 = new Label (); mv.visitLabel(l4); mv.visitLineNumber(21 , l4); mv.visitTypeInsn(NEW, "java/lang/NoSuchMethodError" ); mv.visitInsn(DUP); mv.visitVarInsn(ALOAD, 0 ); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/NoSuchMethodException" , "getMessage" , "()Ljava/lang/String;" , false ); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/NoSuchMethodError" , "<init>" , "(Ljava/lang/String;)V" , false ); mv.visitInsn(ATHROW); mv.visitLabel(l3); mv.visitLineNumber(23 , l3); mv.visitFrame(Opcodes.F_SAME, 0 , null , 0 , null ); mv.visitInsn(RETURN); mv.visitLocalVariable("e" , "Ljava/lang/NoSuchMethodException;" , null , l4, l3, 0 ); mv.visitMaxs(3 , 1 ); mv.visitEnd(); } cw.visitEnd(); return cw.toByteArray(); } }
编写测试方法使用 $Proxy0Dump 生成 $Proxy0 的 class 文件:
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(); } }
运行 main() 方法后,在工作目录下生成 $Proxy0.class。
工作目录查看方式:
也就是说会在 D:\Code\IdeaCode\advanced-spring 目录下生成 $Proxy0.class 文件,IDEA 反编译后的内容与手动编写的 $Proxy0.java 文件的内容无异。
实际使用时并不需要使用 $Proxy0Dump 生成 $Proxy.class 文件,而是利用 ClassLoader 直接加载类信息:
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("indi.lcp.$Proxy0" ); Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class); Foo fooProxy = (Foo) constructor.newInstance((InvocationHandler) (proxy, method, args1) -> { System.out.println("before..." ); System.out.println("模拟调用目标" ); return null ; }); fooProxy.foo(); }
运行上述代码后,控制台打印出:
12.4 JDK 反射优化 使用 JDK 的动态代理时,会使用反射调用方法:
Object result = method.invoke(target, params);
相比于正常调用方法,利用反射的性能要稍微低一些,JDK 有怎么反射进行优化吗?
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" ); } }
运行 main() 方法后,控制台打印出:
1 : null 1 : foo2 : sun.reflect.NativeMethodAccessorImpl@1be6f5c32 : foo3 : sun.reflect.NativeMethodAccessorImpl@1be6f5c33 : foo4 : sun.reflect.NativeMethodAccessorImpl@1be6f5c34 : foo5 : sun.reflect.NativeMethodAccessorImpl@1be6f5c35 : foo6 : sun.reflect.NativeMethodAccessorImpl@1be6f5c36 : foo7 : sun.reflect.NativeMethodAccessorImpl@1be6f5c37 : foo8 : sun.reflect.NativeMethodAccessorImpl@1be6f5c38 : foo9 : sun.reflect.NativeMethodAccessorImpl@1be6f5c39 : foo10 : sun.reflect.NativeMethodAccessorImpl@1be6f5c310 : foo11 : sun.reflect.NativeMethodAccessorImpl@1be6f5c311 : foo12 : sun.reflect.NativeMethodAccessorImpl@1be6f5c312 : foo13 : sun.reflect.NativeMethodAccessorImpl@1be6f5c313 : foo14 : sun.reflect.NativeMethodAccessorImpl@1be6f5c314 : foo15 : sun.reflect.NativeMethodAccessorImpl@1be6f5c315 : foo16 : sun.reflect.NativeMethodAccessorImpl@1be6f5c316 : foo17 : sun.reflect.GeneratedMethodAccessor2@5b2133b117 : foo
从上述信息可知,第一次调用时没有使用 MethodAccessor 对象,从第二次到第十六次,使用了 NativeMethodAccessorImpl 对象,而在第十七次使用了 GeneratedMethodAccessor2 对象。
NativeMethodAccessorImpl 基于 Java 本地 API 实现,性能较低,第十七次调用换成 GeneratedMethodAccessor2 后,性能得到一定的提升。
使用 Arthas 反编译查看 GeneratedMethodAccessor2 类中的信息,内容如下:
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()); } } }
在反编译得到的代码中,不再是通过反射调用方法,而是直接正常调用方法,即:
TestMethodProxy.foo((int )c);
因此性能得到了提升,但这样的提升也是有一定代价的:为优化 一个 方法的反射调用,生成了一个 GeneratedMethodAccessor2 代理类。
CGLib 动态代理原理 13.1 CGLib 动态代理的模拟 同样先模拟下 CGLib 动态代理的模拟:
被代理的类,即目标类:
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 动态代理生成的代理类:
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); } } } 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 ); }
运行 main() 方法后,控制台打印出:
before save () before save (int ) before save (long )
13.2 MethodProxy 在上述 Proxy 类中,重写了父类中的方法,并在重写的方法中调用了 intercept()
方法,重写的这些方法相当于是带增强功能的方法。
在 JDK 的动态代理中,使用反射对方法进行调用,而在 CGLib 动态代理中,可以使用 intercept()
方法中 MethodProxy
类型的参数实现不经过反射来调用方法。
接收的 MethodProxy
类型的参数可以像 Method
类型的参数一样,在静态代码块中被实例化。
可以通过静态方法 MethodProxy.create()
来创建 MethodProxy
对象:
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
指方法描述符(【Lambda 与序列化】一文中介绍了关于 Java 描述符的更多内容); 参数 name1
指带 增强 功能的方法名称; 参数 name2
指带 原始 功能的方法名称。 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); } } }
在 main()
方法中不再使用 Method
类型的参数对方法进行调用,而是使用 MethodProxy
类型的参数:
return methodProxy.invoke(target, args);
或者:
return methodProxy.invokeSuper(o, args);
MethodProxy 原理 调用 methodProxy.invoke() 方法时,会额外使用一个代理类,该代理类配合目标对象使用。调用 methodProxy.invokeSuper() 方法时,也会额外使用一个代理类,该代理类配合代理对象使用。
当调用 MethodProxy 对象的 invoke() 方法或 invokeSuper() 方法时,就会生成这两个代理类,它们都继承至 FastClass。
FastClass 是一个抽象类,其内部有多个抽象方法:
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) 方法。
模拟生成的与目标类相关的代理类
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 ; } if (s1.equals(signature)) { return 1 ; } 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 ; } if (index == 1 ) { ((Target) target).save((int ) args[0 ]); return null ; } if (index == 2 ) { ((Target) target).save((long ) args[0 ]); return null ; } 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 }); } }
运行 main() 方法后,控制台打印出:
模拟生成的与代理类相关的代理类
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 ; } if (s1.equals(signature)) { return 1 ; } if (s2.equals(signature)) { return 2 ; } return -1 ; } public Object invoke (int index, Object proxy, Object[] args) { if (index == 0 ) { ((Proxy) proxy).saveSuper(); return null ; } if (index == 1 ) { ((Proxy) proxy).saveSuper((int ) args[0 ]); return null ; } if (index == 2 ) { ((Proxy) proxy).saveSuper((long ) args[0 ]); return null ; } throw new RuntimeException ("无此方法" ); } public static void main (String[] args) { ProxyFastClass fastClass = new ProxyFastClass (); int index = fastClass.getIndex(new Signature ("saveSuper" , "()V" )); fastClass.invoke(index, new Proxy (), new Object [0 ]); } }
运行 main() 方法后,控制台打印出:
总结
调用 MethodProxy.create() 方法创建 MethodProxy 对象时,要求传递带增强功能的方法名称、带原始功能的方法名称以及方法描述符。
根据两个方法名称和方法描述符可以在调用生成的两个代理类中的 getIndex() 方法时获取被增强方法的编号,之后:
调用 methodProxy.invoke() 方法时,就相当于调用 TargetFastClass 中的 invoke() 方法,并在这个 invoke() 方法中正常调用目标对象方法(Spring 底层的选择)。
调用 methodProxy.invokeSuper() 方法时,就相当于调用 ProxyFastClass 中的 invoke() 方法,并在这个 invoke() 方法中正常调用代理对象中带原始功能的方法。
与 JDK 中优化反射调用方法的对比
在 JDK 中需要反射调用 16 次方法后才会生成优化反射调用的代理类,而在 CGLib 中,当调用 MethodProxy.create() 方法时就会生成由于优化反射调用的代理类;
在 JDK 中一个方法的反射调用优化就要生成一个代理类,而在 CGLib 中,一个代理类生成两个 FastClass 代理类。
JDK 和 CGLib 的统一 15.1. advisor 切面有 aspect 和 advisor 两个概念,aspect 是多组通知(advice)和切点(pointcut)的组合,也是实际编码时使用的,advisor 则是更细粒度的切面,仅包含一个通知和切点,aspect 在生效之前会被拆解成多个 advisor。
Spring 中对切点、通知、切面的抽象如下:
切点:即 Pointcut,其典型实现是 AspectJExpressionPointcut
通知:即 Advice,其典型子类接口为 MethodInterceptor,表示环绕通知
切面:即 Advisor,仅包含一个切点和通知
本节将重点介绍 advisor 切面。
15.2 切面与代理对象的创建 通过以下四步创建切面和代理:
在 Spring 中,切点通过接口 org.springframework.aop.Pointcut 来表示:
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 的工厂,其内部通过不同的情况选择不同的代理实现,更方便地创建代理对象。
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(); }
运行 main() 方法,控制台打印出:
class indi .lcp.a15.A15$Target1$$EnhancerBySpringCGLIB$$381723d1before... 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 信息:
factory.setInterfaces(target.getClass().getInterfaces());
之后再运行 main(),控制台打印出:
class indi .lcp.a15.$Proxy0before... target1 foo after... target1 bar
此时选择的动态代理实现方式是 JDK 动态代理。
再设置 factory 对象的 proxyTargetClass 为 true:
factory.setProxyTargetClass(true );
运行 main() 方法后,控制台打印出以下内容,选择 CGLib 动态代理作为动态代理的实现方式:
class indi .lcp.a15.A15$Target1$$EnhancerBySpringCGLIB$$34c2d9b8before... target1 foo after... target1 bar
再将 proxyTargetClass 的值修改回 false,并修改目标对象的所在类为 Target2,Target2 并未实现任何接口:
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(); }
运行 main() 方法后,控制台打印出以下内容,依旧选择 CGLib 动态代理作为动态代理的实现方式:
class indi .lcp.a15.A15$Target2$$EnhancerBySpringCGLIB$$4bb2ac74before... target2 foo after... target2 bar
ProxyFactory 是用来创建代理的核心实现,使用 AopProxyFactory 选择具体的代理实现:
JdkDynamicAopProxy ObjenesisCglibAopProxy
AopProxyFactory 根据 proxyTargetClass 等设置选择 AopProxy 实现,AopProxy 通过 getProxy() 方法创建代理对象。
上述类图中的类与接口都实现了 Advised 接口,能够获得关联的切面集合与目标(实际上是从 ProxyFactory 中获取的)。
调用代理方法时,会借助 ProxyFactory 统一将通知转换为环绕通知 MethodInterceptor。
切点匹配 上一节中,选择 AspectJExpressionPointcut 作为切点的实现,判断编写的 AspectJ 表达式是否与某一方法匹配可以使用其 matches() 方法。
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 () { } }
运行 main() 方法后,控制台打印出:
@Transactional 是 Spring 中使用频率非常高的注解,那它底层是通过 AspectJExpressionPointcut 与 @annotation() 切点表达式相结合对目标方法进行匹配的吗?
答案是否定的。@Transactional 注解除了可以作用在方法上,还可以作用在类(或接口)上。
在底层 @Transactional 注解的匹配使用到了 StaticMethodMatcherPointcut,在此模拟一下:
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 () { } }
运行 main() 方法后,控制台打印出:
无论是 AspectJExpressionPointcut 还是 StaticMethodMatcherPointcut,它们都实现了MethodMatcher 接口,用来执行方法的匹配。
从 @Aspect 到 Advisor AnnotationAwareAspectJAutoProxyCreator 准备一下类:
一个使用 @Aspect 的高级切面
一个利用配置类实现的低级切面 Advisor
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:
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(); }
运行 main() 方法后,控制台打印出:
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() 方法:
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(); }
运行 main() 方法后,控制台打印出:
org.springframework.aop.interceptor.ExposeInvocationInterceptor.ADVISOR org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.framework.autoproxy.A17$Config$$Lambda$56 /802243390 @7bd4937b] 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 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(); }
运行 main() 方法后,控制台打印出:
class org .springframework.aop.framework.autoproxy.A17$Target1$$EnhancerBySpringCGLIB$$634976f6class org .springframework.aop.framework.autoproxy.A17$Target2
Target1 对象是被代理的,而 Target2 依旧是原对象。
如果将 o1 转换为 Target1,并调用 foo() 方法,foo() 方法将被增强:
public static void main (String[] args) { ((Target1) o1).foo(); context.close(); } advice3 before... aspect1 before... target1 foo aspect1 after... advice3 after...
切面的顺序控制
根据上述打印的信息可知,低级切面相比于高级切面先一步被执行,这个执行顺序是可以被控制的。
针对高级切面来说,可以在类上使用 @Order 注解,比如:
@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() 方法上是无用的:
@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 初始化完成之后
这两个时机二选一,不会重复创建代理对象。
以下述代码为例,查看代理对象的创建时机:
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。运行 main() 方法后,控制台打印出:
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 初始化完成后,额外打印了一句日志信息:
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,出现循环依赖:
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 列表。在搜索过程中,如果遇到高级切面,则会将其转换成低级切面。
现有切面类与目标类信息如下:
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 注解为例:
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); } }
运行 main() 方法,控制台打印出:
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.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 部分,这些通知在被调用时无需再次检查切点,直接调用即可。
切面类与目标类:
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" ); } }
将高级切面转换成低级切面,并将通知统一转换成环绕通知:
@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); } }
根据打印信息可知:
前置通知 AspectJMethodBeforeAdvice 被转换成 MethodBeforeAdviceInterceptor
环绕通知 AspectJAroundAdvice 保持不变
后置通知 AspectJAfterReturningAdvice 被转换成 AfterReturningAdviceInterceptor
18.2 调用链执行 高级切面成功转换成低级切面,切面中的通知也全部转换成环绕通知 MethodInterceptor,最后还要调用这些通知和目标方法。
这个调用交由调用链对象 MethodInvocation 来完成,在调用链对象中存放了所有经过转换得到的环绕通知和目标方法。
MethodInvocation 是一个接口,其最根本的实现是 ReflectiveMethodInvocation。
构建 ReflectiveMethodInvocation 对象需要 6 个参数:
proxy:代理对象 target:目标对象 method:目标对象中的方法对象 arguments:调用目标对象中的方法需要的参数 targetClass:目标对象的 Class 对象 interceptorsAndDynamicMethodMatchers:转换得到的环绕通知列表 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:
提示没有找到 MethodInvocation。但调用链对象不是已经创建好了吗?
这是因为调用链在执行过程会调用到很多通知,而某些通知内部可能需要使用调用链对象。因此需要将调用链对象存放在某一位置,使所有通知都能获取到调用链对象。
这个“位置”就是 当前线程。
那怎么将调用链对象放入当前线程呢?
可以在所有通知的最外层再添加一个环绕通知,其作用是将调用链对象放入当前线程。
可以使用 Spring 提供的 ExposeInvocationInterceptor 作为最外层的环绕通知。
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(); }
再次运行 main() 方法不再报错,控制台打印出:
before1 around...before before2 target foo around...after afterReturning
18.3 模拟实现调用链 调用链执行过程是一个递归过程。执行 proceed() 方法将调用调用链中下一个通知或目标方法。当调用链中没有通知时,就调用目标方法,反之调用下一个通知。
这体现了设计模式中的责任链模式。
目标类 Target:
static class Target { public void foo () { System.out.println("Target foo()" ); } }
实现 MethodInterceptor 接口,编写两个环绕通知:
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 接口,实现自己的调用链:
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; } }
编写 main() 方法,执行调用链,查看控制台输出结果:
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 返回最终的结果 动态通知调用 前文的示例都是静态通知调用,无需参数绑定,执行时无需切点信息,性能较高。
相应地就有动态通知调用,它需要参数绑定,执行时还需要切点信息,性能较低。比如:
@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:
static class Target { public void foo (int x) { System.out.printf("target foo(%d)\n" , x); } }
配置类 MyConfig:
@Configuration static class MyConfig { @Bean public AnnotationAwareAspectJAutoProxyCreator proxyCreator () { return new AnnotationAwareAspectJAutoProxyCreator (); } @Bean public MyAspect myAspect () { return new MyAspect (); } }
编写 main() 方法,新建 Spring 容器,查找符合条件的切面,将所有通知转换成环绕通知:
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); } }
执行 main() 方法,控制台打印出:
org.springframework.aop.interceptor.ExposeInvocationInterceptor@73e22a3d org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@47faa49c org.springframework.aop.framework.InterceptorAndDynamicMethodMatcher@28f2a10f
第一个 ExposeInvocationInterceptor 对象是 Spring 添加的环绕通知,第二个 MethodBeforeAdviceInterceptor 对象是前置通知转换得到的环绕通知,那 InterceptorAndDynamicMethodMatcher 对象是什么呢?
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 修饰,也没提供获取的方式,但可以使用反射:
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); } }
运行 main() 方法后,控制台打印出:
普通环绕通知:org.springframework.aop.interceptor.ExposeInvocationInterceptor@73e22a3d 普通环绕通知:org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@47faa49c 环绕通知和切点:org.springframework.aop.framework.InterceptorAndDynamicMethodMatcher@f736069 切点为:AspectJExpressionPointcut: (int x) execution(* foo(..)) && args(x) 通知为:org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@6da21078
根据打印的切点信息可知,InterceptorAndDynamicMethodMatcher 对象的确对应了动态通知调用。
最后创建调用链对象,执行通知和原始方法:。
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 )
动态通知调用需要切点信息,需要对参数进行匹配和绑定,复杂程度高,性能比静态通知调用低。
RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter 20.1 DispatcherServlet 的初始化 选择支持内嵌Tomcat 服务器的 Spring 容器作为 ApplicationContext 的实现:
public static void main (String[] args) { AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext (WebConfig.class); }
WebConfig 作为配置类,向 Spring 容器中添加内嵌 Web 容器工厂、DispatcherServlet 和 DispatcherServlet 注册对象。
@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) { return new DispatcherServletRegistrationBean (dispatcherServlet, "/" ); } }
运行 main() 方法,控制台打印出:
Tomcat initialized with port (s) : 8080 (http) Root WebApplicationContext: initialization completed in 2132 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); 的所在行:
protected void onRefresh (ApplicationContext context) { this .initStrategies(context); }
以 DEBUG 方式重启程序,此时程序尚未执行到断点处。
再次在浏览器中访问 localhost:8080,程序执行到断点处。
查看调用栈可知,是从 GenericServlet 的 init() 方法执行到 onRefresh() 方法的:
public void init (ServletConfig config) throws ServletException { this .config = config; this .init(); }
因此 DispatcherServlet 的初始化流程走的是 Servlet 的初始化流程。
使 DispatcherServlet 在Tomcat 服务器启动时被初始化
修改添加到 Spring 容器的 DispatcherServlet 注册 Bean:
@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 注解标记的方法的参数直接在方法中使用即可:
server.port=9090 spring.mvc.servlet.load-on-startup=1 @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() 方法:
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() 为例:
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 容器:
@Bean public RequestMappingHandlerMapping requestMappingHandlerMapping () { return new RequestMappingHandlerMapping (); }
定义一个控制器类:
@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,再获取请求路径与映射器方法的映射关系,并根据给定请求获取控制器方法:
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((k, v) -> System.out.println(k + " = " + v)); HandlerExecutionChain chain = handlerMapping.getHandler(new MockHttpServletRequest ("GET" , "/test1" )); System.out.println(chain); } {GET [/test1]} = indi.lcp.a20.Controller1#test1() {POST [/test2]} = indi.lcp.a20.Controller1#test2(String) {PUT [/test3]} = indi.lcp.a20.Controller1#test3(String) { [/test4]} = indi.lcp.a20.Controller1#test4() HandlerExecutionChain with [indi.lcp.a20.Controller1#test1()] and 0 interceptors
getHandler() 方法返回的对象时处理器执行链,不仅包含映射器方法,还包含需要执行的拦截器信息
需要导入以下依赖:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> </dependency>
20.3 RequestMappingHandlerAdapter RequestMappingHandlerAdapter 实现了 HandlerAdapter 接口,HandlerAdapter 用于执行控制器方法,而 RequestMapping 表明 RequestMappingHandlerAdapter 用于执行被 @RequestMapping 注解标记的控制器方法。
同样需要在配置类中将 RequestMappingHandlerAdapter 添加到 Spring 容器,但该类中需要测试的方法被 protected 修饰,无法直接使用,因此创建一个子类,将子类添加到 Spring 容器中:
public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter { @Override public ModelAndView invokeHandlerMethod (HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { return super .invokeHandlerMethod(request, response, handlerMethod); } } @Bean public MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter () { return new MyRequestMappingHandlerAdapter (); }
在 main() 方法中测试 RequestMappingHandlerAdapter 的 invokeHandlerMethod() 方法:
public static void main (String[] args) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest ("POST" , "/test2" ); request.setParameter("name" , "lcp" ); MockHttpServletResponse response = new MockHttpServletResponse (); MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class); handlerAdapter.invokeHandlerMethod(request, response, ((HandlerMethod) handlerMapping.getHandler(request).getHandler())); }
实现控制器方法的调用很简单,但如何将请求参数与方法参数相绑定的呢?
显然是需要解析 @RequestParam 注解。
Spring 支持许多种类的控制器方法参数,不同种类的参数使用不同的解析器,使用 MyRequestMappingHandlerAdapter 的 getArgumentResolvers() 方法获取所有参数解析器。
Spring 也支持许多种类的控制器方法返回值类型,使用 MyRequestMappingHandlerAdapter 的 getReturnValueHandlers() 方法获取所有返回值处理器。
自定义参数解析器
假如经常需要使用到请求头中的 Token 信息,自定义 @Token 注解,使用该注解标记控制器方法的哪个参数来获取 Token 信息:
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface Token {}
使 test3() 控制器方法参数被 @Token 标记:
@PutMapping("/test3") public ModelAndView test3 (@Token String token) { log.debug("test3({})" , token); return null ; }
自定义参数解析器:
public class TokenArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter (MethodParameter parameter) { return parameter.getParameterAnnotation(Token.class) != null ; } @Override public Object resolveArgument (MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { return webRequest.getHeader("token" ); } }
将参数解析器添加到 HandlerAdapter 中:
@Bean public MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter () { TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver (); MyRequestMappingHandlerAdapter adapter = new MyRequestMappingHandlerAdapter (); adapter.setCustomArgumentResolvers(Collections.singletonList(tokenArgumentResolver)); return adapter; }
测试执行 test3() 控制器方法:
public static void main (String[] args) throws Exception { MockHttpServletRequest tokenRequest = new MockHttpServletRequest ("PUT" , "/test3" ); tokenRequest.addHeader("token" , "token info" ); handlerAdapter.invokeHandlerMethod(tokenRequest, response, ((HandlerMethod) handlerMapping.getHandler(tokenRequest).getHandler())); }
自定义返回值处理器
当 @ResponseBody 标记了控制器方法时,方法的返回值会转换成 JSON 写入响应体中。
自定义 @Yml 注解,被 @Yml 注解标记的控制器方法的返回值会转换成 YAML 写入响应体中。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Yml {}
使 test4() 控制器方法被 @Yml 注解标记:
@RequestMapping("/test4") @Yml public User test4 () { log.debug("test4" ); return new User ("张三" , 18 ); }
自定义返回值处理器将返回值转换成 YAML:
public class YmlReturnValueHandler implements HandlerMethodReturnValueHandler { @Override public boolean supportsReturnType (MethodParameter returnType) { return returnType.getMethodAnnotation(Yml.class) != 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 中:
@Bean public MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter () { MyRequestMappingHandlerAdapter adapter = new MyRequestMappingHandlerAdapter (); YmlReturnValueHandler ymlReturnValueHandler = new YmlReturnValueHandler (); adapter.setCustomReturnValueHandlers(Collections.singletonList(ymlReturnValueHandler)); return adapter; }
测试执行 test4() 控制器方法:
public static void main (String[] args) throws Exception { MockHttpServletRequest test4Req = new MockHttpServletRequest ("GET" , "/test4" ); handlerAdapter.invokeHandlerMethod(test4Req, response, ((HandlerMethod) handlerMapping.getHandler(test4Req).getHandler())); byte [] content = response.getContentAsByteArray(); System.out.println(new String (content, StandardCharsets.UTF_8)); }
参数解析器 Spring 提供了许多种类的控制器方法参数解析器,定义一个包含多个不同种类参数的控制器方法:
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 ) { } } @Getter @Setter @ToString static class User { private String name; private int age; }
将控制器方法封装成 HandlerMethod 并打印方法中每个参数的信息:
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 appendAt = annotations.length() > 0 ? "@" + annotations + " " : "" ; parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer ()); System.out.println("[" + parameter.getParameterIndex() + "] " + appendAt + 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
21.1. @RequestParam @RequestParam 注解的解析需要使用到 RequestParamMethodArgumentResolver 参数解析器。构造时需要两个参数:
beanFactory:Bean 工厂对象。需要解析 ${} 时,就需要指定 Bean 工厂对象 useDefaultResolution:布尔类型参数。为 false 表示只解析添加了 @RequestParam 注解的参数,为 true 针对未添加 @RequestParam 注解的参数也使用该参数解析器进行解析。 RequestParamMethodArgumentResolver 利用 resolveArgument() 方法完成参数的解析,该方法需要传递四个参数:
parameter:参数对象 mavContainer:ModelAndView 容器,用来存储中间的 Model 结果 webRequest:由 ServletWebRequest 封装后的请求对象 binderFactory:数据绑定工厂,用于完成对象绑定和类型转换,比如将字符串类型的 18 转换成整 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 appendAt = annotations.length() > 0 ? "@" + annotations + " " : "" ; parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer ()); String paramInfo = "[" + parameter.getParameterIndex() + "] " + appendAt + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName(); if (resolver.supportsParameter(parameter)) { Object v = resolver.resolveArgument(parameter, container, new ServletWebRequest (request), binderFactory); System.out.println(Objects.requireNonNull(v).getClass()); System.out.println(paramInfo + " -> " + v); } else { System.out.println(paramInfo); } } } class java .lang.String[0 ] @RequestParam String name1 -> zhangsan class java .lang.String[1 ] String name2 -> lisi class java .lang.Integer[2 ] @RequestParam int age -> 18 class java .lang.String[3 ] @RequestParam String home1 -> D:\environment\JDK1.8 class org .springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile[4 ] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@f2ff811 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.
控制器方法 test() 的前 5 个参数解析成功,但在解析第 6 个参数时产生了异常。
这是因为在构造 RequestParamMethodArgumentResolver 对象时,将 useDefaultResolution 设置为 true,针对未添加 @RequestParam 注解的参数都使用该参数解析器进行解析。第 6 个参数需要的 id 信息使用该解析器解析得到的结果是 null,无法将 null 值赋值给基本类型 int,显然第 6 个及其以后的参数应该使用其他参数解析器进行解析。
多个参数解析器的组合 - 组合模式
不同种类的参数需要不同的参数解析器,当前使用的参数解析器不支持当前参数的解析时,就应该换一个参数解析器进行解析。
可以将所有参数解析器添加到一个集合中,然后遍历这个集合,实现上述需求。
Spring 提供了名为 HandlerMethodArgumentResolverComposite 的类,对上述逻辑进行封装。
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 v = composite.resolveArgument(parameter, container, new ServletWebRequest (request), binderFactory); System.out.println(paramInfo + " -> " + v); } else { System.out.println(paramInfo); } } }
21.2 @PathVariable @PathVariable 注解的解析需要使用到 PathVariableMethodArgumentResolver 参数解析器。构造时无需传入任何参数。
使用该解析器需要一个 Map 集合,该 Map 集合是 @RequestMapping 注解上指定的路径和实际 URL 路径进行匹配后,得到的路径上的参数与实际路径上的值的关系(获取这个 Map 并将其设置给 request 作用域由 HandlerMapping 完成)。
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 [2 ] @RequestParam int age -> 18 [3 ] @RequestParam String home1 -> D:\environment\JDK1.8 [4 ] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@11c9af63 [5 ] @PathVariable int id -> 123
@RequestHeader 注解的解析需要使用到 RequestHeaderMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。
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 [2 ] @RequestParam int age -> 18 [3 ] @RequestParam String home1 -> D:\environment\JDK1.8 [4 ] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@3943a2be [5 ] @PathVariable int id -> 123 [6 ] @RequestHeader String header -> application/json
21.4 @CookieValue @CookieValue 注解的解析需要使用到 ServletCookieValueMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。
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 [2 ] @RequestParam int age -> 18 [3 ] @RequestParam String home1 -> D:\environment\JDK1.8 [4 ] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@1329eff [5 ] @PathVariable int id -> 123 [6 ] @RequestHeader String header -> application/json [7 ] @CookieValue String token -> 123456
21.5 @Value @Value 注解的解析需要使用到 ExpressionValueMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。
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 [2 ] @RequestParam int age -> 18 [3 ] @RequestParam String home1 -> D:\environment\JDK1.8 [4 ] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@46fa7c39 [5 ] @PathVariable int id -> 123 [6 ] @RequestHeader String header -> application/json [7 ] @CookieValue String token -> 123456 [8 ] @Value String home2 -> D:\environment\JDK1.8
21.6 HttpServletRequest HttpServletRequest 类型的参数的解析需要使用到 ServletRequestMethodArgumentResolver 参数解析器。构造时无需传入任何参数。
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 [2 ] @RequestParam int age -> 18 [3 ] @RequestParam String home1 -> D:\environment\JDK1.8 [4 ] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@5f683daf [5 ] @PathVariable int id -> 123 [6 ] @RequestHeader String header -> application/json [7 ] @CookieValue String token -> 123456 [8 ] @Value String home2 -> D:\environment\JDK1.8 [9 ] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@152aa092
ServletRequestMethodArgumentResolver 参数解析器不仅可以解析 HttpServletRequest 类型的参数,还支持许多其他类型的参数,其支持的参数类型可在 supportsParameter() 方法中看到:
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 值,在未显式指定的情况下,默认为对象类型的首字母小写对应的字符串。
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 ) { } } 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 [2 ] @RequestParam int age -> 18 [3 ] @RequestParam String home1 -> D:\environment\JDK1.8 [4 ] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@2beee7ff [5 ] @PathVariable int id -> 123 [6 ] @RequestHeader String header -> application/json [7 ] @CookieValue String token -> 123456 [8 ] @Value String home2 -> D:\environment\JDK1.8 [9 ] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5fa07e12 [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} [12 ] @RequestBody User user3 -> A21.User(name=李四, age=20 ) 模型数据: {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 参数解析器。构造时需要传入一个消息转换器列表。
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 [2 ] @RequestParam int age -> 18 [3 ] @RequestParam String home1 -> D:\environment\JDK1.8 [4 ] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@5e17553a [5 ] @PathVariable int id -> 123 [6 ] @RequestHeader String header -> application/json [7 ] @CookieValue String token -> 123456 [8 ] @Value String home2 -> D:\environment\JDK1.8 [9 ] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@13bc8645 [10 ] @ModelAttribute User user1 -> A21.User(name=张三, age=18 ) [11 ] User user2 -> A21.User(name=张三, age=18 ) [12 ] @RequestBody User user3 -> A21.User(name=李四, age=20 )
@RequestBody User user3 参数数据通过 JSON 数据得到,与上一节的解析进行区分。
除此之外,添加的参数解析器顺序也影响着解析结果:
new ServletModelAttributeMethodProcessor (false ),new RequestResponseBodyMethodProcessor (Collections.singletonList(new MappingJackson2HttpMessageConverter ())),new ServletModelAttributeMethodProcessor (true )
先添加解析 @ModelAttribute 注解的解析器,再添加解析 @RequestBody 注解的解析器,最后添加解析省略了 @ModelAttribute 注解的解析器。如果更换最后两个解析器的顺序,那么 @RequestBody User user3 将会被 ServletModelAttributeMethodProcessor 解析,而不是 RequestResponseBodyMethodProcessor。
因此 String name2 参数也能通过添加同种参数但不同构造参数的解析器进行解析,注意添加的解析器的顺序,先处理对象,再处理单个参数:
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 -> D:\environment\JDK1.8 [4 ] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@5e17553a [5 ] @PathVariable int id -> 123 [6 ] @RequestHeader String header -> application/json [7 ] @CookieValue String token -> 123456 [8 ] @Value String home2 -> D:\environment\JDK1.8 [9 ] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@13bc8645 [10 ] @ModelAttribute User user1 -> A21.User(name=张三, age=18 ) [11 ] User user2 -> A21.User(name=张三, age=18 ) [12 ] @RequestBody User user3 -> A21.User(name=李四, age=20 )
获取参数名 在项目的 src 目录外创建一个 Bean2.java 文件,使其不会被 IDEA 自动编译:
package indi.lcp.a22;public class Bean2 { public void foo (String name, int age) { } }
将命令行切换到 Bean2.java 文件所在目录的位置,执行 javac .\Bean2.java 命令手动编译 Bean2.java。查看 Bean2.class 文件的内容:
package indi.lcp.a22;1 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 文件内容:
package indi.lcp.a22;public class Bean2 { public Bean2 () { } public void foo (String name, int age) { } }
foo() 方法的参数名称得以保留。
还可以使用 javap -c -v .\Bean2.class 命令反编译 Bean2.class,foo() 方法的反编译结果如下:
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 6 : 0 MethodParameters: Name Flags name age
foo() 方法的参数信息被保存在 MethodParameters 中,可以使用 反射 获取:
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() 方法的反编译结果如下:
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 6 : 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 this Lindi/lcp/a22/Bean2; 0 1 1 name Ljava/lang/String; 0 1 2 age I
foo() 方法的参数信息被保存在 LocalVariableTable 中,不能使用反射获取,但可以使用 ASM 获取,使用 Spring 封装的解析工具:
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 将两种实现进行了统一:
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer { public DefaultParameterNameDiscoverer () { if (KotlinDetector.isKotlinReflectPresent() && !NativeDetector.inNativeImage()) { addDiscoverer(new KotlinReflectionParameterNameDiscoverer ()); } addDiscoverer(new StandardReflectionParameterNameDiscoverer ()); addDiscoverer(new LocalVariableTableParameterNameDiscoverer ()); } }
javac -g 的局限性
假设有这样一个接口:
package indi.lcp.a22;public interface Bean1 { public void foo (String name, int age) ; }
如果使用 javac -g .\Bean1.java 命令进行编译后,再利用 javap 查看 foo() 方法的反编译结果:
public abstract void foo (java.lang.String, int ) ; descriptor: (Ljava/lang/String;I)V flags: ACC_PUBLIC, ACC_ABSTRACT
并没有记录抽象方法 foo() 的参数信息。
如果使用 javac -parameters .\Bean1.java 呢?
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.1 三种转换接口 底层第一套转换接口与实现
Printer 把其它类型转为 String
Parser 把 String 转为其它类型
Formatter 综合 Printer 与 Parser 的功能
Converter 把类型 S 转为类型 T
Printer、Parser、Converter 经过适配转换成 GenericConverter 放入 Converters 集合
FormattingConversionService 利用其它接口实现转换
底层第二套转换接口
由 JDK 提供,而不是 Spring。
PropertyEditor 将 String 与其它类型相互转换
PropertyEditorRegistry 可以注册多个 PropertyEditor 对象
可以通过 FormatterPropertyEditorAdapter 与第一套接口进行适配
高层转换接口与实现
它们都实现了 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
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
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 ) 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 ) 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() 方法使数据绑定逻辑走字段赋值,而不是属性赋值:
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。
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 绑定器工厂 现有如下两个类:
@Getter @Setter @ToString public static class User { private Date birthday; private Address address; } @Getter @Setter @ToString public static class Address { private String name; }
在 Web 环境下进行数据绑定:
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); }
birthday 和 address.name 都能绑定成功吗?
TestServletDataBinderFactory.User(birthday=null , address=TestServletDataBinderFactory.Address(name=成都))
birthday 绑定失败,要想使其绑定成功,需要自定义转换器,有两种方式:
使用 Spring 提供的 ConversionService
使用 JDK 提供的 PropertyEditorRegistry
创建 DataBinder 的职责交由 DataBinderFactory 完成,以便添加各种选项,拓展不同的自定义转换器。
@SneakyThrows public static void main (String[] args) { 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 注解标记的方法:
static class MyController { @InitBinder public void myMethod (WebDataBinder dataBinder) { dataBinder.addCustomFormatter(new MyDateFormatter ("用 @InitBinder 进行拓展" )); } }
以 WebDataBinder 作为方法参数,在方法体内添加自定义转换器 MyDateFormatter:
@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 对象:
@SneakyThrows public static void main (String[] args) { InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod (new MyController (), MyController.class.getMethod("myMethod" , WebDataBinder.class)); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory (Collections.singletonList(handlerMethod), null ); }
再次执行 main(),birthday 被成功绑定:
[DEBUG] indi.lcp.a23.MyDateFormatter - >>>>>> 进入了: 用 @InitBinder 进行拓展 TestServletDataBinderFactory.User(birthday=Sat Jan 02 00 :00 :00 CST 1999 , address=TestServletDataBinderFactory.Address(name=成都))
这种方式使用了 JDK 提供的 PropertyEditorRegistry,证据就在 WebDataBinder 的 addCustomFormatter() 方法中:
public void addCustomFormatter (Formatter<?> formatter) { FormatterPropertyEditorAdapter adapter = new FormatterPropertyEditorAdapter (formatter); getPropertyEditorRegistry().registerCustomEditor(adapter.getFieldType(), adapter); }
ConversionService 拓展
选择 FormattingConversionService 作为 ConversionService 的实现,向其中添加自定义转换器 MyDateFormatter。
构造 DataBinderFactory 时传入 WebBindingInitializer 的实现,因此将 FormattingConversionService 封装成 ConfigurableWebBindingInitializer 传入 DataBinderFactory 的构造方法中:
@SneakyThrows public static void main (String[] args) { FormattingConversionService service = new FormattingConversionService (); service.addFormatter(new MyDateFormatter ("用 ConversionService 方式拓展转换功能" )); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer (); initializer.setConversionService(service); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory (null , initializer); } [DEBUG] indi.lcp.a23.MyDateFormatter - >>>>>> 进入了: 用 ConversionService 方式拓展转换功能 TestServletDataBinderFactory.User(birthday=Sat Jan 02 00 :00 :00 CST 1999 , address=TestServletDataBinderFactory.Address(name=成都))
如果同时存在 @InitBinder 和 ConversionService,将以 @InitBinder 为主,@InitBinder 实现的转换器属于自定义转换器,自定义转换器的优先级更高:
@SneakyThrows public static void main (String[] args) { FormattingConversionService service = new FormattingConversionService (); service.addFormatter(new MyDateFormatter ("用 ConversionService 方式拓展转换功能" )); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer (); initializer.setConversionService(service); InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod (new MyController (), MyController.class.getMethod("myMethod" , WebDataBinder.class)); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory (Collections.singletonList(handlerMethod), initializer); } [DEBUG] indi.lcp.a23.MyDateFormatter - >>>>>> 进入了: 用 @InitBinder 进行拓展 TestServletDataBinderFactory.User(birthday=Sat Jan 02 00 :00 :00 CST 1999 , address=TestServletDataBinderFactory.Address(name=成都))
默认的 ConversionService
ConversionService 有一个默认实现 DefaultFormattingConversionService,它还是 FormattingConversionService 的子类:
@SneakyThrows public static void main (String[] args) { DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService (); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer (); initializer.setConversionService(conversionService); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory (null , initializer); }
运行 main() 方法后,控制台打印出:
TestServletDataBinderFactory.User(birthday=null , address=TestServletDataBinderFactory.Address(name=成都))
birthday 绑定失败,默认的 ConversionService 需要搭配注解 @DateTimeFormat 使用。在目标类的字段上使用该注解标记,并指定被转换的日期格式:
@Getter @Setter @ToString public static class User { @DateTimeFormat(pattern = "yyyy|MM|dd") private Date birthday; private Address address; }
再次运行 main() 方法:
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00 :00 :00 CST 1999 , address=TestServletDataBinderFactory.Address(name=成都))
在 SpringBoot 中还提供了 ApplicationConversionService,它也是 FormattingConversionService 的子类,上述代码将 DefaultFormattingConversionService 换成 ApplicationConversionService 也能达到相同效果。
23.4 Spring 的泛型操作技巧 有一基类 BaseDao,接收一个泛型参数:
public class BaseDao <T> { T findOne () { return null ; } }
围绕 BaseDao 有如下五个类:
public class EmployeeDao extends BaseDao {} public class Student {} public class StudentDao extends BaseDao <Student> {} public class Teacher {} public class TeacherDao extends BaseDao <Teacher> {}
尝试获取 BaseDao 子类泛型参数:
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: indi.lcp.a23.sub.BaseDao TeacherDao type class: class sun .reflect.generics.reflectiveObjects.ParameterizedTypeImpl EmployeeDao type: class indi .lcp.a23.sub.BaseDao EmployeeDao type class: class java .lang.Class class indi .lcp.a23.sub.Teacher>>>>>>>>>>>>>>>>>>>>>>> class indi .lcp.a23.sub.Teacher>>>>>>>>>>>>>>>>>>>>>>> class indi .lcp.a23.sub.Student
ControllerAdvice 之 @InitBinder 功能与使用
@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 foo () { } } }
当 @InitBinder 作用的方法存在于被 @ControllerAdvice 标记的类里面时,是对 所有 控制器都生效的自定义类型转换器。当 @InitBinder 作用的方法存在于被 @Controller 标记的类里面时,是 只对当前 控制器生效的自定义类型转换器。
@InitBinder 的来源有两个:
@ControllerAdvice 标记的类中 @InitBinder 标记的方法,由 RequestMappingHandlerAdapter 在初始化时解析并记录 @Controller 标记的类中 @InitBinder 标记的方法,由 RequestMappingHandlerAdapter 在控制器方法首次执行时解析并记录 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()) ); }
运行 main() 方法后,控制台打印出:
indi.lcp.a24.A24 - 1. 刚开始... indi.lcp.a24.A24 - 全局的 @InitBinder 方法 [binder3] indi.lcp.a24.A24 - 控制器的 @InitBinder 方法 []
全局的 @InitBinder 方法被解析并记录,但控制器中被 @InitBinder 标记的方法并没有被解析记录。
模拟调用控制器方法:
public static void main (String[] args) throws Exception { log.debug("1. 刚开始..." ); showBindMethods(handlerAdapter); Method getDataBinderFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getDataBinderFactory" , HandlerMethod.class); getDataBinderFactory.setAccessible(true ); log.debug("2. 模拟调用 Controller1 的 foo 方法..." ); getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod (new WebConfig .Controller1(), WebConfig.Controller1.class.getMethod("foo" ))); showBindMethods(handlerAdapter); log.debug("3. 模拟调用 Controller2 的 bar 方法时..." ); getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod (new WebConfig .Controller2(), WebConfig.Controller2.class.getMethod("bar" ))); showBindMethods(handlerAdapter); context.close(); } 1. 刚开始... 全局的 @InitBinder 方法 [binder3] 控制器的 @InitBinder 方法 [] 2. 模拟调用 Controller1 的 foo 方法... 全局的 @InitBinder 方法 [binder3] 控制器的 @InitBinder 方法 [Controller1.binder1] 3. 模拟调用 Controller2 的 bar 方法时... 全局的 @InitBinder 方法 [binder3] 控制器的 @InitBinder 方法 [Controller1.binder1, Controller2.binder22, Controller2.binder21]
首次调用控制器中的方法时,控制器中被 @InitBinder 标记方法被解析记录。
控制器方法执行流程 ServletInvocableHandlerMethod 的组成
HandlerMethod 需要:
bean,即哪个 Controller method,即 Controller 中的哪个方法 ServletInvocableHandlerMethod 需要:
WebDataBinderFactory,用于对象绑定、类型转换 ParameterNameDiscoverer,用于参数名解析 HandlerMethodArgumentResolverComposite,用于解析参数 HandlerMethodReturnValueHandlerComposite,用于处理返回值 控制器方法执行流程
以 RequestMappingHandlerAdapter 为起点,创建 WebDataBinderFactory,添加自定义类型转换器,再创建 ModelFactory,添加 Model 数据
接下来调用 ServletInvocableHandlerMethod,主要完成三件事:
ControllerAdvice 之 @ModelAttribute 准备 @ModelAttribute 在整个 HandlerAdapter 调用过程中所处的位置
功能与使用
@ModelAttribute 可以作用在参数上和方法上。
当其作用在参数上时,会将请求中的参数信息 按名称 注入到指定对象中,并将这个对象信息自动添加到 ModelMap 中。当未指定 @ModelAttribute 的 value 时,添加到 ModelMap 中的 key 是对象类型首字母小写对应的字符串。此时的 @ModelAttribute 注解由 ServletModelAttributeMethodProcessor 解析。
当其作用在方法上时:
如果该方法在被 @Controller 注解标记的类中,会在当前控制器中每个控制器方法执行前执行被 @ModelAttribute 标记的方法,如果该方法有返回值,自动将返回值添加到 ModelMap 中。当未指定 @ModelAttribute 的 value 时,添加到 ModelMap 中的 key 是返回值类型首字母小写对应的字符串。
如果该方法在被 @ControllerAdvice 注解标记的类中,会在所有控制器方法执行前执行该方法。
作用在方法上的 @ModelAttribute 注解由 RequestMappingHandlerAdapter 解析。
@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() 方法:
foo {u=WebConfig.User(name=lcp), org.springframework.validation.BindingResult.u=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
再解析方法上的 @ModelAttribute 注解:
@SneakyThrows public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (WebConfig.class); RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter (); adapter.setApplicationContext(context); adapter.afterPropertiesSet(); MockHttpServletRequest request = new MockHttpServletRequest (); request.setParameter("name" , "lcp" ); Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory" , HandlerMethod.class, WebDataBinderFactory.class); getModelFactory.setAccessible(true ); ModelFactory modelFactory = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, binderFactory); modelFactory.initModel(new ServletWebRequest (request), container, handlerMethod); } foo {a=aa, u=WebConfig.User(name=lcp), org.springframework.validation.BindingResult.u=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
{a=aa} 也被放入到 ModelAndViewContainer 中。
返回值处理器 含有多种返回值的控制器
@Slf4j static class Controller { public ModelAndView test1 () { log.debug("test1()" ); ModelAndView mav = new ModelAndView ("view1" ); mav.addObject("name" , "张三" ); return mav; } public String test2 () { log.debug("test2()" ); return "view2" ; } @ModelAttribute public User test3 () { log.debug("test3()" ); return new User ("李四" , 20 ); } public User test4 () { log.debug("test4()" ); return new User ("王五" , 30 ); } public HttpEntity<User> test5 () { log.debug("test5()" ); return new HttpEntity <>(new User ("赵六" , 40 )); } public HttpHeaders test6 () { log.debug("test6()" ); HttpHeaders headers = new HttpHeaders (); headers.add("Content-Type" , "text/html" ); return headers; } @ResponseBody public User test7 () { log.debug("test7()" ); return new User ("钱七" , 50 ); } } @Getter @Setter @ToString @AllArgsConstructor public static class User { private String name; private int age; }
测试渲染视图需要用到的配置
为测试对视图的渲染,采用 Freemarker 进行测试,先导入 Freemarker 依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency>
Freemarker 配置类:
@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; } }
渲染视图使用的方法:
@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 对象的方法
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 的响应信息。
@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 中,之后根据视图信息找到对应的模板页面,再将模型数据填充到模板页面中,完成视图的渲染。
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:
<!doctype html> <html lang="zh" > <head> <meta charset="UTF-8" > <title>view1</title> </head> <body> <h1>Hello! ${name}</h1> </body> </html> indi.lcp.a27.A27$Controller - test1() {name=张三} view1 indi.lcp.a27.A27 - >>>>>> 渲染视图 indi.lcp.a27.A27 - 没有获取到视图名, 采用默认视图名: view1 indi.lcp.a27.WebConfig$1 $1 - View name 'view1' , model {name=张三} indi.lcp.a27.WebConfig$1 $1 - Rendering [/view1.ftl] <!doctype html> <html lang="zh" > <head> <meta charset="UTF-8" > <title>view1</title> </head> <body> <h1>Hello! 张三</h1> </body> </html>
27.2 字符串类型 控制器方法的返回值是字符串类型时,返回的字符串即为视图的名称。与 ModelAndView 类型的返回值相比,不包含模型数据。
此种类型的返回值由 ViewNameMethodReturnValueHandler 处理,构造时无需传入任何参数。
private static void test2 (ApplicationContext context) { testReturnValueProcessor(context, "test2" , null , null ); }
对应的模板页面 view2.ftl:
<!doctype html> <html lang="zh" > <head> <meta charset="UTF-8" > <title>view2</title> </head> <body> <h1>Hello!</h1> </body> </html> indi.lcp.a27.A27$Controller - test2() {} view2 indi.lcp.a27.A27 - >>>>>> 渲染视图 indi.lcp.a27.A27 - 没有获取到视图名, 采用默认视图名: view2 indi.lcp.a27.WebConfig$1 $1 - View name 'view2' , model {} indi.lcp.a27.WebConfig$1 $1 - Rendering [/view2.ftl] <!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 标记,并指定请求地址,比如:
@ModelAttribute @RequestMapping("/test3") public User test3 () { log.debug("test3()" ); return new User ("李四" , 20 ); }
当未找到视图名称时,默认以请求路径作为视图名称。
但在本节测试中省略了路径映射这一步,因此需要通过编程的方式将请求路径解析后的结果放入 request 作用域中。
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:
<!doctype html> <html lang="zh" > <head> <meta charset="UTF-8" > <title>test3</title> </head> <body> <h1>Hello! ${user.name} ${user.age}</h1> </body> </html> indi.lcp.a27.A27$Controller - test3() {user=A27.User(name=李四, age=20 )} null indi.lcp.a27.A27 - >>>>>> 渲染视图 indi.lcp.a27.A27 - 没有获取到视图名, 采用默认视图名: test3 indi.lcp.a27.WebConfig$1 $1 - View name 'test3' , model {user=A27.User(name=李四, age=20 )} indi.lcp.a27.WebConfig$1 $1 - Rendering [/test3.ftl] <!doctype html> <html lang="zh" > <head> <meta charset="UTF-8" > <title>test3</title> </head> <body> <h1>Hello! 李四 20 </h1> </body> </html>
针对控制器方法 test4() 也可以按照相同方式测试:
private static void test4 (ApplicationContext context) { String methodName = "test4" ; testReturnValueProcessor(context, methodName, mockHttpServletRequestConsumer(methodName), null ); }
对应的模板页面 test4.ftl:
<!doctype html> <html lang="zh" > <head> <meta charset="UTF-8" > <title>test4</title> </head> <body> <h1>Hello! ${user.name} ${user.age}</h1> </body> </html> indi.lcp.a27.A27$Controller - test4() {user=A27.User(name=王五, age=30 )} null indi.lcp.a27.A27 - >>>>>> 渲染视图 indi.lcp.a27.A27 - 没有获取到视图名, 采用默认视图名: test4 indi.lcp.a27.WebConfig$1 $1 - View name 'test4' , model {user=A27.User(name=王五, age=30 )} indi.lcp.a27.WebConfig$1 $1 - Rendering [/test4.ftl] <!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() 方法中得以论证:
public void handleReturnValue (@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { mavContainer.setRequestHandled(true ); }
HttpEntity 中包含了状态码、响应体信息和响应头信息。
尝试在请求处理完毕后,输出响应体信息:
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); } indi.lcp.a27.A27$Controller - test5() {} null Content-Type = application/json {"name" :"赵六" ,"age" :40 }
与 HttpEntity 相比,HttpHeaders 只包含响应头信息,HttpHeaders 类型的返回值由 HttpHeadersReturnValueHandler 处理,构造时无需传入任何参数。
与 HttpEntity 一样,这种类型的返回值也表示响应完成,无需经过视图的解析、渲染流程再生成响应,也可在处理器的 handleReturnValue() 方法中得以论证(省略源码)。
private static void test6 (ApplicationContext context) { testReturnValueProcessor(context, "test6" , null , RESPONSE_CONSUMER); } indi.lcp.a27.A27$Controller - test6() {} null Content-Type = text/html
27.6 @ResponseBody @ResponseBody 标记的方法的返回值由 RequestResponseBodyMethodProcessor 处理,构造时需要传入一个消息转换器列表。
这样的返回值也表示响应完成,无需经过视图的解析、渲染流程再生成响应,也可在处理器的 handleReturnValue() 方法中得以论证(省略源码)。
private static void test7 (ApplicationContext context) { testReturnValueProcessor(context, "test7" , null , RESPONSE_CONSUMER); } indi.lcp.a27.A27$Controller - test7() {} null Content-Type = application/json {"name" :"钱七" ,"age" :50 }
消息转换器 在构造参数解析器 RequestResponseBodyMethodProcessor、返回值解析器 HttpEntityMethodProcessor 和 HttpEntityMethodProcessor 时,都需要传入消息转换器列表。
消息转换器的基类是 HttpMessageConverter。
介绍两个常见的消息转换器的实现:
MappingJackson2XmlHttpMessageConverter MappingJackson2XmlHttpMessageConverter 一个 User 类:
@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 格式的数据:
@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 格式的数据:
@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 时,需要额外导入依赖:
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency> <User><name>李四</name><age>20 </age></User>
将 JSON 格式的数据转换成 User 对象:
@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 )
如果存在多个消息转换器呢?
@SneakyThrows public static void test4 () { MockHttpServletRequest request = new MockHttpServletRequest (); MockHttpServletResponse response = new MockHttpServletResponse (); ServletWebRequest webRequest = new ServletWebRequest (request, response); request.addHeader(HttpHeaders.ACCEPT, MimeTypeUtils.APPLICATION_XML_VALUE); response.setContentType(MimeTypeUtils.APPLICATION_JSON_VALUE); 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 格式的数据:
调换添加的消息转换器顺序:
@SneakyThrows public static void test4 () { RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor (Arrays.asList( new MappingJackson2XmlHttpMessageConverter (), new MappingJackson2HttpMessageConverter () )); }
这下会将 User 对象转换成 XML 格式的数据:
<User><name>张三</name><age>18 </age></User>
再将添加的消息转换器顺序还原,在请求头中添加 Accept 信息,指定数据格式为 XML:
@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 信息为主:
<User><name>张三</name><age>18 </age></User>
在上文基础上,在指定响应的 Content-Type 为 application/json:
@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 的信息为主:
总结
@ResponseBody 注解由 RequestResponseBodyMethodProcessor 解析,但涉及到的数据格式转换由消息转换器完成。
当存在多个消息转换器时,如果选择 MediaType:
首先看 @RequestMapping 注解的 produces 为主,相当于设置了响应的 Content-Type,比如: @ResponseBody @RequestMapping(produces = MimeTypeUtils.APPLICATION_JSON_VALUE) public User user () { return null ; }
再看请求头中的 Accept 是否指定了目标格式 最后按照消息转换器的添加顺序进行转换 ControllerAdvice 之 ResponseBodyAdvice ResponseBodyAdvice 是一个接口,对于实现了这个接口并被 @ControllerAdvice 标记的类来说,能够在调用每个控制器方法返回结果前,调用重写的 ResponseBodyAdvice 接口中的 beforeBodyWrite() 方法对返回值进行增强。
现有一个控制器类与内部使用到的 User 类:
@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; }
调用控制器方法,并输出响应数据:
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)); context.close(); }
运行 main() 方法后,控制台输出
在实际开发场景中常常需要对返回的数据类型进行统一,比如都返回 Result 类型:
@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 进行增强:
@ControllerAdvice static class MyControllerAdvice implements ResponseBodyAdvice <Object> { @Override public boolean supports (MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return returnType.getMethodAnnotation(ResponseBody.class) != null || AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null ; } @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 添加到返回值处理器中。
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 }}
如果将控制器方法修改成以下形式,也能输出相同的结果:
@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 ); } }
异常处理 DispatcherServlet 中对异常处理的核心方法是 processHandlerException(),在这个方法中会对所有异常解析器进行遍历,然后使用每个异常解析器对异常信息进行处理。
存放异常解析器的是 DispatcherServlet 中泛型为 HandlerExceptionResolver、名为 handlerExceptionResolvers 的列表成员变量。
HandlerExceptionResolver 是一个接口,本节讲解解析 @ExceptionHandler 注解的异常解析器 ExceptionHandlerExceptionResolver。
四个控制器类,测试异常处理方法被 @ResponseBody 注解标记、异常处理方法返回 ModelAndView、嵌套异常和对异常处理方法的参数处理:
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()); } } @SneakyThrows public static void main (String[] args) { ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver (); resolver.setMessageConverters(Collections.singletonList(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 ("除以零" ); resolver.resolveException(request, response, handlerMethod, e); System.out.println(new String (response.getContentAsByteArray(), StandardCharsets.UTF_8)); 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()); handlerMethod = new HandlerMethod (new Controller3 (), Controller3.class.getMethod("foo" )); e = new Exception ("e1" , new RuntimeException ("e2" , new IOException ("e3" ))); resolver.resolveException(request, response, handlerMethod, e); System.out.println(new String (response.getContentAsByteArray(), StandardCharsets.UTF_8)); handlerMethod = new HandlerMethod (new Controller4 (), Controller4.class.getMethod("foo" )); e = new Exception ("e4" ); resolver.resolveException(request, response, handlerMethod, e); System.out.println(new String (response.getContentAsByteArray(), StandardCharsets.UTF_8)); }
运行 main() 方法后,控制台打印出:
{"error" :"除以零" } {error=除以零} test2 {"error" :"除以零" }{"error" :"e3" } org.springframework.mock.web.MockHttpServletRequest@7c1e2a9e {"error" :"除以零" }{"error" :"e3" }{"error" :"e4" }
ControllerAdvice 之 @ExceptionHandler 控制器中被 @ExceptionHandler 标记的异常处理方法只会在当前控制器中生效,如果想要某个异常处理方法全局生效,则需要将异常处理方法编写在被 @ControllerAdvice 注解标记的类中。
一个“朴素”的控制器类:
static class Controller1 { public void foo () { } }
当不存在任何异常处理方法时,调用控制器中的 foo() 方法:
@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,并声明全局异常处理方法:
@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 容器中获取:
@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)); }
Tomcat 异常处理 可以利用 @ExceptionHandler 和 @ControllerAdvice 注解全局对控制器方法中抛出的异常进行处理,但针对诸如 filter 中不在控制器方法中的异常就变得无能为力了。
因此需要一个更上层的“异常处理者”,这个“异常处理者”就是 Tomcat 服务器。
32.1 Tomcat 的错误页处理 首先将“老三样”利用配置类添加到 Spring 容器中,还要将 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter 也添加到 Spring 容器中。
必要的控制器也不能少,控制器方法手动制造异常,但不提供使用 @ExceptionHandler 实现的异常处理方法,将产生的异常交由 Tomcat 处理:
@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 容器,并输出所有的路径映射信息:
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)); }
运行 main() 方法后,控制台只输出一条路径映射信息:
映射路径: { [/test]} 方法信息: indi.lcp.a32.WebConfig$MyController#test()
在浏览器中访问 http://localhost:8080/test 地址:
显示 Tomcat 的错误处理页,并在页面中输出了错误信息。
Tomcat 默认提供的错误处理方式返回的是 HTML 格式的数据,但需要返回 JSON 格式的数据又该怎么自定义呢?
修改 Tomcat 默认的错误处理路径,并添加后置处理器进行注册:
@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 格式的数据:
@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]} 方法信息: indi.lcp.a32.WebConfig$MyController#error(HttpServletRequest) 映射路径: { [/test]} 方法信息: indi.lcp.a32.WebConfig$MyController#test()
在浏览器中访问 http://localhost:8080/test :
32.2 BasicErrorController BasicErrorController 是由 SpringBoot 提供的类,它也是一个控制器:
@Controller @RequestMapping({"${server.error.path:${error.path:/error}}"}) public class BasicErrorController extends AbstractErrorController { }
它的映射路径会先从配置文件中读取,在未进行任何配置的情况下,默认路径是 /error。
向容器中添加 BasicErrorController,构造 BasicErrorController 时需要传递两个参数:
errorAttributes:错误属性,可以理解成封装的错误信息对象 errorProperties:也可以翻译成错误属性,用于对输出的错误信息进行配置 @Bean public BasicErrorController basicErrorController () { return new BasicErrorController (new DefaultErrorAttributes (), new ErrorProperties ()); }
移除前文添加的 error() 控制器方法。
再次重启程序,控制台输出的路径映射信息为:
映射路径: { [/error]} 方法信息: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest) 映射路径: { [/test]} 方法信息: indi.lcp.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 格式的数据,比如:
{ "timestamp" : 1674736682248 , "status" : 500 , "error" : "Internal Server Error" , "path" : "/test" }
timestamp、status 等响应内容就是错误属性 errorAttributes 的中包含的内容。
返回的数据中并没有显示异常信息,可以通过配置文件进行配置:
server.error.include-exception=true
也可以在添加 BasicErrorController 到 Spring 容器中时,设置错误属性 errorProperties:
@Bean public BasicErrorController basicErrorController () { ErrorProperties errorProperties = new ErrorProperties (); errorProperties.setIncludeException(true ); return new BasicErrorController (new DefaultErrorAttributes (), errorProperties); }
重启程序,再次使用接口测试工具访问 http://localhost:8080/test:
{ "timestamp" : 1674736991768 , "status" : 500 , "error" : "Internal Server Error" , "exception" : "java.lang.ArithmeticException" , "path" : "/test" }
使用浏览器访问
如果使用浏览器访问 http://localhost:8080/test,又会回到“解放前”,显示与 Tomcat 的默认错误处理页相同的内容。
这是因为使用浏览器访问时,将调用 BasicErrorController 中的 errorHtml() 控制器方法:
@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:
@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 的名称进行匹配,还需要添加一个解析器:
@Bean public ViewResolver viewResolver () { return new BeanNameViewResolver (); }
重启程序,使用浏览器访问 http://localhost:8080/test:
控制台还打印出:
{timestamp=Thu Jan 26 21 :01 :50 CST 2023 , status=500 , error=Internal Server Error, exception=java.lang.ArithmeticException, path=/test}
BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter 33.1 功能与使用 BeanNameUrlHandlerMapping 与 RequestMappingHandlerMapping 类似,也是用于解析请求路径,只不过 BeanNameUrlHandlerMapping 将根据请求路径在 Spring 容器中寻找同名的 Bean,对请求进行处理,这个 Bean 必须 以 / 开头。比如:请求路径为 /c1,寻找的 Bean 的名称也是 /c1。
SimpleControllerHandlerAdapter 与 RequestMappingHandlerAdapter 也类似,也是用于调用控制器方法,但要求控制器类必须实现 org.springframework.web.servlet.mvc.Controller 接口。
现有三个控制器类:
@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:
@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 BeanNameUrlHandlerMapping beanNameUrlHandlerMapping () { return new BeanNameUrlHandlerMapping (); } @Bean public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter () { return new SimpleControllerHandlerAdapter (); } } 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,使其不能被解析到。
@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。
RouterFunctionMapping 与 HandlerFunctionAdapter RouterFunctionMapping 在初始化时,在 Spring 容器中收集所有 RouterFunction,RouterFunction 包括两部分:
RequestPredicate:设置映射条件 HandlerFunction:处理逻辑 当请求到达时,根据映射条件找到 HandlerFunction,即 handler,然后使用 HandlerFunctionAdapter 调用 handler。
@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" )); } } public static void main (String[] args) { AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext (WebConfig.class); }
运行 main() 方法后,在浏览器中访问 http://localhost:8080/r1,页面上显示 this is r1,访问 r2 时也类似。
SimpleUrlHandlerMapping 与HttpRequestHandlerAdapter 35.1 功能与使用 概括一下,这两个主要用于静态资源处理,SimpleUrlHandlerMapping 用于静态资源映射,而静态资源处理器是 ResourceHttpRequestHandler,HttpRequestHandlerAdapter 用于处理器。
@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():
@Override public void afterPropertiesSet () throws Exception { resolveResourceLocations(); if (this .resourceResolvers.isEmpty()) { this .resourceResolvers.add(new PathResourceResolver ()); } }
当使用的资源解析器列表为空时,默认添加最基本的资源解析器 PathResourceResolver。
尝试添加额外的资源解析器:
@Bean("/**") public ResourceHttpRequestHandler handler1 () { ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler (); handler.setLocations(Collections.singletonList(new ClassPathResource ("static/" ))); handler.setResourceResolvers(Arrays.asList( new CachingResourceResolver (new ConcurrentMapCache ("cache1" )), new EncodedResourceResolver (), new PathResourceResolver () )); return handler; }
添加了三个资源解析器:
CachingResourceResolver:对静态资源进行缓存 EncodedResourceResolver:对静态资源进行压缩 PathResourceResolver:最基本的资源处理器 还要注意添加的顺序,先尝试从缓存中获取,再尝试获取压缩文件,最后才是直接从磁盘上读取。
针对 EncodedResourceResolver 来说,Spring 不会自行对静态资源进行压缩,需要在配置类中提供压缩方法:
@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 文件:
@Bean public WelcomePageHandlerMapping welcomePageHandlerMapping (ApplicationContext context) { Resource resource = context.getResource("classpath:static/index.html" ); return new WelcomePageHandlerMapping ( null , context, resource, "/**" ); }
程序会根据配置的欢迎页映射器生成一个实现了 Controller 接口的处理器,使用 SimpleControllerHandlerAdapter 执行生成的处理器:
@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 时,会直接访问静态资源 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() 方法是 责任链 模式体现。
MVC 处理流程 当浏览器发送一个请求 http://localhost:8080/hello 后,请求到达服务器,其处理流程是:
服务器提供了 DispatcherServlet,它使用的是标准 Servlet 技术
路径:默认映射路径为 /,即会匹配到所有请求 URL,可作为请求的统一入口,DispatcherServlet 也被称之为 前控制器。但也有例外:
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 调用处理器方法,准备数据绑定工厂、模型工厂、ModelAndViewContainer、将 HandlerMethod 完善为 ServletInvocableHandlerMethod
@ControllerAdvice 全局增强点 1️⃣:利用 @ModelAttribute 补充模型数据
@ControllerAdvice 全局增强点 2️⃣:利用 @InitBinder 补充自定义类型转换器
使用 HandlerMethodArgumentResolver 准备参数
@ControllerAdvice 全局增强点 3️⃣:利用 RequestBodyAdvice 接口对请求体增强
调用 ServletInvocableHandlerMethod
使用 HandlerMethodReturnValueHandler 处理返回值
@ControllerAdvice 全局增强点 4️⃣:利用 RequestBodyAdvice 对响应体增强
根据 ModelAndViewContainer 获取 ModelAndView
如果返回的 ModelAndView 为 null,不走第 4 步视图解析及渲染流程。例如返回值处理器调用了 HttpMessageConverter 将结果转换为 JSON,这时 ModelAndView 就为 null
如果返回的 ModelAndView 不为 null,会在第 4 步走视图解析及渲染流程
调用拦截器的 postHandle() 方法
处理异常或视图渲染
如果 1~3 步中出现异常,使用 ExceptionHandlerExceptionResolver 处理异常流程
@ControllerAdvice 全局增强点 5️⃣:利用 @ExceptionHandler 进行统一异常处理
未出现异常时,进行视图解析及渲染流程
调用拦截器的 afterCompletion() 方法
Boot 骨架项目 使用 IDEA 创建 SpringBoot 项目时,会创建出 .mvn 目录、HELP.md、mvnw 和 mvnw.cmd 等不必要的文件。
如果是 Linux 环境下,执行以下命令获取 SpringBoot 的骨架,并添加 web、mysql、mybatis 依赖:
也可以使用 Postman 等接口测试工具来实现。
更多用法执行以下命令进行参考:
Boot War 项目 38.1 项目的构建 利用 IDEA 创建新模块 test_war,区别在于选择的打包方式是 War:
选择依赖时,勾选 Spring Web。
一般来说,选择 War 作为打包方式都是为了使用 JSP,因为 JSP 不能配合 Jar 打包方式使用。
JSP 文件的存放路径是固定的,在 src/main 目录下的 webapp 目录,如果没有 webapp 目录,需要自行创建。之后新建 hello.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h3>Hello!</h3> </body> </html>
之后新建控制器类 HelloController,编写控制器方法 hello(),返回值类型是 String,要求返回的是视图名称:
@Controller public class HelloController { @RequestMapping("/hello") public String hello () { return "hello" ; } }
最后要在配置文件中配置视图的前缀、后缀,使控制器方法返回的视图名称对应视图名称的 JSP 页面:
spring.mvc.view.prefix=/ spring.mvc.view.suffix=.jsp
38.2 项目的测试 使用外置 Tomcat 测试
首先得安装外置 Tomcat,省略安装步骤。
然后在 IDEA 的 Run/Debug Configurations 中进行配置,选择安装的外置 Tomcat:
然后在 Deployment 中指定当前项目的部署方式和应用程序上下文路径:
尽管使用外置 Tomcat 进行测试,但主启动类不能少:
@SpringBootApplication public class TestWarApplication { public static void main (String[] args) { SpringApplication.run(TestWarApplication.class, args); } }
除此之外,还要编写 ServletInitializer,在外置 Tomcat 启动时,找到 SpringBoot 项目的主启动类,执行 SpringBoot 流程:
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 的能力,需要添加依赖:
<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
Boot 启动过程 39.1 SpringApplication 的构造 SpringBoot 的主启动类类似于:
@SpringBootApplication public class BootApplication { public static void main (String[] args) { SpringApplication.run(BootApplication.class, args); } }
其中 SpringApplication#run() 方法是核心方法:
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() 方法。
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 源
@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。向容器中添加即可:
@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 来源: indi.lcp.a39.A39_1 name: servletWebServerFactory 来源: indi.lcp.a39.A39_1
来源为 null 的 Bean 是由 Spring 提供的“内置” Bean。
使用 XML 配置文件添加 Bean,并利用 setSources() 方法设置创建 ApplicationContext 的其他源:
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]
推断应用类型
应用类型的推断在构造方法中可以看到:
public SpringApplication (ResourceLoader resourceLoader, Class<?>... primarySources) { this .webApplicationType = WebApplicationType.deduceFromClasspath(); }
推断逻辑由 WebApplicationType 枚举中的 deduceFromClasspath() 方法完成:
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() 方法:
@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 的构造方法中,添加的初始化器信息从配置文件中读取:
public SpringApplication (ResourceLoader resourceLoader, Class<?>... primarySources) { setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); }
也可以调用 SpringApplication 对象的 addInitializers() 方法添加自定义初始化器:
@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 的构造方法中,添加的事件监听器信息从配置文件中读取:
public SpringApplication (ResourceLoader resourceLoader, Class<?>... primarySources) { setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); }
可以调用 SpringApplication 对象的 addListeners() 方法添加自定义事件监听器:
@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
主类推断
主类推断在构造方法中可以看到:
public SpringApplication (ResourceLoader resourceLoader, Class<?>... primarySources) { this .mainApplicationClass = deduceMainApplicationClass(); }
推断逻辑由 deduceMainApplicationClass() 方法完成,利用反射调用该方法:
@SneakyThrows public static void main (String[] args) { Method deduceMainApplicationClass = SpringApplication.class.getDeclaredMethod("deduceMainApplicationClass" ); deduceMainApplicationClass.setAccessible(true ); System.out.println("\t主类是: " + deduceMainApplicationClass.invoke(spring)); } 主类是: class indi .lcp.a39.A39_1
39.2 SpringApplication#run() 的分析 第一步:获取 SpringApplicationRunListeners
在执行 run() 方法时,首先会获取到 SpringApplicationRunListeners,它是事件发布器的组合,能够在 SpringBoot 启动的各个阶段中发布事件。
SpringApplicationRunListeners 中使用 SpringApplicationRunListener 来描述单个事件发布器,SpringApplicationRunListener 是一个接口,它有且仅有一个实现类 EventPublishingRunListener。
在 SpringBoot 中,事件发布器都是在配置文件中读取,从 META-INF/spring.factories 中读取,该文件中有这样一句:
# Run Listeners org.springframework.boot.SpringApplicationRunListener=\ org.springframework.boot.context.event.EventPublishingRunListener
自行实现从 META-INF/spring.factories 配置文件中读取事件发布器信息,并发布各种事件:
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.ApplicationStartingEventclass org .springframework.boot.context.event.ApplicationEnvironmentPreparedEventclass org .springframework.boot.context.event.ApplicationContextInitializedEventclass org .springframework.boot.context.event.ApplicationPreparedEventclass org .springframework.context.event.ContextRefreshedEventclass org .springframework.boot.context.event.ApplicationStartedEventclass org .springframework.boot.availability.AvailabilityChangeEventclass org .springframework.boot.context.event.ApplicationReadyEventclass org .springframework.boot.availability.AvailabilityChangeEventclass org .springframework.boot.context.event.ApplicationFailedEvent
但打印出的事件种类并不止 7 种,这是因为包含了其他事件发布器发布的事件,EventPublishingRunListener 发布的事件的全限定类名包含 boot.context.event,根据这个条件重新计算,恰好 7 个。
第八到十一步:完成 Spring 容器的创建
第八步:创建容器。在构造 SpringApplication 时已经推断出应用的类型,使用应用类型直接创建即可。
第九步:准备容器。回调在构造 SpringApplication 时添加的初始化器。
第十步:加载 Bean 定义。从配置类、XML 配置文件读取 BeanDefinition,或者扫描某一包路径下的 BeanDefinition。
第十一步:调用 ApplicationContext 的 refresh() 方法,完成 Spring 容器的创建。
@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("indi.lcp.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; }
涉及到的配置类:
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 配置文件:
<?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="indi.lcp.a39.A39_3.Bean4" /> </beans>
indi.lcp.a39.sub 包下的 Bean 信息:
package indi.lcp.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\indi\lcp\a39\sub\Bean7.class] name: org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory 来源: null name: bean5 来源: indi.lcp.a39.A39_3$Config name: servletWebServerFactory 来源: indi.lcp.a39.A39_3$Config
第二步:封装启动 args
调用 DefaultApplicationArguments 的构造方法,传入 args 即可:
@SneakyThrows @SuppressWarnings("all") public static void main (String[] args) { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 2. 封装启动 args" ); DefaultApplicationArguments arguments = new DefaultApplicationArguments (args); }
第十二步:执行 Runner
在 SpringBoot 启动成功后,可以执行一些 Runner,进行一些预处理或测试。Runner 有两种,分别是 CommandLineRunner 和 ApplicationRunner:
@FunctionalInterface public interface CommandLineRunner { void run (String... args) throws Exception; } @FunctionalInterface public interface ApplicationRunner { void run (ApplicationArguments args) throws Exception; }
它们都是函数式接口,内部的抽象方法长得也很像,只不过:
CommandLineRunner 直接接收启动参数;
ApplicationRunner 则是接收封装后的 ApplicationArguments,即 第二步 封装的对象。
在配置类中添加这两种类型的 Bean:
@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:
@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]
第三步:准备 Environment 添加命令行参数
Environment 即环境对象,是对配置信息的抽象,配置信息的来源有多种,比如:系统环境变量、properties 配置文件、YAML 配置文件等等。
SpringBoot 提供了名为 ApplicationEnvironment 的类表示环境对象,它是 Spring 中 StandardEnvironment 环境对象的子类。
默认情况下,创建的 ApplicationEnvironment 对象中配置信息的来源只有两个:
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 的配置信息:
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() 方法,控制台打印出:
如果想从配置文件 application.properties 中读取配置信息,可以添加配置信息的来源。配置文件的优先级最低,添加来源时调用 addLast() 方法:
@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" )); } author.name="lcp" PropertiesPropertySource {name='systemProperties' } SystemEnvironmentPropertySource {name='systemEnvironment' } ResourcePropertySource {name='class path resource [application.properties]' } "lcp"
而在 SpringBoot 中,这里 只 添加 SimpleCommandLinePropertySource,并且它的优先级最高,使用 addFirst() 方法添加:
@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=默烦:
SimpleCommandLinePropertySource {name='commandLineArgs' } PropertiesPropertySource {name='systemProperties' } SystemEnvironmentPropertySource {name='systemEnvironment' } ResourcePropertySource {name='class path resource [application.properties]' } 默烦
第四步:添加 ConfigurationPropertySources
有一 step4.properties 文件,其内容如下:
user.first-name=George user.middle_name=Walker user.lastName=Bush
尝试读取文件中的内容:
@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:
@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。
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:/'' } "lcp"
EnvironmentPostProcessor 还有一个有趣的实现:RandomValuePropertySourceEnvironmentPostProcessor,该实现提供了随机值的生成。
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 配置文件中读取并初始化后置处理器:
# Environment Post Processors 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 配置文件中读取并初始化的:
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
要想该监听器成功监听到事件,需要在第五步中发布一个事件,而事件的发布由第一步获取的事件发布器完成:
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 实现的。
@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 配置文件的信息如下:
spring.main.banner-mode=off spring.main.lazy-initialization=true
绑定 spring.main 开头的配置信息到 SpringApplication 对象中:
@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
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:
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:
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 的版本号可以使用:
System.out.println("SpringBoot: " + SpringBootVersion.getVersion()); System.out.println("Spring: " + SpringVersion.getVersion());
步骤总结
得到 SpringApplicationRunListeners 事件发布器 发布 Application Starting 事件 封装启动 args 准备 Environment 添加命令行参数 ConfigurationPropertySources 处理 发布 Application Environment 已准备事件 通过 EnvironmentPostProcessorApplicationListener 进行 env 后处理 application.properties 由 StandardConfigDataLocationResolver 解析 spring.application.json 绑定 spring.main 到 SpringApplication 对象 打印 Banner 创建容器 准备容器 发布 Application Context 已初始化事件 加载 Bean 定义 发布 Application Prepared 事件 refresh 容器 发布 Application Started 事件 执行 Runner 发布 Application Ready 事件 这其中有异常,发布 Application Failed 事件 Tomcat 内嵌容器 Tomcat 基本结构:
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 创建连接器,设置监听端口 @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); }
自行实现的 Servlet 需要继承 HttpServlet,并重写 doGet() 方法:
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 整合 首先肯定需要一个 Spring 容器,选择不支持内嵌 Tomcat 的 Spring 容器,使其使用前文中的 Tomcat:
public static WebApplicationContext getApplicationContext () { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext (); context.register(Config.class); context.refresh(); return context; }
容器中注册了 Config Bean:
@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:
@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 并执行 `` 方法,
@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.1 自动配置类原理 有以下四个类:
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 容器中:
@Configuration @Import({AutoConfiguration1.class, AutoConfiguration2.class}) static class Config {} 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 indi.lcp.a41.A41$AutoConfiguration1 bean1 indi.lcp.a41.A41$AutoConfiguration2 bean2
如果有多个第三方配置类,难不成到一个个地导入?
可以使用导入选择器 ImportSelector,重写 selectImports() 方法,返回需要自动装配的 Bean 的全限定类名数组:
@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:
# 内部类作为 key 时,最后以 $ 符号分割 indi.lcp.a41.A41$MyImportSelector=\ indi.lcp.a41.A41.AutoConfiguration1, \ indi.lcp.a41.A41.AutoConfiguration2
修改 selectImports() 方法实现逻辑:
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 使用如下语句加载:
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 的全限定类名,可以使用 # 作为注释。其加载方式使用:
ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader());
其中 AutoConfiguration 是一个注解,它的全限定类名为 org.springframework.boot.autoconfigure.AutoConfiguration。
也就是说可以自定义一个注解,创建 META-INF/spring/full-qualified-annotation-name.imports 文件,在文件里声明需要自动装配的类
package indi.lcp.a41;@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MyAutoConfiguration {} package indi.lcp.a41;public class A41 { static class Bean3 { } }
创建 META-INF/spring/indi.lcp.a41.MyAutoConfiguration.imports 文件:
修改 selectImports() 方法实现逻辑:
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 多了 一个:
定义了冲突的 Bean
第三方装配了 Bean1:
static class AutoConfiguration1 { @Bean public Bean1 bean1 () { return new Bean1 ("第三方" ); } }
用户又自行定义了 Bean1:
@Configuration @Import(MyImportSelector.class) static class Config { @Bean public Bean1 bean1 () { return new Bean1 ("本项目" ); } }
修改测试的 main() 方法:
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 的设置:
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 indi.lcp.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 indi.lcp.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=indi.lcp.a41.A41$AutoConfiguration1; factoryMethodName=bean1; initMethodName=null ; destroyMethodName=(inferred); defined in class path resource [indi/lcp/a41/A41$AutoConfiguration1.class]] bound.
那这样是合理的吗?
显然不是。比如 SpringBoot 默认的数据连接池是 Hikari,如果用户想换成 Druid,岂不是做不到?
实际情况下是能做到的,这又是怎么做到的呢?
首先需要使用户的配置类中定义的 Bean 先于 @Import 导入的 Bean 添加到 Spring 容器中,只需将选择器 MyImportSelector 实现的 ImportSelector 接口更换成其子接口 DeferredImportSelector 即可
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 [indi/lcp/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=indi.lcp.a41.A41$AutoConfiguration1; factoryMethodName=bean1; initMethodName=null ; destroyMethodName=(inferred); defined in class path resource [indi/lcp/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 indi.lcp.a41.A41$Config] bound.
尽管还是出现了异常,但异常信息中显示的是在配置类定义的 Bean 已存在,第三方装配的 Bean 无法再添加,这表明 Bean 的添加顺序修改成功。
最后在第三方定义的 Bean 上添加 @ConditionalOnMissingBean 注解,表示容器中存在同名的 Bean 时忽略该 Bean 的添加:
static class AutoConfiguration1 { @Bean @ConditionalOnMissingBean public Bean1 bean1 () { return new Bean1 ("第三方" ); } }
再次运行 main() 方法,不再出现异常:
>>>>>>>>>>>>>>>>>>>>>>>>>>> A41.Bean1(name=本项目)
41.2 Aop 自动配置 确保当前模块下已导入:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
使用 AopAutoConfiguration 自动装配与 AOP 相关的 Bean:
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 indi.lcp.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
以 indi.lcp.a41.TestAopAuto$Config 为分割线,上方是添加的一些后置处理器,下方就是 AOP 自动装配添加的 Bean。
在配置类 AopAutoConfiguration 中,使用注解判断配置类是否生效。首先是最外层的 AopAutoConfiguration:
@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:
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 indi.lcp.a41.TestAopAuto$Config
如果 spring.aop.auto 的值是 true,又会成功添加上 AOP 自动装配的 Bean。
再看 AopAutoConfiguration 的内部类:
@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 内部又有两个配置类:
@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 注解:
@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:
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar { public void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry); } }
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary() 方法是注册 Bean 的主要逻辑:
@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:
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:
public interface ImportBeanDefinitionRegistrar { default void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { this .registerBeanDefinitions(importingClassMetadata, registry); } default void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { } }
该接口需要搭配 @Import 注解使用。
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" , "lcp" ) .addPropertyValue("age" , 20 ) .getBeanDefinition(); registry.registerBeanDefinition("user" , beanDefinition); } } @Setter @ToString static class User { private String name; private int age; }
注意: 使用时一定要确保 Spring 容器中存在 ConfigurationClassPostProcessor 类型的 Bean。
除此之外,使用 BeanDefinitionRegistryPostProcessor 接口也能拿到 BeanDefinitionRegistry:
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor { void postProcessBeanDefinitionRegistry (BeanDefinitionRegistry var1) throws BeansException; }
41.3 数据库相关的自动配置 确保当前模块下已导入:
<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 注解进行导入:
@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 信息:
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:
@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 生效:
@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 添加到容器中:
@EnableConfigurationProperties(DataSourceProperties.class) public class DataSourceAutoConfiguration { }
在 DataSourceProperties 中会绑定配置文件中以 spring.datasource 为前缀的配置:
@ConfigurationProperties(prefix = "spring.datasource") public class DataSourceProperties implements BeanClassLoaderAware , InitializingBean { }
获取 DataSourceProperties 类型的 Bean,并打印其 url、username 和 password:
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: root 123456
MyBatis 自动配置
@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() 方法:
@Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory (DataSource dataSource) throws Exception { }
依赖 Spring 容器中的 DataSource,当容器中不存在 SqlSessionFactory 时,将其添加到 Spring 容器中。
然后是 sqlSessionTemplate() 方法,它与添加 SqlSessionFactory 到 Spring 容器的逻辑一样:
@Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate (SqlSessionFactory sqlSessionFactory) { }
SqlSessionTemplate 也是 SqlSession 的实现,提供了与当前线程绑定的 SqlSession。针对多个方法调用,如果它们来自同一个线程,那么获取到的 SqlSession 对象是同一个。这也是为什么有了 DefaultSqlSession 作为 SqlSession 的实现了,还需要 SqlSessionTemplate。
在 MyBatis 中,使用 MapperFactoryBean 将接口转换为对象,其核心是 getObject() 方法:
public T getObject () throws Exception { return this .getSqlSession().getMapper(this .mapperInterface); }
方法中获取了 sqlSession 对象,而获取的就是 SqlSessionTemplate 对象:
public SqlSession getSqlSession () { return this .sqlSessionTemplate; }
最后来到 MapperScannerRegistrarNotFoundConfiguration 内部类:
@Configuration @Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class}) @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class}) public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean { }
利用 @ConditionalOnMissingBean 判断 Spring 容器中缺失 MapperFactoryBean 和 MapperScannerConfigurer 时,该配置类生效。生效时利用 @Import 导入 AutoConfiguredMapperScannerRegistrar:
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware , EnvironmentAware, ImportBeanDefinitionRegistrar { }
AutoConfiguredMapperScannerRegistrar 实现了 ImportBeanDefinitionRegistrar 接口,允许通过编程的方式加 Bean 添加到 Spring 容器中,而这里是去扫描 Mapper 接口,将其转换为对象添加到 Spring 容器中。
在 main() 所在类的包路径下创建 mapper 包,并新建三个接口,其中两个被 @Mapper 注解标记:
@Mapper public interface Mapper1 {} @Mapper public interface Mapper2 {} public interface Mapper3 {}
运行 main() 方法,查看 Mapper1 和 Mapper2 是否被添加到 Spring 容器中。
结果是否定的。因为 没有设置要扫描的包路径 。
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); } } 当前包名: indi.lcp.a41 mapper1 来源: file [D:\Code\IdeaCode\advanced-spring\boot\target\classes\indi\lcp\a41\mapper\Mapper1.class] mapper2 来源: file [D:\Code\IdeaCode\advanced-spring\boot\target\classes\indi\lcp\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 的信息、来源:
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.1 @Conditional 在 SpringBoot 的自动配置中,经常看到 @Conditional 注解的使用,使用该注解可以按条件加载配置类。
@Conditional 注解并不具备条件判断功能,而是通过指定的 Class 列表来进行判断,指定的 Class 需要实现 Condition 接口。
假设有这样一个需求:通过判断类路径下是否存在 com.alibaba.druid.pool.DruidDataSource 类来加载不同的配置类,当存在 DruidDataSource 时,加载 AutoConfiguration1,反之加载 AutoConfiguration2。
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 indi.lcp.a42.A42$AutoConfiguration1 bean1
导入 druid 依赖:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2 .15 </version> </dependency>
再次运行 main() 方法:
config org.springframework.context.annotation.ConfigurationClassPostProcessor indi.lcp.a42.A42$AutoConfiguration1 bean1
42.2 @ConditionalOnXxx 在 SpringBoot 的自动配置中,经常看到 @ConditionalOnXxx 注解的使用,这种注解是将某个 @Conditional 的判断进行了封装,比如 ConditionalOnClass 就是用于判断某个 Class 是否存在。
因此针对上文中的代码可以做出修改:
自定义 @ConditionalOnClass 注解,填入需要判断的全限定类名和判断条件;
移除模拟的第三方配置上的 @Conditional 注解,而是使用自定义的 @ConditionalOnClass;
Condition 接口的使用类重写的 matches() 方法利用 @ConditionalOnClass 注解进行条件判断。
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】一样。
FactoryBean FactoryBean 是一个接口,可以实现该接口,并指定一个泛型,在重写的方法指定泛型类型对象的创建,然后将实现类交由 Spring 管理,最后 Spring 容器中会增加泛型类型的 Bean。这个 Bean 并不是完全受 Spring 管理,或者说部分受 Spring 管理。
为什么这么说呢?
首先定义一个 Bean2,交由 Spring 管理,但它不是重点:
@Component public class Bean2 {}
然后定义 Bean1,它未交由 Spring 管理,但是在其内部注入了 Bean2、定义初始化方法、实现 Aware 接口:
@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:
@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。
@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() 方法后,控制台打印出:
indi.lcp.a43.Bean1FactoryBean - create bean: indi.lcp.a43.Bean1@2667f029 indi.lcp.a43.Bean1@2667f029
Bean1 类型的 Bean 被成功添加到 Spring 容器中,但根据打印的日志信息可以看出这个 Bean 没有经历依赖注入阶段、没有回调 Aware 接口、没有经历初始化阶段,其创建是由重写的 getObject() 方法完成的。
这个 Bean 就真的没有经历 Spring Bean 的生命周期中的任何阶段吗?
定义 Bean1PostProcessor,实现 BeanPostProcessor 接口,在 bean1 初始化前后打印日志信息:
@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() 方法后,控制台打印出:
indi.lcp.a43.Bean1FactoryBean - create bean: indi.lcp.a43.Bean1@6a28ffa4 indi.lcp.a43.Bean1PostProcessor - after [bean1] init indi.lcp.a43.Bean1@6a28ffa4
bean1 进行了初始化后的增强逻辑,但未进行初始化前的增强逻辑。
创建代理对象的时机就是在初始化后,因此由 FactoryBean 创建的 Bean 可以进行代理增强 。
FactoryBean 接口
FactoryBean 接口中有三个可以被重写的方法:
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,那通过类型获取呢?比如:
context.getBean(Bean1FactoryBean.class)
答案是可行的。
除此之外,还可以在名称前添加 &,然后通过名称来获取(有点指针的味道?):
context.getBean("&bean1" )
@Indexed Spring 在进行组件扫描时,会遍历项目中依赖的所有 Jar 包中类路径下所有的文件,找到被 @Component 及其衍生注解标记的类,然后把它们组装成 BeanDefinition 添加到 Spring 容器中。
如果扫描的返回过大,势必会大大地影响项目启动速度。
为了优化扫描速度,引入以下依赖,Spring 将扫描过程提前到编译期:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-indexer</artifactId> <optional>true </optional> </dependency>
现有如下类信息:
@Component public class Bean1 {} @Component public class Bean2 {} @Component public class Bean3 {}
这几个类都与 A44 存放于同一包路径下:
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 文件里有以下信息
indi.lcp.a44.Bean1=org.springframework.stereotype.Component indi.lcp.a44.Bean2=org.springframework.stereotype.Component indi.lcp.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 注解中:
@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 信息。
代理进一步理解 在 Spring 的代理中,依赖注入和初始化针对的是目标对象,代理对象和目标对象是两个对象,两者的成员变量不会共享。
确保项目中已导入以下依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
依赖注入和初始化针对的是目标对象
现有如下类信息:
@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; } } @Component public class Bean2 {}
为 Bean1 中的每个方法定制一个前置通知:
@Aspect @Component public class MyAspect { @Before("execution(* indi.lcp.a45.Bean1.*(..))") public void before () { System.out.println("before" ); } }
有一 SpringBoot 主启动类,它与 Bean1、Bean2 和 MyAspect 在同一包路径下,确保它们能被自动添加到 Spring 容器中:
@SpringBootApplication public class A45 { public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(A45.class, args); context.close(); } }
运行 main() 方法,控制台输出:
indi.lcp.a45.Bean1 - setBean2(Bean2 bean2) indi.lcp.a45.Bean1 - init
Bean1 中的依赖注入和初始化被成功执行,但 并没有被增强。
由于 Bean1 被增强了,从 Spring 容器中获取的对象将是代理对象:
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 indi.lcp.a45.Bean1 - setBean2(Bean2 bean2) before indi.lcp.a45.Bean1 - init
主动调用的 setBean2() 和 init() 方法 都被增强。
代理对象与目标对象的成员变量不共享
尝试打印代理对象和目标对象的成员变量信息(直接访问,不使用方法):
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 = indi.lcp.a45.Bean2@771db12c
由于依赖注入和初始化只针对目标对象,因此代理对象中的成员变量的值都是初始值。
在实际应用过程中,不会直接去访问成员变量,而是通过方法去访问:
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 indi.lcp.a45.Bean1 - getBean2() indi.lcp.a45.Bean2@771db12c before indi.lcp.a45.Bean1 - isInitialized() true
通过方法访问代理对象的成员变量时,这些方法会被增强,同时代理对象中的方法又会去调用目标对象的方法,从而读取出正确的值。
只会对能被重写的方法进行增强
在 Bean1 中增加几个方法:
@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 方法" ); } } @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 修饰的方法和私有方法由于无法被重写,因此它们不能被增强。如果想增强这些方法,可以使用 AspectJ 编译器增强或者 Agent 类加载。
@Value 注入底层 现有一 Bean1 类如下:
public class Bean1 { @Value("${JAVA_HOME}") private String home; @Value("18") private int age; }
需要解析 @Value(“${JAVA_HOME}”) 和 @Value(“18”) 的值,其中 JAVA_HOME 以系统环境变量填充,18 为整型。
解析分为两步:
获取 @Value 注解中 value 属性值; 解析属性值 @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 @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 表达式的解析
假设有如下几个类:
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 'indi.lcp.a46.A46$Bean3' ; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'indi.lcp.a46.A46$Bean3' : no matching editors or conversion strategy found
最后一步数据转换出了问题,无法将 String 转换成 A46$Bean3 类型,也就是说解析 @bean3 失败了,程序仍然把它当成字符串,而不是注入的 Bean。
为了解析成功,需要在转换前解析 #{}:
@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 indi .lcp.a46.A46$Bean3 >>>>>>>>>>>>>>>>>>> @Value 的 value 属性值: #{'hello, ' + '${JAVA_HOME}' }解析得到的值: #{'hello, ' + 'D:\environment\JDK1.8' } 解析得到的值的类型: class java .lang.String 转换后的类型: class java .lang.String
@Autowired 注入底层 47.1 注入方式 按成员变量类型注入
现有一 Bean1 类如下:
static class Bean1 { @Autowired private Bean2 bean2; }
需要被注入的对象所在类:
@Component("bean2") static class Bean2 {}
从容器中获取需要被注入的 Bean 对象:
@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 )); } } indi.lcp.a47.A47_1$Bean2@222545dc
按参数类型注入
对 Bean1 进行修改:
static class Bean1 { @Autowired public void setBean2 (Bean2 bean2) { this .bean2 = bean2; } }
根据 setBean2() 方法的 Bean2 类型参数进行注入:
@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 )); } indi.lcp.a47.A47_1$Bean2@222545dc
包装为 Optional
对 Bean1 进行修改:
static class Bean1 { @Autowired private Optional<Bean2> bean3; }
如果直接按照以下方式获取 DependencyDescriptor 对象:
DependencyDescriptor dd3 = new DependencyDescriptor (Bean1.class.getDeclaredField("bean3" ), false );
其 dd3.getDependencyType() 方法将返回 Optional 的 Class 对象,这显然是不对的。
Spring 提供为 DependencyDescriptor 提供了解决这个问题的方法,即“增加嵌套等级”来获取内嵌类型:
dd3.increaseNestingLevel();
执行 increaseNestingLevel() 方法后,dd3.getDependencyType() 方法返回的 Bean2 的 Class 对象。
因此注入 Optional类型的成员变量应该按照:
@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[indi.lcp.a47.A47_1$Bean2@222545dc]
注入 Optional 对象和使用 @Autowired(required = false) 的作用是一样的,当容器中不存在目标 Bean 时,不会抛出 NoSuchBeanDefinitionException 异常。
包装为 ObjectFactory
对 Bean1 进行修改:
static class Bean1 { @Autowired private ObjectFactory<Bean2> bean4; }
注入 ObjectFactory类型的对象与注入 Optional类型的对象类似,只不过 ObjectFactory 提供了 延迟注入 的能力,也就是说 Bean2 对象不会立即被注入,而是在需要时才被注入。
ObjectFactory 是一个函数式接口:
@FunctionalInterface public interface ObjectFactory <T> { T getObject () throws BeansException; }
注入的应该是 ObjectFactory 对象,在调用该对象的 getObject() 方法时,Bean2 对象才被注入:
@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()); } } indi.lcp.a47.A47_1$Bean2@222545dc
与 ObjectFactory 类似的还有个名为 ObjectProvider 的接口,后者继承了前者。
与 ObjectFactory 相比,ObjectProvider 提供了类似于 Optional 的安全注入功能,当容器中不存在目标 Bean 时, 不会抛出 NoSuchBeanDefinitionException 异常。ObjectProvider 提供的 getIfAvailable() 在获取不存在的 Bean 时,不会抛出异常,而是返回 null。
对 @Lazy 的处理
对 Bean1 进行修改,在成员变量 bean2 上使用 @Lazy 注解:
static class Bean1 { @Autowired @Lazy private Bean2 bean2; }
对于 @Lazy 注解标记的成员变量,注入的对象不再是目标对象,而是其代理对象,因此不能使用 DefaultListableBeanFactory 对象的 doResolveDependency() 方法来获取注入的对象。
@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()); } indi.lcp.a47.A47_1$Bean2@222545dc class indi .lcp.a47.A47_1$Bean2$$EnhancerBySpringCGLIB$$d631a20c
@Lazy 实现的 延迟注入 (前面讲的 ObjectFactory 和 ObjectProvider 也有延迟注入功能,但与 @Lazy 的实现不一样)不是不注入,而是注入目标对象的代理对象,当使用到代理对象中的方法时,代理对象就会去 Spring 容器中寻找真正的目标对象,然后调用目标对象对应的方法。
@Lazy 的实现细节可以在 ContextAnnotationAutowireCandidateResolver 中看到:
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
Provider 接口是由 JSR-330 提出,要想使用此接口,需要导入以下依赖:
<dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> <!-- 不要惊讶,版本号就是 1 --> <version>1 </version> </dependency>
对 Bean1 进行修改:
static class Bean1 { @Autowired private Provider<Bean2> bean5; }
注入 Provider 类型的对象与注入 ObjectFactory类型的对象极其相似,Provider 也提供了 延迟注入 的能力,注入的是 Provider 对象,在调用该对象的 get() 方法时,Bean2 对象才被注入:
@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()); } } indi.lcp.a47.A47_1$Bean2@222545dc
Optional 类型、ObjectFactory 类型、ObjectProvider 类型、JSR-330 提供的类型的注入逻辑可在 DefaultListableBeanFactory#resolveDependency() 方法中看到:
@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() 方法。
现有如下几个类:
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,对其进行依赖注入:
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 数组:
@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 indi .lcp.a47.A47_2$Serviceservice3 service2 service1 [Lindi.lcp.a47.A47_2$Service;@49139829
相关源码可在 DefaultListableBeanFactory#resolveMultipleBeans() 方法中看到。
List 类型
注入 List类型数据的逻辑与注入 Service[] 类型数据的逻辑类似,只不过在容器中寻找目标 Bean 时不再通过数组元素类型,而是通过 List 的泛型类型:
@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 indi .lcp.a47.A47_2$Service[indi.lcp.a47.A47_2$Service3@35e2d654, indi.lcp.a47.A47_2$Service2@1bd4fdd, indi.lcp.a47.A47_2$Service1@55183b20]
注意: 对于注入的集合类型数据,注入的类型必须是 Collection 及其 子接口,比如不支持直接注入 ArrayList 类型的数据。
相关源码可在 DefaultListableBeanFactory#resolveMultipleBeans() 方法中看到:
@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 中有一成员变量:
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry { private final Map<String, Object> singletonObjects = new ConcurrentHashMap <>(256 ); }
singletonObjects 用于存放 Spring 容器中的所有单例 Bean 对象。
类似 ApplicationContext、BeanFactory 类型的对象则是放在 DefaultListableBeanFactory 中的 resolvableDependencies 中:
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory , BeanDefinitionRegistry, Serializable { private final Map<Class<?>, Object> resolvableDependencies = new ConcurrentHashMap <>(16 ); }
这些特殊对象是在调用 ApplicationContext 的 refresh() 方法时添加到 resolvableDependencies 中的。可在 AbstractApplicationContext 的 refresh() 方法中看到:
public void refresh () throws BeansException, IllegalStateException { synchronized (this .startupShutdownMonitor) { prepareBeanFactory(beanFactory); } } 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 集合中获取。
@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类型的对象,因此需要判断容器中的 Bean 对象泛型类型是否为指定类型。判断逻辑可以使用 ContextAnnotationAutowireCandidateResolver 的 isAutowireCandidate() 方法:
@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 indi.lcp.a47.A47_2$Dao2@74f0ea28
@Qualifier
当容器中存在多个相同类型的 Bean 对象,在执行依赖注入时可以使用 @Qualifier 注解来指定需要注入的 Bean 对象的名称。判断逻辑同样使用 ContextAnnotationAutowireCandidateResolver 的 isAutowireCandidate() 方法:
@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 indi.lcp.a47.A47_2$Service2@1bd4fdd
@Primary
当容器中存在多个相同类型的 Bean 对象时,在执行依赖注入时除了可以使用 @Qualifier 注解外,还可以在被注入的 Bean 对象所在类上使用 @Primary 注解,指定执行依赖注入时使用的主要 Bean 对象。
如果 Bean 对象的所在类 被 @Primary 注解标记,那么在构造 BeanDefinition 时就会记录这个信息。
通常情况下,@Primary 注解只有一个作用在同种类型的 Bean 上,存在多个时,Spring 依旧无法区分。
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 {} @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 注解还支持按照成员变量名称进行匹配。
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 {} @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