Spring注解驱动开发

概述

Spirng注解驱动开发,目前在我看来目前在我看来就是注解替代配置文件(不是部分而是可以全部替代)

以前我们在使用spring的时候,基本上都是注解跟xml配置相互结合,自己写的类就使用注解,导入的jar包例如框架整合等就基本上都是xml配置

现在我们可以使用完全可以使用Sspring为我们提供的注解来替代我们的xml的配置文件(不知道对不对,但是能替代大部分xml的配置是肯定的)

学习目的
为将来的SpirngBoot跟SpringCloud学习打下基础(学习阶段暂时就想到这么多)

组件注册

注册Bean

我们以前注册Bean要么是在xml文件中声明,要么就是使用 @Repository,@Service,
@Controller,@Component,然后配置包扫描

现在我们将不再使用配置文件(至少现在是),而是通过写代码的方式完成将Bean加入到IOC容器中

@Bean

指定配置Spirng的类,并且注册Bean

@Configuration
public class MainConfig {

    @Bean
    public Person person(){
        return new Person("lisi",12);
    }
}

@Configuration注解可以不加,创建ApplicationContext对象时传入的类即可被认定为容器配置类,Spring并不会自动扫描注解有@Configuration的类并将其视为容器类,@Bean注解的方法的返回值即为注册的Bean,Bean的id默认为方法名,id可通过@Bean注解的属性指定

public class MainTest {
    public static void main(String args[]){
//        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//        Person p =  (Person) context.getBean("person");
//        System.out.print(p);
        ApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class);
        Person person = context.getBean(Person.class);
        System.out.print(person);
    }
}

ClassPathXmlApplicationContext和AnnotationConfigApplicationContext均为ApplicationContext的实现类,前者用来加载spring的xml配置文件,后者用来加载spring的配置类,加载完spring的xml配置文件或配置类后spring容器就会创建对象,这两种方式会有些不同:使用xml配置的方式会默认调用指定Class的无参构造器,也可以制定构造器,属性的值是通过Setter方法注入的;而使用配置类时则会调用配置类中所有@Bean注解的方法,调用的构造器是在该方法中指明的。

@ComponentScan

指定容器类的扫描范围和扫描规则

@Configuration
@ComponentScan(value = "com.xiaoming", 
        excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, Service.class})})
public class MainConfig {

    @Bean
    public Person person() {
        return new Person("lisi", 12);
    }
}
@Configuration
@ComponentScan(value = "com.bdm", useDefaultFilters = false, includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})})
public class MainConfig {

    @Bean
    public Person person() {
        return new Person("lisi", 12);
    }
}

@ComponentScan注解的value属性指定了扫描的包,表示此包中使用了@Controller、@Service、@Repository、@Component等注解的类会被纳入Spring容器的管理;excludeFilters属性表示该包中哪些类别的类不会被纳入Spring容器管理,它的属性值是一个数组;includeFilters属性表示指定的包中哪些类别的类会被纳入到Spring容器管理,注意要同时设置useDefaultFilters的属性为false。

JDK1.8之后一个容器类可以使用多次@ComponentScan注解,JDK1.8之前则可以使用@ComponentScans实现这个需求

1.8之前

@Configuration
@ComponentScans(value = {@ComponentScan(value = "com.bdm", useDefaultFilters = false, includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})}), @ComponentScan(value = "com.bdm", excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})})})
public class MainConfig {

    @Bean
    public Person person() {
        return new Person("lisi", 12);
    }
}

1.8之后

@ComponentScan(value = "com.xiaoming", useDefaultFilters = false, includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})})
@ComponentScan(value = "com.xiaoming", excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})})
public class MainConfig {

    @Bean
    public Person person() {
        return new Person("lisi", 12);
    }
}

各个注解的解释
- @ComponentScan value:指定要扫描的包
- excludeFilters = Filter[] :指定扫描的时候按照什么规则排除那些组件
- includeFilters = Filter[] :指定扫描的时候只需要包含哪些组件
- FilterType.ANNOTATION:按照注解排除
- FilterType.ASSIGNABLE_TYPE:按照给定的类型排除
- FilterType.ASPECTJ:使用ASPECTJ表达式排除
- FilterType.REGEX:使用正则指定排除
- FilterType.CUSTOM:使用自定义规则排除

CUSTOM:使用自定义规则
自定义规则类需实现TypeFilter接口:

public class BdmTypeFilter implements TypeFilter{

