Spring注解驱动开发 视频学习来源:尚硅谷
博客参考:Spring注解驱动开发
学习时间:2021年12月28日
1 组件注册 在Spring容器的底层,最重要的功能就是IOC和DI,也就是控制反转和依赖注入。
DI和IOC它俩之间的关系是DI不能单独存在,DI需要在IOC的基础上来完成。
在Spring内部,所有的组件都会放到IOC容器 中,组件之间的关系通过IOC容器来自动装配,也就是我们所说的依赖注入。接下来,我们就使用注解的方式来完成容器中组件的注册、管理及依赖、注入等功能。
在介绍使用注解完成容器中组件的注册、管理及依赖、注入等功能之前,我们先来看看使用XML配置文件是如何注入bean的。
1.1 组件注册 @Configuration和@bean 1.1.1 xml方式注入
引入spring依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.3.9</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.20</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > <scope > test</scope > </dependency >
实体类Person
1 2 3 4 5 6 7 @Data @AllArgsConstructor @NoArgsConstructor public class Person { private String name; private Integer age; }
工程结构
beans.xml配置文件注入bean
1 2 3 4 5 6 7 8 9 10 <?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 ="person" class ="com.hongyi.bean.Person" > <property name ="name" value ="Mark" > </property > <property name ="age" value ="12" > </property > </bean > </beans >
测试类
1 2 3 4 5 6 7 8 9 public class Test1 { @Test public void test1 () { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml" ); Person bean = (Person) context.getBean("person" ); System.out.println(bean); } }
从输出结果中,我们可以看出,Person类通过beans.xml文件的配置,已经注入到Spring的IOC容器中去了。
1.1.2 注解注入
配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration public class MainConfig { @Bean public Person person () { return new Person("Lisi" , 20 ); } }
测试类
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void test2 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class); Person bean = context.getBean(Person.class); System.out.println(bean); String[] namesForType = context.getBeanNamesForType(Person.class); for (String name : namesForType) { System.out.println(name); } }
1.2 组件扫描 @ComponentScan 在实际项目中,我们更多的是使用Spring的包扫描功能对项目中的包进行扫描,凡是在指定的包或其子包中的类上标注了@Repository、@Service、@Controller、@Component注解的类都会被扫描到,并将这个类注入到Spring容器中。
Spring包扫描功能可以使用XML配置文件进行配置,也可以直接使用@ComponentScan注解进行设置,使用@ComponentScan注解进行设置比使用XML配置文件来配置要简单的多。
1.2.1 xml方式 我们可以在Spring的XML配置文件中配置包的扫描,在配置包扫描时,需要在Spring的XML配置文件中的beans节点中引入context标签
。
1 2 <context:component-scan base-package ="com.hongyi" > </context:component-scan >
这样配置以后,只要在com.hongyi包下,或者com.hongyi的子包下标注了@Repository、@Service、@Controller、@Component注解的类都会被扫描到,并自动注入到Spring容器中 。
此时,我们分别创建BookDao、BookService以及BookController这三个类,并在这三个类中分别添加@Repository、@Service、@Controller注解。
1 2 3 4 @Repository public class BookDao { }
1 2 3 4 @Service public class BookService { }
1 2 3 4 @Controller public class BookController { }
我们就可以在IOCTest测试类中编写如下一个方法来进行测试了,即看一看IOC容器中现在有哪些bean。
1 2 3 4 5 6 7 8 9 @Test public void test () { ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml" ); String[] definitionNames = applicationContext.getBeanDefinitionNames(); for (String name : definitionNames) { System.out.println(name); } }
1 2 3 4 5 6 7 8 9 org.springframework.context.annotation.internalConfigurationAnnotationProcessor org.springframework.context.annotation.internalAutowiredAnnotationProcessor org.springframework.context.event.internalEventListenerProcessor org.springframework.context.event.internalEventListenerFactory mainConfig bookController bookDao bookService person
可以看到,除了输出我们自己创建的bean的名称之外,也输出了Spring内部使用的一些重要的bean的名称。接下来,我们使用注解来完成这些功能。
1.2.2 注解方式 1) 包扫描
配置类
1 2 3 4 5 6 7 8 9 10 @Configuration @ComponentScan(value = "com.hongyi") public class MainConfig { @Bean public Person person () { return new Person("Lisi" , 20 ); } }
测试
1 2 3 4 5 6 7 8 9 10 @Test public void test3 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class); String[] names = context.getBeanDefinitionNames(); for (String name : names) { System.out.println(name); } }
2) 扫描过滤器
需求:除了@Controller和@Service标注的组件之外,IOC容器中剩下的组件我都要,即相当于是我要排除 @Controller和@Service这俩注解标注的组件。要想达到这样一个目的,我们可以在MainConfig类上通过@ComponentScan注解的excludeFilters()
方法实现。例如,我们在MainConfig类上添加了如下的注解。
1 2 3 @ComponentScan(value = "com.hongyi", excludeFilters = { @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, Service.class}) })
执行结果:
需求:扫描时只包含 注解标注的类。我们也可以使用ComponentScan注解类中的includeFilters()
方法来指定Spring在进行包扫描时,只包含哪些注解标注的类。
这里需要注意的是 ,当我们使用includeFilters()
方法来指定只包含哪些注解标注的类时,需要禁用掉默认的过滤规则 。
还记得我们以前在XML配置文件中配置这个只包含的时候,应该怎么做吗?我们需要在XML配置文件中先配置好use-default-filters=”false”,也就是禁用掉默认的过滤规则,因为默认的过滤规则就是扫描所有的,只有我们禁用掉默认的过滤规则之后,只包含才能生效 。
1 2 3 @ComponentScan(value = "com.hongyi", includeFilters = { @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, Service.class}) }, useDefaultFilters = false)
执行结果:
1.2.3 自定义TypeFilter Spring的强大之处不仅仅是提供了IOC容器,能够通过过滤规则指定排除和只包含哪些组件,它还能够通过自定义TypeFilter来指定过滤规则。如果Spring内置的过滤规则不能够满足我们的需求,那么我们便可以通过自定义TypeFilter 来实现我们自己的过滤规则。
在使用@ComponentScan注解实现包扫描时,我们可以使用@Filter指定过滤规则,在@Filter中,通过type
来指定过滤的类型。而@Filter注解中的type属性是一个FilterType枚举,其源码如下。
1 2 3 4 5 6 7 8 9 10 public enum FilterType { ANNOTATION, ASSIGNABLE_TYPE, ASPECTJ, REGEX, CUSTOM; private FilterType () { } }
1) FilterType.ANNOTATION 按照注解 进行包含或者排除。例如,使用@ComponentScan注解进行包扫描时,如果要想按照注解只包含标注了@Controller注解的组件,那么就需要像下面这样写了。
1 2 3 4 5 6 7 8 @ComponentScan(value="com.hongyi", includeFilters={ /* * type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,还是按照正则表达式进行排除,等等 * classes:我们需要Spring在扫描时,只包含@Controller注解标注的类 */ @Filter(type=FilterType.ANNOTATION, classes={Controller.class}) }, useDefaultFilters=false)
2) FilterType.ASSIGNABLE_TYPE 按照给定的类型 进行包含或者排除。
1 2 3 4 @ComponentScan(value="com.hongyi", includeFilters={ // 只要是BookService这种类型的组件都会被加载到容器中,不管是它的子类还是什么它的实现类。记住,只要是BookService这种类型的 @Filter(type=FilterType.ASSIGNABLE_TYPE, classes={BookService.class}) }, useDefaultFilters=false)
此时,只要是BookService这种类型的组件,都会被加载到容器中。也就是说,当BookService是一个Java类时,该类及其子类都会被加载到Spring容器中;当BookService是一个接口时,其子接口或实现类都会被加载到Spring容器中。
3) FilterType.REGEX 按照REGEX(正则表达式)表达式进行包含或者排除。
4) FilterType.CUSTOM 按照自定义规则 进行包含或者排除。如果实现自定义规则进行过滤时,自定义规则的类必须是org.springframework.core.type.filter.TypeFilter接口的实现类 。
要想按照自定义规则进行过滤,首先我们得创建org.springframework.core.type.filter.TypeFilter接口的一个实现类,例如MyTypeFilter,该实现类的代码一开始如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class MyTypeFilter implements TypeFilter { @Override public boolean match (MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata(); ClassMetadata classMetadata = metadataReader.getClassMetadata(); Resource resource = metadataReader.getResource(); String className = classMetadata.getClassName(); System.out.println("--->" + className); if (className.contains("er" )){ return true ; } return false ; } }
使用:
1 2 3 @ComponentScan(value = "com.hongyi", includeFilters = { @ComponentScan.Filter(type = FilterType.CUSTOM, classes = {MyTypeFilter.class}) }, useDefaultFilters = false)
执行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 --->com.hongyi.test.Test1 --->com.hongyi.bean.Person --->com.hongyi.config.MyTypeFilter --->com.hongyi.controller.BookController --->com.hongyi.dao.BookDao --->com.hongyi.service.BookService org.springframework.context.annotation.internalConfigurationAnnotationProcessor org.springframework.context.annotation.internalAutowiredAnnotationProcessor org.springframework.context.event.internalEventListenerProcessor org.springframework.context.event.internalEventListenerFactory mainConfig person myTypeFilter bookController bookService
1.3 组件作用域 @Scope Spring容器中的组件默认是单例的 ,在Spring启动时就会实例化并初始化这些对象,并将其放到Spring容器中,之后,每次获取对象时,直接从Spring容器中获取,而不再创建对象。如果每次从Spring容器中获取对象时,都要创建一个新的实例对象,那么该如何处理呢?此时就需要使用@Scope注解来设置组件的作用域了。
1.3.1 概述
从@Scope注解类的源码中可以看出,在@Scope注解中可以设置如下值:
ConfigurableBeanFactory#SCOPE_PROTOTYPE
ConfigurableBeanFactory#SCOPE_SINGLETON
org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
很明显,在@Scope注解中可以设置的值包括ConfigurableBeanFactory
接口中的SCOPE_PROTOTYPE和SCOPE_SINGLETON,以及WebApplicationContext类中的SCOPE_REQUEST和SCOPE_SESSION。
首先,我们查看一下ConfigurableBeanFactory接口的源码,发现在该接口中存在两个常量的定义,如下所示。
没错,SCOPE_SINGLETON就是singleton,而SCOPE_PROTOTYPE就是prototype。
综上,在@Scope注解中的取值如下所示。
1.3.2 单实例bean作用域
测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void test4 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig2.class); String[] names = context.getBeanDefinitionNames(); for (String name : names) { System.out.println(name); } Object bean1 = context.getBean("person" ); Object bean2 = context.getBean("person" ); System.out.println(bean1 == bean2); }
这也正好验证了我们的结论:对象在Spring容器中默认是单实例的,Spring容器在启动时就会将实例对象加载到Spring容器中,之后,每次从Spring容器中获取实例对象,都是直接将对象返回,而不必再创建新的实例对象了 。
1.3.3 多实例bean作用域 修改Spring容器中组件的作用域,我们需要借助于@Scope注解。此时,我们将MainConfig2配置类中Person对象的作用域修改成prototype,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration public class MainConfig2 { @Scope("prototype") @Bean("person") public Person person () { System.out.println("给容器中添加Person..." ); return new Person("Hongyi" , 24 ); } }
测试结果:
1.3.4 注意事项
单实例bean注意事项
单实例bean是整个应用所共享的 ,所以需要考虑到线程安全问题,之前在玩SpringMVC的时候,SpringMVC中的Controller默认是单例的,有些开发者在Controller中创建了一些变量,那么这些变量实际上就变成共享的了,Controller又可能会被很多线程同时访问,这些线程并发去修改Controller中的共享变量,此时很有可能会出现数据错乱的问题,所以使用的时候需要特别注意。
多实例bean注意事项
多实例bean每次获取的时候都会重新创建,如果这个bean比较复杂,创建时间比较长,那么就会影响系统的性能,因此这个地方需要注意点。
1.3.5 懒加载 @Lazy 懒加载:针对单实例bean:默认在容器启动时创建对象。对于懒加载,容器启动时不创建对象,第一次获取bean创建对象,并且初始化。
1 2 3 4 5 6 7 8 9 @Configuration public class MainConfig2 { @Bean("person") @Lazy public Person person () { System.out.println("给容器中添加Person..." ); return new Person("Hongyi" , 24 ); } }
懒加载,也称延时加载 ,仅针对单实例bean生效 。 单实例bean是在Spring容器启动的时候加载的,添加@Lazy注解后就会延迟加载,在Spring容器启动的时候并不会加载,而是在第一次使用此bean的时候才会加载,但当你多次获取bean的时候并不会重复加载,只是在第一次获取的时候才会加载,这不是延迟加载的特性,而是单实例bean的特性。
1.4 条件注册 @Conditional
按照一定条件进行判断,满足条件则给容器中注册bean。
需求:如果系统是Windows,则给容器注册(“bill”),如果系统是Linux,则给容器注册(“linus”)
编写Condition接口实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class LinuxCondition implements Condition { @Override public boolean matches (ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { ConfigurableListableBeanFactory beanFactory = conditionContext.getBeanFactory(); ClassLoader classLoader = conditionContext.getClassLoader(); Environment environment = conditionContext.getEnvironment(); String property = environment.getProperty("os.name" ); if (property.contains("linux" )) { return true ; } return false ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class WindowsCondition implements Condition { @Override public boolean matches (ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { Environment environment = conditionContext.getEnvironment(); String property = environment.getProperty("os.name" ); if (property.contains("Windows" )) { return true ; } return false ; } }
配置类MainConfig1.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration public class MainConfig2 { @Conditional({WindowsCondition.class}) @Bean("bill") public Person person01 () { return new Person("Bill Gates" , 62 ); } @Conditional({LinuxCondition.class}) @Bean("linus") public Person person02 () { return new Person("Linus" , 48 ); } }
测试方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void test5 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig2.class); ConfigurableEnvironment environment = context.getEnvironment(); String property = environment.getProperty("os.name" ); System.out.println(property); String[] namesForType = context.getBeanNamesForType(Person.class); for (String name : namesForType) { System.out.println(name); } Map<String, Person> beansOfType = context.getBeansOfType(Person.class); System.out.println(beansOfType); }
执行结果:
修改vm参数为linux环境
执行结果:
1.5 导入组件 @Import 1.5.1 注册组件的方式 向Spring容器中注册bean通常有以下几种方式:
包扫描+给组件标注注解(@Controller、@Servcie、@Repository、@Component),但这种方式比较有局限性,局限于我们自己写的类
@Bean
注解,通常用于导入第三方包中的组件
@Import
注解,快速向Spring容器中导入一个组件
1.5.2 @Import概述和使用
概述
Spring 3.0之前,创建bean可以通过XML配置文件与扫描特定包下面的类来将类注入到Spring IOC容器内。而在Spring 3.0之后提供了JavaConfig的方式,也就是将IOC容器里面bean的元信息以Java代码的方式进行描述,然后我们可以通过@Configuration与@Bean这两个注解配合使用来将原来配置在XML文件里面的bean通过Java代码的方式进行描述。
@Import注解提供了@Bean注解的功能,同时还有XML配置文件里面标签组织多个分散的XML文件的功能,当然在这里是组织多个分散的@Configuration,因为一个配置类就约等于一个XML配置文件。
注意:@Import注解只允许放到类上面,不允许放到方法上。
使用方式
@Import注解的三种用法主要包括:
直接填写class数组的方式
ImportSelector接口的方式,即批量导入,这是重点
ImportBeanDefinitionRegistrar接口方式,即手工注册bean到容器中
注意:我们先来看第一种方法,即直接填写class数组的方式:
MainConfig1.class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration @Import({Color.class, Animal.class}) public class MainConfig2 { @Bean("person") public Person person () { return new Person("Hongyi" , 24 ); } @Conditional({WindowsCondition.class}) @Bean("bill") public Person person01 () { return new Person("Bill Gates" , 62 ); } }
测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void printBeans (AnnotationConfigApplicationContext context) { String[] names = context.getBeanDefinitionNames(); for (String name : names) { System.out.println(name); } } @Test public void test6 () { printBeans(context); }
1.5.3 @ImportSelector使用 ImportSelector接口是Spring中导入外部配置的核心接口,在Spring Boot的自动化配置和@EnableXXX(功能性注解)都有它的存在。它返回需要导入的组件的全类名数组 (是一个字符串数组)。
该接口文档上说的明明白白,其主要作用是收集需要导入的配置类,selectImports()方法的返回值就是我们向Spring容器中导入的类的全类名。如果该接口的实现类同时实现EnvironmentAware,BeanFactoryAware,BeanClassLoaderAware或者ResourceLoaderAware,那么在调用其selectImports()方法之前先调用上述接口中对应的方法,如果需要在所有的@Configuration处理完再导入时,那么可以实现DeferredImportSelector接口。
在ImportSelector接口的selectImports()方法中,存在一个AnnotationMetadata
类型的参数,这个参数能够获取到当前标注@Import注解的类的所有注解信息,也就是说不仅能获取到@Import注解里面的信息,还能获取到其他注解的信息。
MyImportSelector.java
创建一个MyImportSelector类实现ImportSelector接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata annotationMetadata) { return new String[]{"com.hongyi.bean.Blue" , "com.hongyi.bean.Yellow" }; } @Override public Predicate<String> getExclusionFilter () { return ImportSelector.super .getExclusionFilter(); } }
配置类
1 @Import({Color.class, Animal.class, MyImportSelector.class})
测试类
1 2 3 4 5 6 @Test public void test6 () { printBeans(context); Blue blue = context.getBean(Blue.class); System.out.println(blue); }
1.5.4 @ImportBeanDefinitionRegistrar使用
接口实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { boolean yellow = registry.containsBeanDefinition("com.hongyi.bean.Yellow" ); boolean blue = registry.containsBeanDefinition("com.hongyi.bean.Blue" ); if (yellow && blue){ RootBeanDefinition definition = new RootBeanDefinition(Rainbow.class); registry.registerBeanDefinition("rainbow" , definition); } } }
配置类的使用
1 @Import({Color.class, Animal.class, MyImportSelector.class, MyImportBeanDefinitionRegistrar.class})
测试结果
1.5.5 FactoryBean使用 1) 概述 一般情况下,Spring是通过反射机制利用bean的class属性指定实现类来实例化bean的。在某些情况下,实例化bean过程比较复杂,如果按照传统的方式,那么则需要在标签中提供大量的配置信息,配置方式的灵活性是受限的,这时采用编码的方式可以得到一个更加简单的方案。Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口 ,用户可以通过实现该接口定制实例化bean的逻辑。
FactoryBean接口 对于Spring框架来说占有非常重要的地位,Spring自身就提供了70多个FactoryBean接口的实现。它们隐藏了实例化一些复杂bean的细节,给上层应用带来了便利 。从Spring 3.0开始,FactoryBean开始支持泛型,即接口声明改为FactoryBean<T>
的形式。
FactoryBean源码
1 2 3 4 5 6 7 8 9 10 11 12 13 public interface FactoryBean <T > { String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType" ; @Nullable T getObject () throws Exception ; @Nullable Class<?> getObjectType(); default boolean isSingleton () { return true ; } }
T getObject():返回由FactoryBean创建的bean实例,如果isSingleton()返回true,那么该实例会放到Spring容器中单实例缓存池中
boolean isSingleton():返回由FactoryBean创建的bean实例的作用域是singleton还是prototype
Class getObjectType():返回FactoryBean创建的bean实例的类型
这里,需要注意的是:当配置文件中标签的class属性配置的实现类是FactoryBean时,通过 getBean()方法返回的不是FactoryBean本身,而是FactoryBean#getObject()方法所返回的对象 ,相当于FactoryBean#getObject()代理了getBean()方法。
2) 使用
ColorFactoryBean.java
接口实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class ColorFactoryBean implements FactoryBean <Color > { @Override public Color getObject () throws Exception { System.out.println("ColorFactoryBean...getObject" ); return new Color(); } @Override public Class<?> getObjectType() { return Color.class; } @Override public boolean isSingleton () { return true ; } }
然后,我们在MainConfig2配置类中加入ColorFactoryBean的声明,如下所示。
1 2 3 4 @Bean public ColorFactoryBean colorFactoryBean () { return new ColorFactoryBean(); }
这里需要注意的是:这里使用@Bean注解向Spring容器中注册的是ColorFactoryBean对象。
那现在我们就来看看Spring容器中到底都有哪些bean。我们所要做的事情就是,运行IOCTest类中的testImport()方法,此时,输出的结果信息如下所示。
1 2 3 4 5 6 7 @Test public void test6 () { printBeans(context); Object colorFactoryBean = context.getBean("colorFactoryBean" ); System.out.println(colorFactoryBean.getClass()); }
可以看到,虽然在代码中使用@Bean注解注入的是ColorFactoryBean对象,但是实际上从Spring容器中获取到的bean对象却是调用ColorFactoryBean类中的getObject()方法获取到的Color对象 。
获取FactoryBean对象本身
只需要在获取工厂Bean本身时,在id前面加上&符号即可,例如&colorFactoryBean。
1 2 3 4 5 6 7 @Test public void test6 () { printBeans(context); Object colorFactoryBean = context.getBean("&colorFactoryBean" ); System.out.println(colorFactoryBean.getClass()); }
2 生命周期 bean的生命周期:创建—初始化—销毁
通常意义上讲的bean的生命周期,指的是bean从创建到初始化,经过一系列的流程,最终销毁的过程。只不过,在Spring中,bean的生命周期是由Spring容器来管理的。在Spring中,我们可以自己来指定bean的初始化和销毁的方法 。我们指定了bean的初始化和销毁方法之后,当容器在bean进行到当前生命周期的阶段时,会自动调用我们自定义的初始化和销毁方法。
容器管理bean的生命周期:我们可以自定义初始化和销毁方法,容器在bean进行到当前生命周期的时候来调用我们自定义的初始化和销毁。
2.1 通过@Bean指定 首先,创建一个名称为Car的类,这个类的实现比较简单,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Car { public Car () { System.out.println("car constructor..." ); } public void init () { System.out.println("car init..." ); } public void destroy () { System.out.println("car destroy..." ); } }
然后,我们将Car类对象通过注解的方式注册到Spring容器中,具体的做法就是新建一个MainConfigOfLifeCycle类作为Spring的配置类,将Car类对象通过MainConfigOfLifeCycle类注册到Spring容器中,MainConfigOfLifeCycle类的代码如下所示。
1 2 3 4 5 6 7 8 @Configuration public class MainConfigOfLifeCycle { @Bean(initMethod = "init", destroyMethod = "destroy") public Car car () { return new Car(); } }
构造(对象创建)
单实例:在容器启动时创建对象
多实例:在每次获取对象的时候创建对象
2.1.1 初始化 初始化:对象创建完成,并赋值好,调用初始化方法
单实例:在容器启动时创建对象,并调用初始化方法
1 2 3 4 5 6 @Test public void test7 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class); System.out.println("容器创建完成..." ); }
多实例:在每次获取对象的时候创建对象,并调用初始化方法
1 2 3 4 5 6 7 8 9 @Configuration public class MainConfigOfLifeCycle { @Scope("prototype") @Bean(initMethod = "init", destroyMethod = "destroy") public Car car () { return new Car(); } }
1 2 3 4 5 6 7 8 @Test public void test7 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class); System.out.println("容器创建完成..." ); Object car = context.getBean("car" ); }
注意与单实例的顺序
2.1.2 销毁
单实例:容器关闭的时候,调用销毁方法
1 2 3 4 5 6 7 8 @Test public void test7 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class); System.out.println("容器创建完成..." ); context.close(); }
多实例:容器不会管理这个bean,不会调用销毁方法
1 2 3 4 5 6 7 8 9 10 @Test public void test7 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class); System.out.println("容器创建完成..." ); Object car = context.getBean("car" ); context.close(); }
可见ioc容器没有调用car的销毁方法
2.2 InitializingBean和DisposableBean接口 通过让Bean实现InitializingBean(定义初始化逻辑),DisposableBean(定义销毁逻辑)
Cat类
首先创建一个Cat类,实现其接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Component public class Cat implements InitializingBean , DisposableBean { public Cat () { System.out.println("cat constructor..." ); } @Override public void afterPropertiesSet () throws Exception { System.out.println("cat init..." ); } @Override public void destroy () throws Exception { System.out.println("cat destroy..." ); } }
配置类
1 2 3 4 5 @ComponentScan("com.hongyi.bean") @Configuration public class MainConfigOfLifeCycle { }
测试类
1 2 3 4 5 6 7 8 @Test public void test7 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class); System.out.println("容器创建完成..." ); context.close(); }
2.3 @PostConstructor和@PreDestroy
@PostConstructor
:在bean创建完成并且属性赋值完成,来执行初始化方法
@PreDestroy
:在容器销毁bean之前,通知我们进行清理工作
首先添加依赖(这个api在java9之后被移除):
1 2 3 4 5 <dependency > <groupId > javax.annotation</groupId > <artifactId > javax.annotation-api</artifactId > <version > 1.3.2</version > </dependency >
然后创建一个Dog类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Component public class Dog { public Dog () { System.out.println("Dog Constructor..." ); } @PostConstruct public void init () { System.out.println("Dog init..." ); } @PreDestroy public void destroy () { System.out.println("dog destroy..." ); } }
测试类
1 2 3 4 5 6 7 8 @Test public void test7 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class); System.out.println("容器创建完成..." ); context.close(); }
2.4 BeanPostProcessor后置处理器接口 2.4.1 使用 在bean初始化前后进行一些处理工作。
postProcessBeforeInitialization
:在初始化之前工作
postProcessAfterInitialization
:在初始化之后工作
首先创建MyBeanPostProcessor
类,实现BeanPostProcessor
接口,并添加进容器中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Component public class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization (Object bean, String beanName) throws BeansException { System.out.println("postProcessBeforeInitialization..." + beanName + "=>" + bean); return bean; } @Override public Object postProcessAfterInitialization (Object bean, String beanName) throws BeansException { System.out.println("postProcessAfterInitialization..." + beanName + "=>" + bean); return bean; } }
测试类:
1 2 3 4 5 6 7 8 @Test public void test7 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfLifeCycle.class); System.out.println("容器创建完成..." ); context.close(); }
对象构造后(Dog Constructor),调用初始化之前(Dog init),postProcessBeforeInitialization方法被调用;
对象初始化之后(Dog init),postProcessAfterInitialization方法被调用。
2.4.2 原理 暂略
2.4.3 在spring底层的使用 暂略
3 属性赋值 3.1 @Value赋值 3.1.1 源码 Spring中的@Value注解可以为bean中的属性赋值。我们先来看看@Value注解的源码,如下所示。
1 2 3 4 5 6 @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Value { String value () ; }
从@Value注解的源码中我们可以看出,@Value注解可以标注在字段、方法、参数以及注解 上,而且在程序运行期间生效。
3.1.2 使用 通过@Value注解将外部的值动态注入 到bean的属性中,一般有如下这几种情况:
注入普通字符串
1 2 @Value("Mark") private String name;
注入操作系统属性
1 2 @Value("#{systemProperties['os.name']}") private String systemPropertiesName;
注入SpEL表达式结果
1 2 @Value("#{ T(java.lang.Math).random() * 100.0 }") private double randomNumber;
注入其他bean中属性的值
1 2 @Value("${person.name}") private String username;
注入文件资源
1 2 @Value("classpath:/config.properties") private Resource resourceFile;
注入URL资源
1 2 @Value("http://www.baidu.com") private Resource url;
示例
1 2 3 4 5 6 7 @Configuration public class MainConfigOfPropertyValues { @Bean public Person person () { return new Person(); } }
1 2 3 4 5 6 7 8 9 10 11 @Data @AllArgsConstructor @NoArgsConstructor @ToString public class Person { @Value("Mark") private String name; @Value("#{20-2}") private Integer age; }
1 2 3 4 5 6 7 8 @Test public void test1 () { printBeans(context); System.out.println("------------------" ); Person person = (Person) context.getBean("person" ); System.out.println(person); context.close(); }
可见属性已被成功赋值
3.2 @PropertySource赋值 @PropertySource注解是Spring 3.1开始引入的配置类注解。通过@PropertySource注解可以将properties配置文件中的key/value存储到Spring的Environment中 ,Environment接口提供了方法去读取配置文件中的值,参数是properties配置文件中定义的key值。当然,也可以使用@Value注解用${}
占位符为bean的属性注入值。
3.2.1 使用
首先创建properties文件:person.properties
1 person.nickName = Jayden
在Person类中添加nickName属性,并使用@Value注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Data @AllArgsConstructor @NoArgsConstructor @ToString public class Person { @Value("Mark") private String name; @Value("#{20-2}") private Integer age; @Value("${person.nickName}") private String nickName; }
在配置类中使用@PropertySource注解 ,并导入properties文件
1 2 3 4 5 6 7 8 9 @PropertySource(value = {"classpath:/person.properties"}) @Configuration public class MainConfigOfPropertyValues { @Bean public Person person () { return new Person(); } }
测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void test1 () { printBeans(context); System.out.println("------------------" ); Person person = (Person) context.getBean("person" ); System.out.println(person); ConfigurableEnvironment environment = context.getEnvironment(); String property = environment.getProperty("person.nickName" ); System.out.println(property); context.close(); }
3.2.2 #{ }和${ }的区别 1) ${ }的用法 { }里面的内容必须符合SpEL表达式 ,通过@Value(“${spelDefault.value}”)我们可以获取属性文件中对应的值,但是如果属性文件中没有这个属性,那么就会报错。不过,我们可以通过赋予默认值来解决这个问题,如下所示。
1 2 @Value("${author.name:meimeixia}") private String name;
上述代码的含义是表示向bean的属性中注入属性文件中的author.name属性所对应的值,如果属性文件中没有author.name这个属性,那么便向bean的属性中注入默认值meimeixia 。
2) #{ }的用法 { }里面的内容同样也是必须符合SpEL表达式 。例如,
1 2 3 4 5 6 7 @Value("#{'Hello World'.concat('!')}") private String helloWorld;@Value("#{'Hello World'.bytes.length}") private String helloWorldBytes;
3) 混合使用 1 2 3 @Value("#{'${server.name}'.split(',')}") private List<String> severs;
那么反过来是否可以呢?也就是说能否让${}
在外面,#{}
在里面? 失败!
因为Spring执行${}
的时机要早于#{}
,当Spring执行外层的${}
时,内部的#{}
为空,所以会执行失败!
4) 小结
#{···}
:用于执行SpEl表达式,并将内容赋值给属性
${···}
:主要用于加载 外部属性文件中的值
${···}
和#{···}
可以混合使用,但是必须#{}
在外面,${}
在里面
4 自动装配 Spring组件的自动装配就是Spring利用依赖注入,也就是我们通常所说的DI,完成对IOC容器中各个组件的依赖关系赋值。
4.1 Spring规范注解 4.1.1 @Autowired @Autowired注解可以对类成员变量 、方法和构造函数进行标注,完成自动装配的工作。
@Autowired注解可以放在类、接口以及方法上。
在使用@Autowired注解之前,我们对一个bean配置属性时,是用如下XML配置文件的形式进行配置的。
1 <property name ="属性名" value =" 属性值" />
@Autowired源码
1 2 3 4 5 6 @Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Autowired { boolean required () default true ; }
@Autowired注解默认是优先按照类型 去容器中找对应的组件,相当于是调用了如下这个方法:
1 applicationContext.getBean(类名.class);
若找到则就赋值。
如果找到多个相同类型的组件,那么是将变量名称作为组件的id ,再用这个id到IOC容器中进行查找,这时就相当于是调用了如下这个方法:
1 applicationContext.getBean("组件的id" );
4.1.2 @Qualifier @Autowired是根据类型进行自动装配的,如果需要按名称进行装配 ,那么就需要配合 @Qualifier注解来使用了。
源码
1 2 3 4 5 6 7 @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Qualifier { String value () default "" ; }
4.1.3 @Primary 在Spring中使用注解时,常常会使用到@Autowired这个注解,它默认是根据类型Type来自动注入的。但有些特殊情况,对同一个接口而言,可能会有几种不同的实现类,而在默认只会采取其中一种实现的情况下,就可以使用@Primary注解来标注优先使用哪一个实现类。
源码
1 2 3 4 5 @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Primary {}
4.1.4 注解使用 1) @Autowired 这里,我们以之前项目中创建的BookDao、BookService和BookController为例进行说明。
BookDao、BookService和BookController的初始代码分别如下所示。
1 2 3 4 5 @Repository public class BookDao { }
1 2 3 4 5 6 7 8 9 10 11 12 @Service @ToString public class BookService { @Autowired private BookDao bookDao; public void print () { System.out.println(bookDao); } }
1 2 3 4 5 6 7 8 @Controller public class BookController { @Autowired private BookService bookService; }
可以看到,我们在BookService中使用@Autowired注解注入了BookDao,在BookController中使用@Autowired注解注入了BookService。在IOC容器中,有bookDao(类型为BookDao),bookService(类型为BookService),bookController(类型为BookController)的组件bean。
1 2 3 4 5 6 7 8 9 @Configuration @ComponentScan({"com.hongyi.service", "com.hongyi.dao", "com.hongyi.controller"}) public class MainConfigOfAutowired { }
1 2 3 4 5 6 7 8 9 10 11 @Test public void test2 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAutowired.class); BookService bookService = context.getBean(BookService.class); BookDao bookDao = context.getBean(BookDao.class); System.out.println(bookService); System.out.println(bookDao); context.close(); }
可以看到,我们在BookService类中使用@Autowired注解注入的BookDao对象和直接从IOC容器中获取的BookDao对象是同一个对象 。
如果在Spring容器中存在对多个BookDao对象,那么这时又该如何处理呢?
首先,为了更加直观的看到我们使用@Autowired注解装配的是哪个BookDao对象,我们得对BookDao类进行改造,为其加上一个lable字段,并为其赋一个默认值,如下所示。
1 2 3 4 5 6 7 @Repository @Getter @Setter @ToString public class BookDao { private String label = "1" ; }
然后,我们就在MainConfigOfAutowired配置类中手动注入一个BookDao对象,并且显示指定该对象在IOC容器中的bean的id为bookDao2
,并还为该对象的lable字段赋值为2,如下所示。
1 2 3 4 5 6 7 8 9 10 @Configuration @ComponentScan({"com.hongyi.service", "com.hongyi.dao", "com.hongyi.controller"}) public class MainConfigOfAutowired { @Bean("bookDao2") public BookDao bookDao () { BookDao bookDao = new BookDao(); bookDao.setLabel("2" ); return bookDao; } }
目前,在我们的IOC容器中就会注入两个BookDao对象。那此时,@Autowired注解到底装配的是哪个BookDao对象呢?
发现输出的结果信息如下所示(这是我的结果,报错了)。
1 org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.hongyi.dao.BookDao' available: expected single matching bean but found 2: bookDao,bookDao2
下面是参考博客的结果和说明:
可以看到,结果信息输出了lable=1
,这说明,@Autowired注解默认是优先按照类型去容器中找对应的组件,找到就赋值;如果找到多个相同类型的组件,那么再将属性变量的名称作为组件的id,到IOC容器中进行查找。
那我们如何让@Autowired注解装配bookDao2呢? 这个问题问的好,其实很简单,我们只须将BookService类中的bookDao属性的名称全部修改为bookDao2即可,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 @Service @ToString public class BookService { @Autowired private BookDao bookDao2; public void print () { System.out.println(bookDao2); } }
此时在IOC容器中有两个相同类型(BookDao)的组件bookDao(BookDao扫描后添加,label=1)和bookDao2(配置类手动声明添加,label=2),自动装配时,Spring在容器中查找到两个,但是要按照@Autowired下面的变量名称(bookDao2)进行注入,于是得到label=2的bookDao2。
这是我的结果,还是报错了:
1 org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.hongyi.dao.BookDao' available: expected single matching bean but found 2: bookDao,bookDao2
下面是参考博客的结果和结论:
2) @Qulifier 从测试@Autowired注解的结果来看,@Autowired注解默认是优先按照类型去容器中找对应的组件,找到就赋值;如果找到多个相同类型的组件,那么再将属性变量的名称作为组件的id,到IOC容器中进行查找。
如果IOC容器中存在多个相同类型的组件时,那么我们可不可以显示指定@Autowired注解装配哪个组件呢?有些小伙伴肯定会说:废话!你都这么问了,那肯定可以啊!没错,确实是可以的!此时,@Qualifier注解就派上用场了!
在之前的测试案例中,Eclipse控制台中输出了BookDao [lable=2]
,这说明@Autowired注解装配了bookDao2,那我们如何显示的让@Autowired注解装配bookDao呢?
比较简单,我们只需要在BookService类里面的bookDao2字段上添加@Qualifier注解,显示指定@Autowired注解装配bookDao即可,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 @Service @ToString public class BookService { @Qualifier("bookDao") @Autowired private BookDao bookDao2; public void print () { System.out.println(bookDao2); } }
此时,我们再次运行IOCTest_Autowired类中的test01()方法,输出的结果信息如下所示。
可以看到,此时尽管字段的名称为bookDao2,但是我们使用了@Qualifier注解显示指定了@Autowired注解装配bookDao对象,所以,最终的结果中输出了bookDao对象的信息。
3) 容器中无组件的情况 如果IOC容器中无相应的组件,那么会发生什么情况呢?这时我们可以做这样一件事情,先注释掉BookDao类上的@Repository注解,然后再注释掉MainConfigOfAutowired配置类中的bookDao()方法上的@Bean注解。
此时IOC容器中不再有任何BookDao对象了。
接着,我们再次运行IOCTest_Autowired类中的test01()方法,发现Eclipse控制台报了一个错误,截图如下。
1 org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'bookService': Unsatisfied dependency expressed through field 'bookDao2'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.meimeixia.dao.BookDao' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Qualifier(value=bookDao), @org.springframework.beans.factory.annotation.Autowired(required=true)}
解决方案就是在BookService类的@Autowired注解里面添加一个属性required=false
,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 @Service @ToString public class BookService { @Qualifier("bookDao") @Autowired(required = false) private BookDao bookDao2; public void print () { System.out.println(bookDao2); } }
加上required=false
的意思就是说找到就装配,找不到就拉到,就别装配了。
执行结果:
可以看到,当为@Autowired注解添加属性required=false
后,即使IOC容器中没有对应的对象,Spring也不会抛出异常了。不过,此时装配的对象就为null了 。
4) @Primary 在Spring中,对同一个接口而言,可能会有几种不同的实现类,而默认只会采取其中一种实现的情况下,就可以使用@Primary注解来标注优先使用哪一个实现类。
如果IOC容器中相同类型的组件有多个,那么我们不可避免地就要来回用@Qualifier注解来指定要装配哪个组件,这还是比较麻烦的,Spring正是帮我们考虑到了这样一种情况,就提供了这样一个比较强大的注解,即@Primary
。我们可以利用这个注解让Spring进行自动装配的时候,默认使用首选的bean。
首先,我们在MainConfigOfAutowired配置类的bookDao()方法上添加上@Primary注解,如下所示。
1 2 3 4 5 6 7 8 9 10 11 @Configuration @ComponentScan({"com.hongyi.service", "com.hongyi.dao", "com.hongyi.controller"}) public class MainConfigOfAutowired { @Primary @Bean("bookDao2") public BookDao bookDao () { BookDao bookDao = new BookDao(); bookDao.setLabel("2" ); return bookDao; } }
注意:此时,我们需要注释掉BookService类中bookDao字段上的@Qualifier注解,这是因为@Qualifier注解为显示指定装配哪个组件,如果使用了@Qualifier注解,无论是否使用了@Primary注解,都会装配@Qualifier注解标注的对象。
1 2 3 4 5 6 7 8 9 10 11 12 @Service @ToString public class BookService { @Autowired(required = false) private BookDao bookDao2; public void print () { System.out.println(bookDao2); } }
此时在IOC容器中有两个相同类型(BookDao)的组件bookDao(BookDao扫描后添加,label=1)和bookDao2(配置类手动声明添加,label=2),自动装配时,Spring在容器中查找到两个,但是要按照@Primary下面的变量名称(bookDao2)进行注入,于是得到label=2的bookDao2。
4.2 Java规范注解 这两种是Java规范的注解,第一节是Spring规范的注解。
4.2.1 @Resource @Resource注解是Java规范里面的,也可以说它是JSR250规范里面定义的一个注解。该注解默认按照名称 进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,那么默认取字段名将其作为组件的名称在IOC容器中进行查找,如果注解写在setter方法上,那么默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配 。但是需要注意的一点是,如果name属性一旦指定,那么就只会按照名称进行装配。
它与@Autowired注解不同,与@Primary和@Qualifier搭配时,后者并不生效。
使用
1 2 3 4 5 6 7 8 9 10 11 @Service @ToString public class BookService { @Resource private BookDao bookDao; public void print () { System.out.println(bookDao); } }
1 2 3 4 5 6 7 8 9 10 11 @Configuration @ComponentScan({"com.hongyi.service", "com.hongyi.dao", "com.hongyi.controller"}) public class MainConfigOfAutowired { @Primary @Bean("bookDao2") public BookDao bookDao () { BookDao bookDao = new BookDao(); bookDao.setLabel("2" ); return bookDao; } }
可见@Primary注解并不生效
4.2.2 @Inject @Inject注解也是Java规范里面的,也可以说它是JSR330规范里面定义的一个注解。该注解默认是根据参数名 去寻找bean注入,支持Spring的@Primary注解优先注入,@Inject注解还可以增加@Named注解指定要注入的bean。
作用等同于@Autowired,只不过默认按照参数名注入。
要想使用@Inject注解,需要在项目的pom.xml文件中添加如下依赖,即导入javax.inject这个包。
1 2 3 4 5 <dependency > <groupId > javax.inject</groupId > <artifactId > javax.inject</artifactId > <version > 1</version > </dependency >
使用
1 2 3 4 5 6 7 8 9 10 11 @Service @ToString public class BookService { @Inject private BookDao bookDao; public void print () { System.out.println(bookDao); } }
1 2 3 4 5 6 7 8 9 10 11 @Configuration @ComponentScan({"com.hongyi.service", "com.hongyi.dao", "com.hongyi.controller"}) public class MainConfigOfAutowired { @Primary @Bean("bookDao2") public BookDao bookDao () { BookDao bookDao = new BookDao(); bookDao.setLabel("2" ); return bookDao; } }
可见@Primary已将@Inject作用覆盖。
4.2.3 总结
@Autowired是Spring中的专有注解,而@Resource是Java中JSR250规范里面定义的一个注解,@Inject是Java中JSR330规范里面定义的一个注解
@Autowired支持参数required=false,而@Resource和@Inject都不支持
@Autowired和@Inject支持@Primary注解优先注入,而@Resource不支持
@Autowired通过@Qualifier指定注入特定bean,@Resource可以通过参数name指定注入bean,而@Inject需要通过@Named注解指定注入bean
4.3 其他位置的自动装配 @Autowired注解不仅可以标注在字段上,而且还可以标注在构造方法、实例方法以及参数上。
4.3.1 案例准备 首先,我们在项目中新建一个Boss类,在Boss类中有一个Car类的引用,并且我们使用@Component注解将其加载到IOC容器中,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Component public class Boss { private Car car; public Car getCar () { return car; } @Override public String toString () { return "Boss{" + "car=" + car + '}' ; } }
1 2 3 4 @Component public class Car {}
注意,Car类上也要标注@Component注解,即它也要被加载到IOC容器中。
新建好以上Boss类之后,我们还需要在MainConfigOfAutowired配置类的@ComponentScan注解中进行配置,使其能够扫描com.hongyi.bean包下的类。
4.3.2 标注在实例方法 我们可以将@Autowired注解标注在setter方法上,如下所示。
1 2 3 4 @Autowired public void setCar (Car car) { this .car = car; }
1 2 3 4 public void setCar (@Autowired Car car) { this .car = car; }
标注在方法上:Spring容器创建该对象(boss),就会调用该方法,完成赋值。方法使用的参数和自定义类型的值都会从ioc中获取。接下来验证:
1 2 3 4 5 6 7 8 @Test public void test3 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAutowired.class); Boss boss = context.getBean(Boss.class); Car car = context.getBean(Car.class); System.out.println(boss); System.out.println(car); }
可见是同一个对象。
4.3.3 标注在构造方法 spring会默认将类加载进IOC容器中,IOC容器启动的时候默认会调用bean的无参构造器 创建对象,然后再进行初始化、赋值等操作。
接下来,我们为Boss类添加一个有参构造方法,将@Autowired注解标注在有参构造方法上,并在构造方法中打印一条信息,如下所示。
1 2 3 4 5 @Autowired public Boss (Car car) { this .car = car; System.out.println("Boss...有参构造器" ); }
1 2 3 4 5 public Boss (@Autowired Car car) { this .car = car; System.out.println("Boss...有参构造器" ); }
构造器要用到的参数,也是从ioc中获取。
如果组件中只有一个有参构造器 ,这个有参构造器的@Autowired可以省略:
1 2 3 4 public Boss (Car car) { this .car = car; System.out.println("Boss...有参构造器" ); }
运行结果:
4.3.4 标注在参数 见2,3小节的形式2。无论@Autowired注解是标注在字段上、实例方法上、构造方法上还是参数上,参数位置的组件都是从IOC容器中获取。
4.3.5 标注在方法位置 @Autowired注解可以标注在某个方法的位置上。这里,为了更好的演示效果,我们新建一个Color类,在Color类中有一个Car类型的成员变量,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Color { private Car car; public Car getCar () { return car; } public void setCar (Car car) { this .car = car; } @Override public String toString () { return "Color{" + "car=" + car + '}' ; } }
然后,我们在MainConfigOfAutowired配置类中实例化Color类,如下所示。
1 2 3 4 5 6 7 8 9 @Bean public Color color (Car car) { Color color = new Color(); color.setCar(car); return color; }
测试方法:
1 2 3 4 5 6 7 8 9 10 @Test public void test3 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAutowired.class); Boss boss = context.getBean(Boss.class); Car car = context.getBean(Car.class); Color color = context.getBean(Color.class); System.out.println(boss); System.out.println(car); System.out.println(color); }
执行结果:
发现三者用到的car都是同一个对象
综上,我们用到最多的还是把@Autowired注解标注在方法位置,即使用@Bean注解+方法参数这种形式,此时,该方法参数的值从IOC容器中获取,并且还可以默认不写@Autowired注解,因为效果都是一样的,都能实现自动装配!
4.4 获取Spring底层组件 4.4.1 概述 如果我们现在自定义的组件中需要用到Spring底层的一些组件,比如ApplicationContext(IOC容器)、底层的BeanFactory等等,那么该怎么办呢?先说说自定义的组件中能不能用Spring底层的一些组件吧?既然都这样说了,那么肯定是能够的。
回到主题,自定义的组件要想使用Spring容器底层的一些组件,比如ApplicationContext(IOC容器)、底层的BeanFactory等等,那么只需要让自定义组件 实现XxxAware接口 即可。此时,Spring在创建对象的时候,会调用XxxAware接口中定义的方法注入相关的组件。
本质上,Spring中形如XxxAware这样的接口都继承了Aware接口。接下来,我们看看都有哪些接口继承了Aware接口,如下所示。
4.4.2 使用 接下来,我们就挑选几个常用的XxxAware接口来简单的说明一下。
ApplicationContextAware
接口使用的比较多,我们先来说说这个接口,通过ApplicationContextAware接口我们可以获取到IOC容器 。
首先,我们创建一个Red类,它得实现ApplicationContextAware接口,并在实现的setApplicationContext()方法中将ApplicationContext输出,我们也可以让Red类同时实现几个XxxAware接口,例如,使Red类再实现一个BeanNameAware接口,我们可以通过BeanNameAware接口获取到当前bean在Spring容器中的名称,当然了,我们可以再让Red类实现一个EmbeddedValueResolverAware接口,我们通过EmbeddedValueResolverAware接口能够获取到String值解析器,如下所示。
ApplicationContextAware
BeanNameAware
EmbeddedValueResolverAware
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Component public class Red implements ApplicationContextAware , BeanNameAware , EmbeddedValueResolverAware { private ApplicationContext context; @Override public void setApplicationContext (ApplicationContext applicationContext) throws BeansException { System.out.println("传入的ioc: " + applicationContext); this .context = applicationContext; } @Override public void setBeanName (String s) { System.out.println("当前bean的名字: " + s); } @Override public void setEmbeddedValueResolver (StringValueResolver stringValueResolver) { String s = stringValueResolver.resolveStringValue("你好${os.name} 我是#{20 * 18}" ); System.out.println("解析的字符串: " + s); } }
IOC容器启动时会自动地将String值的解析器(即StringValueResolver)传递过来给我们用,咱们可以用它来解析一些字符串,解析哪些字符串呢?比如包含#{}
这样的字符串。
测试类
1 2 3 4 5 6 @Test public void test3 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAutowired.class); System.out.println(context); }
可见返回的是同一个ioc容器。
4.4.3 原理 XxxAware接口的底层原理是由XxxAwareProcessor实现类实现的,也就是说每一个XxxAware接口都有它自己对应的XxxAwareProcessor实现类。 例如,我们这里以ApplicationContextAware接口为例,ApplicationContextAware接口的底层原理就是由ApplicationContextAwareProcessor类实现的。从ApplicationContextAwareProcessor类的源码可以看出,其实现了BeanPostProcessor接口,本质上是一个后置处理器 。
接下来,我们就以分析ApplicationContextAware接口的原理为例,看看Spring是怎么将ApplicationContext对象注入到Red类中的。
首先,我们在Red类的setApplicationContext()方法上打一个断点,如下所示。
然后,我们以debug的方式来运行IOCTest_Autowired类中的test02()方法。
这里,我们可以看到,实际上ApplicationContext对象已经注入到Red类的setApplicationContext()方法中了。
接着,我们在Eclipse的方法调用栈中找到postProcessBeforeInitialization()方法并鼠标单击它,如下所示,此时,自动定位到了postProcessBeforeInitialization()方法中。
其实,postProcessBeforeInitialization()方法所在的类就是ApplicationContextAwareProcessor。postProcessBeforeInitialization()方法的逻辑还算比较简单。
4.5 根据环境注册组件 在实际的企业开发环境中,往往都会将环境分为开发环境、测试环境和生产环境 ,并且每个环境基本上都是互相隔离的,也就是说,开发环境、测试环境和生产环境它们之间是互不相通的。在以前的开发过程中,如果开发人员完成相应的功能模块并通过单元测试后,那么他会通过手动修改配置文件的形式,将项目的配置修改成测试环境,发布到测试环境中进行测试。测试通过后,再将配置修改为生产环境,发布到生产环境中。这样手动修改配置的方式,不仅增加了开发和运维的工作量,而且总是手工修改各项配置文件会很容易出问题。那么,有没有什么方式可以解决这些问题呢?答案是:有!通过@Profile
注解就可以完全做到这点。
4.5.1 @Profile概述 在容器中如果存在同一类型的多个组件 ,那么可以使用@Profile注解标识要获取的是哪一个bean。也可以说@Profile注解是Spring为我们提供的可以根据当前环境,动态地激活和切换一系列组件的功能。这个功能在不同的环境使用不同的变量的情景下特别有用,例如,开发环境、测试环境、生产环境使用不同的数据源,在不改变代码的情况下,可以使用这个注解来动态地切换要连接的数据库。
1 2 3 4 5 6 7 @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional({ProfileCondition.class}) public @interface Profile { String[] value(); }
从其源码中我们可以得出如下三点结论:
@Profile注解不仅可以标注在方法上,也可以标注在配置类上。
如果@Profile注解标注在配置类上,那么只有是在指定的环境的时候,整个配置类里面的所有配置才会生效。
如果一个bean上没有使用@Profile注解进行标注,那么这个bean在任何环境下都会被注册到IOC容器中,当然了,前提是在整个配置类生效的情况下。
4.5.2 注解使用 接下来,我们就一起来看一个案例,即使用@Profile注解实现开发、测试和生产环境的配置和切换 。这里,我们以开发过程中要用到的数据源为例(数据源也是一种组件哟😊)。
我们希望在开发环境中,数据源是连向A数据库的;在测试环境中,数据源是连向B数据库的,而且在这一过程中,测试人员压根就不需要改动任何代码;最终项目上线之后,数据源连向C数据库,而且最重要的一点是在整个过程中,我们不希望改动大量的代码,而实现数据源的切换。
1) 环境搭建
首先,我们需要在pom.xml文件中添加c3p0数据源和MySQL驱动的依赖,如下所示。
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > c3p0</groupId > <artifactId > c3p0</artifactId > <version > 0.9.1.2</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.44</version > </dependency >
添加完以上依赖之后,我们还得在项目中新建一个配置类,例如MainConfigOfProfile,并在该配置类中模拟开发、测试、生产环境的数据源,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @Configuration @PropertySource("classpath:/dbconfig.properties") public class MainConfigOfProfile implements EmbeddedValueResolverAware { @Value("${db.user}") private String user; private StringValueResolver resolver; private String driverClass; @Bean("testDataSource") public DataSource dataSourceTest (@Value("${db.password}") String pwd) throws PropertyVetoException { ComboPooledDataSource dataSource = new ComboPooledDataSource(); dataSource.setUser(user); dataSource.setPassword(pwd); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test" ); dataSource.setDriverClass(driverClass); return dataSource; } @Bean("devDataSource") public DataSource dataSourceDev (@Value("${db.password}") String pwd) throws PropertyVetoException { ComboPooledDataSource dataSource = new ComboPooledDataSource(); dataSource.setUser(user); dataSource.setPassword(pwd); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/dev" ); dataSource.setDriverClass(driverClass); return dataSource; } @Bean("prodDataSource") public DataSource dataSourceProd (@Value("${db.password}") String pwd) throws PropertyVetoException { ComboPooledDataSource dataSource = new ComboPooledDataSource(); dataSource.setUser(user); dataSource.setPassword(pwd); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/prod" ); dataSource.setDriverClass(driverClass); return dataSource; } @Override public void setEmbeddedValueResolver (StringValueResolver stringValueResolver) { this .resolver = stringValueResolver; driverClass = resolver.resolveStringValue("${db.driverClass}" ); } }
其中配置文件dbconfig.properties
:
1 2 3 db.user = root db.password = 12345678 db.driverClass = com.mysql.jdbc.Driver
测试类:
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void test4 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfProfile.class); String[] names = context.getBeanNamesForType(DataSource.class); for (String name: names) { System.out.println(name); } context.close(); }
可见,三个数据源都被注册到容器当中。
2) @Profile使用 指定组件在哪一个环境的情况下才能被注册到容器中,不指定则任何环境下都能注册这个组件
加了环境标识的bean,只有在这个环境被激活的时候才能注册到容器中。@Profile默认的值是default
写在配置类上时,只有是在指定的环境的时候,整个配置类里面所有的配置才能生效
没有标注环境标识的bean,任何环境下都是加载的。
指定运行环境
使用命令行动态参数指定激活环境:在虚拟机参数中添加:
1 -Dspring.profiles.active=test
运行结果:
代码方式指定激活环境:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public void test4 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.getEnvironment().setActiveProfiles("test" , "dev" ); context.register(MainConfigOfProfile.class); context.refresh(); String[] names = context.getBeanNamesForType(DataSource.class); for (String name: names) { System.out.println(name); } context.close(); }
写在配置类上时:
1 2 3 4 5 6 7 @Configuration @Profile("prod") @PropertySource("classpath:/dbconfig.properties") public class MainConfigOfProfile implements EmbeddedValueResolverAware { }
测试类1:
1 2 3 4 5 6 @Test public void test4 () { context.getEnvironment().setActiveProfiles("prod" ); }
测试类2:
1 2 3 4 5 6 @Test public void test4 () { context.getEnvironment().setActiveProfiles("test" ); }
可见没有数据源被注册。
没有环境标识的bean存在时:
1 2 3 4 5 6 7 8 9 10 @Configuration @PropertySource("classpath:/dbconfig.properties") public class MainConfigOfProfile implements EmbeddedValueResolverAware { @Bean public Car car () { return new Car(); } }
测试类1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public void test4 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.getEnvironment().setActiveProfiles("test" ); context.register(MainConfigOfProfile.class); context.refresh(); String[] names = context.getBeanDefinitionNames(); for (String name: names) { System.out.println(name); } context.close(); }
测试类2:
1 2 3 4 5 @Test public void test4 () { context.getEnvironment().setActiveProfiles("dev" );
可见,不管何种环境,car都被注册到容器中了。
5 面向切面编程 AOP:指在程序运行期间动态地将某段代码切入到指定方法的指定位置进行运行的编程方式。
AOP(Aspect Orient Programming),直译过来就是面向切面编程。AOP是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。
在《Spring实战(第4版)》中有如下一张图描述了AOP的大体模型。
从这张图中,我们可以看出:所谓切面,其实就相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。
总之一句话:AOP是指在程序的运行期间动态地将某段代码切入到指定方法、指定位置进行运行的编程方式。AOP的底层是使用动态代理实现的。
5.1 AOP功能测试 5.1.1 步骤
导入AOP模块依赖
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-aop</artifactId > <version > 5.3.9</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-aspects</artifactId > <version > 5.3.9</version > </dependency >
定义一个业务逻辑类,在业务逻辑运行的时候,将日志进行打印(方法之前,方法结束运行,方法出现异常等)
1 2 3 4 5 6 public class MathCalculator { public int div (int i, int j) { System.out.println("div方法被调用了..." ); return i / j; } }
定义一个日志切面类:切面类里面的方法需要动态感知业务逻辑类运行到哪里,然后执行相应方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class LogAspects { public void logStart () { System.out.println("除法运行...参数列表是: {}" ); } public void logEnd () { System.out.println("除法结束..." ); } public void logReturn () { System.out.println("除法正常返回...运行结果: {}" ); } public void logException () { System.out.println("除法异常...异常信息: {}" ); } }
通知方法:
前置通知@Before
:logStart:在目标方法(div)运行之前运行
后置通知@After
:logEnd:在目标方法运行结束之后运行,无论是正常结束还是异常结束
返回通知@AfterReturning
:logReturn:在目标方法正常返回之后运行
异常通知@AfterThrowing
:logException:在目标方法出现异常后运行
环绕通知@Around
:动态代理,手动推进目标方法运行(joinPoint.proceed
)
给切面类的目标方法标注何时何地 运行(利用通知注解)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class LogAspects { @Pointcut("execution(public int com.hongyi.aop.MathCalculator.*(..))") public void pointCut () { } @Before("pointCut()") public void logStart () { System.out.println("@Before除法运行...参数列表是: {}" ); } @After("com.hongyi.aop.LogAspects.pointCut()") public void logEnd () { System.out.println("@After除法结束..." ); } @AfterReturning("pointCut()") public void logReturn () { System.out.println("@AfterReturning除法正常返回...运行结果: {}" ); } @AfterThrowing("pointCut()") public void logException () { System.out.println("@AfterThrowing除法异常...异常信息: {}" ); } }
将切面类和业务逻辑类都加入到容器中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration public class MainConfigOfAOP { @Bean public MathCalculator calculator () { return new MathCalculator(); } @Bean public LogAspects logAspects () { return new LogAspects(); } }
告诉Spring哪个是切面类:给切面类加一个注解@Aspect
1 2 3 4 5 6 7 @Aspect public class LogAspects { }
给配置类加上注解@EnableAspectJAutoProxy
:开启基于注解的AOP模式
1 2 3 4 5 @EnableAspectJAutoProxy @Configuration public class MainConfigOfAOP { }
测试方法1
1 2 3 4 5 6 7 @Test public void test5 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAOP.class); MathCalculator calculator = context.getBean(MathCalculator.class); calculator.div(1 , 1 ); context.close(); }
获取业务方法的参数列表和异常等信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Aspect public class LogAspects { @Pointcut("execution(public int com.hongyi.aop.MathCalculator.*(..))") public void pointCut () { } @Before("pointCut()") public void logStart (JoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); System.out.println(joinPoint.getSignature().getName() + "@Before除法运行...参数列表是: {" + Arrays.asList(args) +"}" ); } @After("com.hongyi.aop.LogAspects.pointCut()") public void logEnd (JoinPoint joinPoint) { System.out.println(joinPoint.getSignature().getName() + "@After除法结束..." ); } @AfterReturning(value = "pointCut()", returning = "result") public void logReturn (JoinPoint joinPoint, Object result) { System.out.println(joinPoint.getSignature().getName() + "@AfterReturning除法正常返回...运行结果: {" + result +"}" ); } @AfterThrowing(value = "pointCut()", throwing = "exception") public void logException (JoinPoint joinPoint, Exception exception) { System.out.println(joinPoint.getSignature().getName() + "@AfterThrowing除法异常...异常信息: {" + exception +"}" ); } }
测试方法2
1 2 3 4 5 6 7 @Test public void test5 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAOP.class); MathCalculator calculator = context.getBean(MathCalculator.class); calculator.div(1 , 1 ); context.close(); }
测试方法3
1 2 3 4 5 6 7 @Test public void test5 () { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAOP.class); MathCalculator calculator = context.getBean(MathCalculator.class); calculator.div(1 , 0 ); context.close(); }
5.1.2 总结
三步走战略
将业务逻辑组件和切面类都加入到容器中,并告诉Spring哪一个是切面类(@Aspect)
在切面类上的每一个通知方法上标注通知注解,告诉Spring何时何地运行(利用切入点表达式)
开启基于注解的aop模式,@EnableXXX
5.2 AOP原理 5.2.1 @EnableAspectJAutoProxy