    /**
     * metadataReader:读取到的当前正在扫描的类的信息
     * metadataReaderFactory:可以获取到其他任何类信息的
     */

    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);

        //自定义规则 (返回true就是代表符合规则,会加入的ioc容器中)
        if(className.contains("er")){
            return true;
        }
        return false;
    }

    }
}
  • MetadataReader:获取正被扫描的类的信息,包括该类的注解信息、路径信息、类名信息等

  • MetadataReaderFactory:获取MetadataReader

使用自定义过滤规则:type=FilterType.CUSTOM

@Configuration
@ComponentScan(value="com.bdm",useDefaultFilters = false,includeFilters = {@ComponentScan.Filter(type = FilterType.CUSTOM,classes = {BdmTypeFilter.class})})
public class MainConfig {

    @Bean
    public Person person() {
        return new Person("lisi", 12);
    }
}

则在指定包com.bdm及其子包下所有符合自定义规则的类将被纳入Spring容器管理

总结

由此可以看出Spring容器在通过扫描的方式决定将哪些类纳入容器管理前会先扫描指定包下所有的类,再看这些类是否符合被纳入管理的条件(有没有指定注解、类名是否符合条件、路径是否符合条件等),所以如果在一个包下需要被Spring容器管理的类所占的比例比较小时最好不要使用扫描包的方式,而使用@Bean注解的方式性能会更好些,因为减少了遍历

拓展

如果通过@ComponentScan设置的包有包含关系时,同一个被纳入Spring容器的Class会创建多少个bean实例呢?答案是1个,因为通过包扫描的方式创建的bean实例的id是类名的首字母小写,spring在创建bean时会先判断该id的bean有无创建,已创建则不再创建,否则才创建;那如果除了包扫描中有该类,同时@Bean注解的方法中也有该Class的实例,这个时候会创建几个bean实例呢?这个要看@Bean注解的方法的方法名或者通过@Bean的name属性为该实例指定的id是否和类名的首字母小写后相同,相同则仅创建一个,否则创建多个,说明spring容器在创建bean时会先检查该id的bean是否已经存在,不存在则创建,否则不创建。

设置Bean属性和动态注册Bean

@Scope

设置组件的作用域

public class MainConfigA {
    @Bean("person")
    @Scope("singleton")
    public Person person(){
        return new Person("张三丰",25);
    }
}

@Scope的取值

  • singleton:单实例的(默认值):ioc容器启动会调用方法创建对象放到ioc容器中。
  • prototype:多实例的:ioc容器启动并不会去调用方法创建对象放在容器中。每次获取的时候才会调用方法创建对象
  • request:同一次请求创建一个实例
  • session:同一个session创建一个实例

@Scope注解也可以在组件的类名上标注

@Lazy

实现组件Bean的懒加载

单实例的Bean对象默认在容器启动的时候创建,可以设置懒加载使Bean的创建时机延后至第一次获取的时候创建和初始化,懒加载仅作用于单实例(singleton)的Bean

//  @Scope("prototype")
    @Lazy
    @Bean("person")
    public Person person(){
        System.out.println("给容器中添加Person....");
        return new Person("张三", 25);
    }

@Conditional

按照条件动态注册Bean

public class JunitTest{

    ApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class);

    @Test
    public void test(){
        Map<String,Person> persons = context.getBeansOfType(Person.class);
        System.out.print(persons);
        ConfigurableEnvironment environment = context.getEnvironment();//获取运行环境
        String osName = environment.getProperty("os.name");//获取操作系统名称
    }
}

从这个测试类获取Bean的方式可以看出Spring容器是将Bean的对象放在了Map中,而且同一个类型的bean可能会有多个(id不同),id为key、bean为value,另外通过ApplicationContenxt的对象可以获取当前的运行环境信息,在调用context.getBeansOfType(Person.class)方法时会得到容器中所有类型为Person的对象

使用@Conditional注解需自定义Condition:实现Condition接口

//判断是否linux系统
public class LinuxCondition implements Condition {

    /**
     * ConditionContext:判断条件能使用的上下文(环境)
     * AnnotatedTypeMetadata:注释信息
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // TODO是否linux系统
        //1、能获取到ioc使用的beanfactory
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        //2、获取类加载器
        ClassLoader classLoader = context.getClassLoader();
        //3、获取当前环境信息
        Environment environment = context.getEnvironment();
        //4、获取到bean定义的注册类
        BeanDefinitionRegistry registry = context.getRegistry();

        String property = environment.getProperty("os.name");

        //可以判断容器中的bean注册情况,也可以给容器中注册bean
        boolean definition = registry.containsBeanDefinition("person");
        if(property.contains("linux")){
            return true;
        }

        return false;
    }

}

容器Bean中使用@Conditional注解:使person类仅会在windows系统中才会创建

@Configuration
public class MainConfig {

    @Bean
    @Conditional({MyCondition.class})
    public Person person(){
        return new Person("张三",12);
    }

    @Bean
    public Person person1(){
        return new Person("李四",13);
    }
}

@Conditional注解可以标注在配置类上也可以标注在@Bean注解的方法上,作用的范围不同,标注在类上时影响整个容器Bean

@Conditional的值是一个Condition类型的数组,Condition是一个接口,有一个matches方法,该方法中有两个参数
- ConditionContext 通过此类型的参数可以获取运行环境、Bean工厂等信息
- AnnotatedTypeMetadata 通过此类型的变量可获取到使用@Conditional注解的类的注解信息

@Import

==给容器注入组件的总结==

  • 包扫描+组件标注注解(@Controller/@Service/@Repository/@Component)[自己写的类]

    • 如果是导入的jar包里面的类例如框架的整合就只能通过在xml中配置Bean了,
    • 我们其实还可用使用spring注解开发的方式将第三方jar包里的bean配置到IOC容器中
  • @Bean[导入的第三方包里面的组件]

  • @Import[快速给容器中导入一个组件]
    • @Import(要导入到容器中的组件);容器中就会自动注册这个组件,id默认是全类名
    • ImportSelector:返回需要导入的组件的全类名数组; (接口)
    • ImportBeanDefinitionRegistrar:手动注册bean到容器中 (接口)

@Import的使用

@Import(Color.class)
public class MainConfig2 {}

上述在配置类(等于配置文件)使用@Import注解,当创建spring容器的时候,Color类会被注册到容器中但是给Bean的ID不会类的首字母小写而是全类名(com.xiaoming.bean.Color)

@Import也可以同时导入多个Bean

@Import({xxx.class,xx.class})

从上面我们可以看出@Import除了可以传入类名.class,还可以传入@ImportSelector,以及@ImportBeanDefinitionRegistrar我们接下来就来学习一下

ImportSelector(注入Bean的选择器)
- 返回需要纳入容器的组件的全类名数组
- springboot中用的比较多

//自定义逻辑返回需要导入的组件
public class MyImportSelector implements ImportSelector {

    //返回值,就是到导入到容器中的组件全类名
    //AnnotationMetadata:当前标注@Import注解的类的所有注解信息
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        //importingClassMetadata
        //方法不要返回null值(可以是一个空数组)
        return new String[]{"com.atguigu.bean.Blue","com.atguigu.bean.Yellow"};
    }

}
  • 配置类
@Import({Color.class,Red.class,MyImportSelector.class})
public class MainConfig2 {}

综上,当加载完配置类创建Spring容器后Color Red 以及 MyImportSelector中返回的数组中的类就全在容器中了但是id都是全类名

ImportBeanDefinitionRegistrar

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    /**
     * AnnotationMetadata:当前类的注解信息
     * BeanDefinitionRegistry:BeanDefinition注册类;
     *      把所有需要添加到容器中的bean;调用
     *      BeanDefinitionRegistry.registerBeanDefinition手工注册进来
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

        //容器中是否有给类型的Bean
        boolean definition = registry.containsBeanDefinition("com.atguigu.bean.Red");
        boolean definition2 = registry.containsBeanDefinition("com.atguigu.bean.Blue");
        if(definition && definition2){
            //指定Bean定义信息;(Bean的类型,Bean。。。)
            RootBeanDefinition beanDefinition = new RootBeanDefinition(RainBow.class);
            //注册一个Bean,指定bean名
            registry.registerBeanDefinition("rainBow", beanDefinition);
        }
    }

}
  • 配置类
@Import({Color.class,Red.class,MyImportSelector.class,MyImportBeanDefinitionRegistrar.class})
public class MainConfig2 {}

FactoryBean接口

FactoryBean是一个接口,子类需实现其三个方法:

  • getObject():获取对象,必须重写

  • getObjectType():获取对象类型,必须重写

  • isSingleton():是否单例,可以重写

public class MyPersonFactoryBean implements FactoryBean<Person> {

    public Person getObject() throws Exception {
        return new Person("小吴",23);
    }

    public Class<?> getObjectType() {
        return Person.class;
    }

    /**
     * true:单例
     * false:每次获取都重新创建新实例
     */
    public boolean isSingleton() {
        return true;
    }

}

配置类中配置工厂Bean

@Configuration
public class MainConfig {

    @Bean
    public MyPersonFactoryBean myPersonFactoryBean(){
        return new MyPersonFactoryBean();
    }
}

测试会发现在容器中获取id为myPersonFactoryBean的Bean的实例的类型是Person:说明将工厂Bean注册后获取的对象其实是工厂Bean的getObject()方法返回的实例,那如果想获取MyPersonFactoryBean类型的实例呢?在id的前面加个&即可

Bean的声明周期

bean的生命周期:

bean创建—初始化—-销毁的过程

Spring容器管理着bean的生命周期
我们可以自定义初始化和销毁方法;容器在bean进行到当前生命周期的时候来调用我们自定义的初始化和销毁方法

Bean完整的生命周期为:

  • BeanPostProcessor.postProcessBeforeInitialization
  • 初始化:对象创建完成,并赋好值调用了初始化方法
  • BeanPostProcessor.postProcessAfterInitialization
  • 销毁:
    • 单实例:容器关闭的时候
    • 多实例:容器不会管理多实例的bean,所以容器不会调用他的销毁方法

我们自定义bean的声明周期方法

  1)、指定初始化和销毁方法;
        通过@Bean指定init-methoddestroy-method;
  2)、通过让Bean实现InitializingBean(定义初始化逻辑),
                DisposableBean(定义销毁逻辑);
  3)、可以使用JSR250;
        @PostConstruct:在bean创建完成并且属性赋值完成;来执行初始化方法
        @PreDestroy:在容器销毁bean之前通知我们进行清理工作
  4)、BeanPostProcessor【interface】:bean的后置处理器;
        在bean初始化前后进行一些处理工作;
        他有两个方法:
            postProcessBeforeInitialization:在初始化之前工作
            postProcessAfterInitialization:在初始化之后工作

Spring底层对 BeanPostProcessor 的使用:

bean赋值,注入其他组件,@Autowired,生命周期注解功能,@Async,xxx BeanPostProcessor;

1.通过@Bean的属性指定初始化和销毁方法:这两个方法必须是无入参的

public class Car {

    public Car(){
        System.out.println("构造");
    }

    public void init(){
        System.out.println("初始化");
    }

    public void destroy(){
        System.out.println("销毁");
    }
}

容器Bean:

@Configuration
public class LifeCycleConfig {

    @Bean(initMethod="init",destroyMethod="destroy")
    public Car car(){
        return new Car();
    }
}

对于单实例的Bean来说:容器启动时就会调用该Bean的构造器和初始化方法,销毁方法会在容器关闭的时候调用

对于其他类型的Bean来说:容器会在获取Bean时调用完构造器之后调用其初始化方法,但是容器不会销毁多实例Bean

public class MainTest {

    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LifeCycleConfig.class);

    @Test
    public void test1(){
        context.close();
    }

}

注意上面的context.close()方法并不是在ApplicationContext接口中定义的,而是在其子类中定义的
这样我们就可以在Bean的初始化和销毁时执行一些逻辑,比如数据源的开启和关闭

2.让Bean实现InitializingBean和DisposableBean两个接口

public class Car implements InitializingBean,DisposableBean{

    public Car(){
        System.out.println("构造");
    }

    public void afterPropertiesSet() throws Exception {
        System.out.println("初始化");
    }

    public void destroy() throws Exception {
        System.out.println("销毁");
    }

}

这样容器在调用完构造器之后就会接着调用该Bean的afterPropertiesSet方法完成初始化,如果为单例Bean,在容器关闭时会调用destroy方法执行销毁逻辑

3.使用@PostConstruct和@PreDestroy注解

public class Car {

    public Car() {
    }

    @PostConstruct
    public void init() {
    }

    @PreDestroy
    public void destroy() {
    }
}

@PostConstruct:在执行完构造器并且对象的所有属性被赋值之后调用

@PreDestroy:在对象销毁之前调用,相当于销毁前的通知方法

了解:

在Bean创建和初始化时的执行顺序:构造器->@PostConstruct注解的方法->InitializingBean接口的afterPropertiesSet->@Bean的initMethod属性指定的方法

在Bean销毁时的调用顺序:@PreDestroy注解的方法->DisposableBean接口的destroy方法->@Bean的destroyMethod属性指定的方法

bean的后置处理器

BeanPostProcessor:是一个接口,bean的后置处理器(实际是在bean的初始化前后执行操作对应着接口里面的两个方法)

public class MyBeanPostProcessor implements BeanPostProcessor{

    /**
     * bean是容器调用构造器创建的实例
     * beanName是实例的id
     * 在所有初始化方法之前调用
     */
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }


    /**
     * bean是容器调用构造器创建的实例
     * beanName是实例的id
     * 在所有初始化方法之后调用
     */
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}

将MyBeanPostProcessor纳入到容器中:

@Configuration
@Import(MyBeanPostProcessor.class)
public class LifeCycleConfig {

    @Bean(initMethod="init0",destroyMethod="destroy0")
    public Car car(){
        return new Car();
    }
}

==这样该容器中的所有bean在初始化前后都会执行该后置处理器中的逻辑,即使未定义初始化方法这些逻辑也会执行的==,由此可以猜测:BeanPostProcessor可能是面向切面的

跟着视频查看源码(实际上是找的博客),整个逻辑大体是这样的:

在调用构造器创建bean的时候,会为对象的所有属性赋值,赋值完成后会调用初始化方法,但是在调用初始化方法时会进入一个切面,完成在初始化的前后执行BeanPostProcessor的逻辑:

容器会遍历所有的BeanPostProcessor并依次执行每个后置处理器的前置方法(postProcessBeforeInitialization),直至全部执行完或者其中一个该类方法返回null,然后完成Bean的初始化,初始化完成后,容器会继续遍历执行所有的BeanPostProcessor的后置方法(postProcessAfterInitialization),直至全部执行完或者其中一个该方法返回null

BeanPostProcessor在Spring底层的使用

1.组件通过实现ApplicationContextAware接口获取IOC容器,其实是ApplicationContextAwareProcessor在起作用

@Component
public class ContextBean implements ApplicationContextAware {

    private ApplicationContext context;

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
}

怎么获取到的IOC容器呢?

容器在调用Bean的构造器并给所有属性赋值之后,在执行所有初始化方法之前和之后,会执行一些BeanPostProcessor接口的子类中的逻辑,比如ApplicationContextAwareProcessor,在执行ApplicationContextAwareProcessor的postProcessBeforeInitialization()方法时,会判断Bean是否实现了ApplicationContextAware接口,然后注入相应的ApplicationContext容器给Bean使用

2.对组件的属性进行校验的BeanValidationPostProcessor

3.InitDestroyAnnotationBeanPostProcessor

扫描Bean中标注了@PostConstruct和@PreDestroy注解的方法并执行,这也就是为什么@PostConstruct标注的初始化方法在其他初始化方法之前执行

4.AutowiredAnnotationBeanPostProcessor

为bean中标注了@Autowired注解的属性赋值,这就是为什么可以在init()方法中使用@Autowired注解的属性的原因,因为在调用init方法之前会将这些属性的值注入

5.AsyncAnnotationBeanPostProcessor:

使标注了@Async的方法异步执行

属性赋值

@Value

  • 使用@Value赋值;
  • 1、基本数值
  • 2、可以写SpEL; #{}
  • 3、可以写${};取出配置文件【properties】中的值(在运行环境变量里面的值)
public class Person {

    @Value("张三丰")
    private String name;
    @Value("#{20-2}")
    private Integer age;
    @Value("${property.address}")
    private String address;

    ......
}

可以通过@Value注入基本数据类型、字符串、SpEL表达式(#{})、也可获取运行环境变量(${}),即properties文件中的变量值,在之前采用配置的方式的时候,需要使用

@Configuration
@PropertySource("classpath:config.properties")
public class MainConfig {

    @Bean
    public Person person() {
        return new Person();
    }
}

配置文件中的值也可以通过applicationContext来获取:因为容器加载时会将这些配置的键值对存放在环境变量中

public class MainTest {

    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class);

    @Test
    public void test1() throws Exception {
        ConfigurableEnvironment environment = context.getEnvironment();
        String property = environment.getProperty("property.address");
        System.out.println(property);
        context.close();
    }
}

@Autowire.@Qualifier

默认先按照类型去容器中找相应的组件(applicationContext.getBean(Xxx.class)),若只找到一个则进行装配,若找到多个则再按照属性的名称作为id找对应的组件(applicationContext.getBean(“beanId”));

另外可使用@Qualifier指定属性Bean的id进行装配,如果没有找到指定的属性bean,则会报NoSuchBeanDefinitionException的异常,如果想在找不到属性bean的时候不让程序出错,则可以设置@Autowired的required属性为false:@Autowired(required=false),此时则在容器中有该属性bean时装配,无则不装配

@Service
public class BookService {
    @Autowired(required=false)
    @Qualifier("bookDao")
    private BookDao bookDao;
}

看下面代码

@Configuration
@ComponentScan({"com.bdm.service","com.bdm.dao"})
public class MyAutowireConfig {
    @Bean
    public BookDao bookDao2(){
        return new BookDao();
    }
}

此时该配置类容器中会有两个BookDao实例,一个是通过自动扫描创建的,一个是通过@Bean注解创建的,通过自动扫描创建的bean的id默认为类名的首字母小写,通过@Bean注解创建的id则为方法名

@Primary

使用Spring提供的@Primary注解指定首选装配的Bean:在容器中有多个该类型的bean时,优先装配该bean

@Configuration
@ComponentScan({"com.bdm.service","com.bdm.dao"})
public class MyAutowireConfig {

    @Bean
    @Primary
    public BookDao bookDao2(){
        return new BookDao();
    }
}

此时在容器中有多个BookDao类型的bean时会优先装配这个由@Primary注解的bean,除非使用@Qualifier明确制定了要装配的bean的id

@Resource , @Inject

@Resource(JSR250规范)和@Inject(JSR330规范):这两个是Java规范的注解,而@Autowired是Spring自己的注解

@Resource默认是按照组件的名称(属性名作为id)装配,而不像@Autowired是默认先按照类型装配,使用@Resource时@Primary注解会失效,也可以使用@Resource注解的name属性指定组件,类似于@Autowired结合@Qualifier,但是@Resource不支持required=false,也就是说使用@Resource时必须装配成功,否则会报错:NoSuchBeanDefinitionException

使用@Inject时需要导入依赖:javax.inject

<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency>

@Inject的功能和@Autowired的功能一样,也支持@Primary注解,只是少了required=false的属性

@Service
public class BookService {

    //@Autowired(required=false)
    //@Qualifier("bookDao3")
    //@Resource(name="bookDao2")
    @Inject
    private BookDao bookDao;

}

这三个注解的区别在于:@Autowired是Spring的注解,脱离不了Spring的支持,@Resource和@Inject是java的规范,其他的框架也会支持,但是@Autowired的功能稍微强大一些

这些注解之所以能完成自动装配的功能,是因为Spring提供的后置处理器AutowiredAnnotationBeanPostProcessor,该后置处理器通过解析@Autowired、@Resource、@Inject等注解完成自动装配

总结:虽然说使用javaee规范理论上更加好(因为是规范侵入性低),但是我个人推荐

@Autowired

@Autowired深入学习

@Autowired不止可以标注在属性上,也可以标注在构造器、方法、参数上

1.标注在方法上

@Component
public class Student {

    private Bike bike;

    public Bike getBike() {
        return bike;
    }

    @Autowired
    public void setBike(Bike bike) {
        this.bike = bike;
    }

}

一般标注在Setter方法上,其实可以标注在任何方法上,但是方法一定要有入参

此时,Spring容器在初始化Student对象后就会调用其标注@Autowired的方法将属性注入进去,注入的方式依然是先根据类型查找,查到多个时再根据参数名匹配。

那么问题来了,标注有@Autowired注解的方法是何时调用的呢?

如果在被注入的对象创建之前调用肯定是不行的,在从容器中获取Student的实例时该实例就已经有了bike属性值,说明该属性的注入并非在第一次使用时,而是在容器创建bean之后通过后置处理的方式注入的该属性,难道是在容器中所有的Bean都创建完之后才会调用这些@Autowired注解的方法吗?

是的,其实是AutowiredAnnotationBeanPostProcessor后置处理器的功劳,在创建完Bean之后会后置处理这些标注有@Autowired注解的方法、属性、入参等。那么还有一个问题假如容器中的bean没有无参构造器,但有一个标注了@Autowired注解的有参构造器(在只有一个有参构造器时该构造器上的@Autowired可以省略,就是说隐藏带有@Autowired),此时在创建bean的时候就需要注入属性,那就不是AutowiredAnnotationBeanPostProcessor能解决的了,这时会怎么样呢?这时Spring会将这个Bean的创建时机置后,待其他的Bean都创建完之后再创建该Bean,如果两个类中都有@Autowired注解的构造器,且彼此的构造器中的入参相互引用的话则会报错,因为这两个对象都无法创建,报UnsatisfiedDependencyException异常,此时采用@Autowired(required=false)也不可以,原因也很简单,因为在创建对象时都需要检查彼此有没有创建好,而返回的都是false,所以不可以。

@Component
public class Student {
    private Bike bike;

    @Autowired
    public Student(Bike bike) {
        this.bike = bike;
    }
    public Bike getBike() {
        return bike;
    }
    public void setBike(Bike bike) {
        this.bike = bike;
    }
}

默认情况下,Spring在实例化bean时会调用类的无参构造器,但是在类提供了有参构造器时,如果有参构造器上有@Autowired注解,Spring则会调用该有参构造器创建实例,如果没有,则还是调无参构造器(无参构造器存在)

那么问题来了,如果类中既有无参构造,又有@Autowired注解的有参构造,或者有多个@Autowired注解的有参构造时Spring会怎么创建实例呢?

  • 在有多个@Autowired标注的构造器时会报错
  • 在仅有一个@Autowired标注的构造器时,会调用@Autowired标注的构造器,
  • 如果只有一个有参构造器时,@Autowired可以省略。

标注在参数上:作用和标注在方法上是一样的,只不过标注在参数上可以更精确的指明哪些参数需要由Spring注入,哪些不必由Spring注入

@Component
public class Student {
    private Bike bike;

    private Car car;

    public Student(@Autowired Bike bike, Car car) {
        this.bike = bike;
        this.car = car;
        System.out.println("两个参数...");
    }

    public Bike getBike() {
        return bike;
    }

    public void setBike(Bike bike) {
        this.bike = bike;
    }

    public Car getCar() {
        return car;
    }

    public void setCar(Car car) {
        this.car = car;
    }
}

在IOC容器中的bean的方法的入参中如果有Spring的底层类型,则会自动注入进去

@Component
public class Car {
    public Car(ApplicationContext applicationContext) {
        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
        for (String string : beanDefinitionNames) {
            System.out.println(string);
        }
    }
}

此处构造器中的ApplicationContext会由容器注入进去

Aware接口

Spring的Aware接口:在Spring中有个Aware(意识到,知道的)接口

public interface Aware {

}

Spring中很多接口继承于该接口,该接口的子接口可以为Spring容器中的组件提供Spring自带的一些组件,比如ApplicationContext、BeanFactory等,只需要实现XxxAware接口即可,比如容器中的自己写的组件通过实现ApplicationContextAware接口就可以获取ApplicationContext对象

public interface ApplicationContextAware extends Aware {
    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}
@Component
public class ContextBean implements ApplicationContextAware{

    //我们可以使用这个applicationContext对象
    private ApplicationContext context;

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }

}

组件Bean仅仅是实现了接口,接口中有个setXxx(Xxx xxx)的方法,组件Bean实现的接口并不会为相应的Xxx赋值,那么这些值是怎么注入到组件Bean中的呢?

是Spring通过回调的方式注入的,也就是说Spring在发现组件Bean实现了XxxAware接口时,会调用该接口的setXxx(Xxx xxx)方法将xxx(ApplicationContext、BeanFactory等)注入到相应的组件bean中

常用的Aware子接口
- BeanNameAware: 查找当前Bean的名字
- ApplicationContextAware: 获取当前的IOC容器
- BeanFactoryAware: 获取beanFactory
- EmbeddedValueResolverAware: Spring内置的值解析器,可以解析出占位符、SpEL表达式等

@Component
public class ContextBean implements EmbeddedValueResolverAware {

    private StringValueResolver resolver;

    public void setEmbeddedValueResolver(StringValueResolver resolver) {
        String resolveStringValue = resolver.resolveStringValue("你好:${os.name},我今年#{2*15}岁了");
        System.out.println(resolveStringValue);//你好:Windows 10,我今年30岁了
        this.resolver = resolver;
    }
}

通过${}获取系统环境变量,#{}则可以动态计算一些值

为什么实现Aware接口就可以获得相关spring底层组件?

还是因为Bean的后置处理器在起作用,比如ApplicationContextAwareProcessor,在这些bean的后置处理器中会回调容器组件bean中的setXxx(Xxx xxx)方法将相应的Spring组件注入

每个XxxAware都有对应的XxxAwareProcessor后置处理器,这些后置处理器会在创建完bean之后再为bean的属性赋值或者注入相应的组件

具体是怎么实现的请看ApplicationContextAwareProcessor的源码

在前置方法中会判断bean是否是XxxAware的子类,然后调用invokeAwareInterfaces(bean)方法,正是在该方法中注入的这些底层组件:,后置处理器的前置方法是整个Bean生命周期的开端在这里赋值比较合适

public Object postProcessBeforeInitialization(final Object bean, String beanName) throws BeansException {
    AccessControlContext acc = null;

    if (System.getSecurityManager() != null &&
            (bean instanceof EnvironmentAware || bean instanceof EmbeddedValueResolverAware ||
                    bean instanceof ResourceLoaderAware || bean instanceof ApplicationEventPublisherAware ||
                    bean instanceof MessageSourceAware || bean instanceof ApplicationContextAware)) {
        acc = this.applicationContext.getBeanFactory().getAccessControlContext();
    }

    if (acc != null) {
        AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
            invokeAwareInterfaces(bean);
            return null;
        }, acc);
    }
    else {
        invokeAwareInterfaces(bean);
    }

    return bean;
}

在该方法中会判断bean具体实现了哪些Aware接口,然后会回调该bean的setXxx(Xxx xxx)方法将xxx注入到bean中,这就是Aware注入的原理

private void invokeAwareInterfaces(Object bean) {
    if (bean instanceof Aware) {
        if (bean instanceof EnvironmentAware) {
            ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
        }
        if (bean instanceof EmbeddedValueResolverAware) {
            ((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver);
        }
        if (bean instanceof ResourceLoaderAware) {
            ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);
        }
        if (bean instanceof ApplicationEventPublisherAware) {
            ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);
        }
        if (bean instanceof MessageSourceAware) {
            ((MessageSourceAware) bean).setMessageSource(this.applicationContext);
        }
        if (bean instanceof ApplicationContextAware) {
            ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
        }
    }
}

@Profile

Profile:Spring提供的可以根据当前环境(开发、测试、生产),动态的激活和切换一系列的组件的功能,可以使用@Profile注解实现,比如数据源根据环境的切换

@Profile注解用于指定组件在哪个环境下会注册到IOC容器中,若不加该注解则在所有环境下都会注册到容器中:

没有加@Profile注解,则容器中会有三个数据源对象,这里使用了三种不同的方式为属性赋值(@Value或者SpringValueResolver都可以)

@PropertySource("classpath:/config.properties")
@Configuration
public class MyProfileConfig implements EmbeddedValueResolverAware {

    @Value("${db.user}")
    private String user;

    private String driverClass;

    @Bean
    public DataSource devDataSource(@Value("${db.password}") String password) throws PropertyVetoException {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setUser(user);
        dataSource.setPassword(password);
        dataSource.setDriverClass(driverClass);
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/dev");
        return dataSource;
    }

    @Bean
    public DataSource testDataSource(@Value("${db.password}") String password) throws PropertyVetoException {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setUser(user);
        dataSource.setPassword(password);
        dataSource.setDriverClass(driverClass);
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test");
        return dataSource;
    }

    @Bean
    public DataSource proDataSource(@Value("${db.password}") String password) throws PropertyVetoException {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setUser(user);
        dataSource.setPassword(password);
        dataSource.setDriverClass(driverClass);
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/pro");
        return dataSource;
    }
        //可以在此处给driverClass赋值说明此方法的调用时机在bean创建之前
    public void setEmbeddedValueResolver(StringValueResolver resolver) {
        this.driverClass = resolver.resolveStringValue("${db.driverClass}");
    }
}

加了环境标识(@Profile)的bean,只有在这个环境被激活时该bean才会被注册到IOC容器中


    @Bean
    @Profile("pro")
    public DataSource proDataSource(@Value("${db.password}") String password) throws PropertyVetoException {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setUser(user);
        dataSource.setPassword(password);
        dataSource.setDriverClass(driverClass);
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/pro");
        return dataSource;
    }

容器的默认环境是default,即在不设置容器的环境的情况下所有的@Profile(“default”)和不加@Profile注解的bean都会注册到容器中,那么怎么改变容器的环境呢?

1.使用命令行参数在Run Configuration的VM arguments中设置
-Dspring.profiles.active=xxx

2.使用代码的方式为容器设置启用环境:

public void test1() throws Exception {
    // 1、创建容器对象
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    // 2、设置容器的环境:此操作要在注册配置bean之前,可以设置多个环境
    context.getEnvironment().setActiveProfiles("dev", "test");
    // 3、注册配置bean
    context.register(MyProfileConfig.class);
    // 4、刷新容器
    context.refresh();
    context.close();
}

==注意此时在创建IOC容器的时候必须使用无参构造器创建==,因为有参构造器在创建对象时就会注册配置bean并刷新容器,此时即使再设置其容器环境也已经迟了,因为bean已经创建了

AnnotationConfigApplicationContext的有参构造

public AnnotationConfigApplicationContext(Class<?>... annotatedClasses) {
    this();
    register(annotatedClasses);
    refresh();
}

AnnotationConfigApplicationContext的有参构造

public AnnotationConfigApplicationContext() {
    this.reader = new AnnotatedBeanDefinitionReader(this);
    this.scanner = new ClassPathBeanDefinitionScanner(this);
}

也可以将@Profile注解标注在配置类上,此时该配置类只有在与IOC容器环境一致时才会被注册到IOC容器中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值