SpringBean注入注解讲解

在IT学习的过程中,很多的学习方向大致都是相似的。首先知道是什么,然后知道怎么用,再去知道为什么需要这样用,其次就是根据自己的理解对其进行总结;

学习Spring就是一个很好的例子,Spring框架上手整体是比较简单的,Spring即程序员的春天,那么Spring具体都做了哪些事情,分别是怎么去做到的,一起了解和学习一下;

1、 Spring中Bean的创建方式
2、 Spring中Bean创建时的属性
3、 Spring中Bean注入的方式

首先,我们创建一个Maven工程,然后我们在maven中添加Spring的依赖

<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.3.20.RELEASE</version>
</dependency>

我们在工程中创建一个Customer实体类,在后面的过程中,我们通过这个实体类来进行演示:

package com.example.bean;

import lombok.Data;

@Data
public class Customer {
    /**
     * 姓名
     */
    private String userName;
    /**
     * 年龄
     */
    private int age;

    public Customer() {
    }

    public Customer(String userName, int age) {
        this.userName = userName;
        this.age = age;
    }
}

我们先来看一下Spring中创建Bean的方式有哪些;

一:Spring中Bean的创建方式

1.1:Spring中基于XML的方式创建Bean

首先,我们在创建好的工程中创建配置文件mySpring.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="customer" class="com.example.bean.Customer" >
        <property name="userName" value="张楚岚"></property>
        <property name="age" value="21"></property>
    </bean>
</beans>

然后我们通过ClassPathXmlApplicationContext来读取配置文件进行Bean的创建

public class Main {
    /**
     * 测试xml的方式进行Bean的创建
     * @param args
     */
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("mySpring.xml");
        Customer customer = (Customer) context.getBean("customer");
        System.out.println(customer);
    }

}

输出结果如下: Customer(userName=张楚岚, age=21)

1.2 基于@Configuration和@Bean的形式创建Bean实体类

代码如下:

@Configuration
public class MyConfiguration {

    @Bean
    public Customer myCustomer(){
        return new Customer("冯宝宝", 120);
    }

}

我们创建一个测试类来看一下从Spring容器中获取对象

public class Main {
    /**
     * 测试通过 @Configuration 和 @Bean的形式注入Bean
     * @param args
     */
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConfiguration.class);
        Customer customer = (Customer) context.getBean("myCustomer");
        System.out.println(customer);
    }

}

输出结果如下:Customer(userName=冯宝宝, age=120)

扩展:Configuration 指定当前类为配置类,其效果类似与xml中的

1.3:基于@Configuration 和@Componet、@Service、@Controller、@Repository注解方式

编写代码如下:

package com.example.bean.componentScanh;

import com.example.bean.Customer;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
/**
 * 定义一个类,将这个类交给Spring管理
 * @Component @Controller @Service @Repository
 * 使用的效果是一样的,都会在扫描包时,将当前类交给Spring管理
 */
@Component
//@Controller
//@Service
//@Repository
public class MyConf {

    @Bean
    public Customer myCsutomer(){
        return new Customer("无根生", 150);
    }

}

然后我们再创建一个配置类

@Configuration
@ComponentScan(basePackages = "com.example.bean.componentScanh")
public class MyComponentScanh {

    @Bean
    public Customer customer(){
        return new Customer("张怀义", 149);
    }

}

接下来,我们编写一个测试类从Spring容器中获取

public class Main {
    /**
     * 测试配置文件中指定扫描包路径的方式创建Bean
     * @param args
     */
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyComponentScanh.class);
        Customer bean = (Customer) context.getBean("customer");
        Customer bean1 = (Customer) context.getBean("myCsutomer");
        System.out.print(bean);
        System.out.println(bean1);
    }

}

输出结果:Customer(userName=张怀义, age=149) Customer(userName=无根生, age=150)

1.4:基于@Configuration 和@Import注解

使用@Import注解可以将其它资源引入Spring,在应用中,有时没有把某个类注入到IOC容器中,但在运用的时候需要获取该类对应的bean,此时就需要用到@Import注解。比如我们不对Customer类做任何实例化操作,用@Import引入;

/**
 * Spring容器中没有Customer对象,Import引入
 */
@Configuration
@Import({Customer.class})
public class MyImport {

}
···
然后我们编写一个测试类看一下效果
```java
public class Main {
    /**
     * 测试@Import注解引入外部资源交由Spring管理
     * @param args
     */
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyImport.class);
        Customer customer1 = context.getBean(Customer.class);
        System.out.println(customer1);
        // TODO 通过@Import引入的类不能根据名称进行注入,只能根据类型注入
        Customer customer = (Customer) context.getBean("customer");
        System.out.println(customer);
    }

}

在上述代码中,通过名称获取实体类的时候会抛出一样;@Import引入的只能根据类型进行获取;输入如下:

Customer(userName=null, age=0)

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'customer' available

1.5基于@Configuration 加@Import 和ImportSelector 接口

ImportSelector 接口其主要作用是收集需要导入的配置类,如果该接口的实现类同时实现EnvironmentAwareBeanFactoryAwareBeanClassLoaderAware或者ResourceLoaderAware,那么在调用其selectImports方法之前先调用上述接口中对应的方法。

如果需要在所有的@Configuration处理完在导入时可以实现DeferredImportSelector接口。

打一个比方,我有一个配置文件,配置文件中有我的用户名和密码,但是我又不希望我的用户名和密码以明文的方式在配置文件中体现,这时候我想给配置文件加个密,这样别人看到的就都是密文了;

这时候可以通过哪些方式实现呢,相信有不少朋友都有听说过jasypt这个工具了;这个工具将配置文件中的值进行加密,然后采用特定的标识标志这些值是加密过后的,默认是采用ENC(密文)的这种形式;

这里我们不深究其实现原理,假设让我们自己设计一个配置文件加密,我们应该如何去做呢;

首先,我们先定义一个配置文件myApplication.properties,内容为:

#我是加密后的用户名
jdbcusername=!@!@#@$
#我是加密后的用户密码
jdbcpassword=!@#$%

然后我们通过配置类可以将配置文件进行导入

/**
 * 引入一个配置类MyConf
 * 同时加载配置文件 myApplication.properties
 */
@Configuration
@Import(MyConf.class)
@PropertySource("myApplication.properties")
public class MyConfiguration {

}

然后我们再定义一个类来实现ImportSelectorEnvironmentAware(其它的也可以)

public class MyConf implements ImportSelector,EnvironmentAware {

    private Environment environment;

    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        if (environment.containsProperty("jdbcusername") && environment.containsProperty("jdbcpassword")){
            System.out.println("加密前的用户名为: " + environment.getProperty("jdbcusername"));
            System.out.println("加密前的用户名密码为: " + environment.getProperty("jdbcpassword"));
            MutablePropertySources sources  = ((ConfigurableEnvironment)environment).getPropertySources();
            Properties p = new Properties();
            // 假设这里在做一系列AES、HS256等加密算法进行解密
            p.put("jdbcusername", "冯宝宝");
            p.put("jdbcpassword", "最大的秘密就是天师度啊~");
            sources.addFirst(new PropertiesPropertySource("defaultProperties", p));
        }
        return new String[0];
    }
}

上述代码中,我们通过拿到配置文件中的配置信息,然后动态的将其值替换掉;我们再编写一个测试类输出一下结果看一下:

public class Main {
    /**
     * 测试ImportSelector,EnvironmentAware类的使用案例
     * @param args
     */
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConfiguration.class);
        Environment environment = context.getEnvironment();
        String userName = environment.getProperty("jdbcusername");
        System.out.println("解密后的用户名为: " + userName);
        String password = environment.getProperty("jdbcpassword");
        System.out.println("解密后的用户名密码为:" + password);
    }

}

输出结果如下:

加密前的用户名为: !@!@#@$
加密前的用户名密码为: !@#$%
解密后的用户名为: 冯宝宝
解密后的用户名密码为:最大的秘密就是天师度啊~

1.6基于@Configuration 加@Import 和ImportBeanDefinitionRegistrar接口

ImportBeanDefinitionRegistrar能够实现动态注册bean到spring容器之中的效果;比如我们可以判断容器中是否存在某个对象,然后进行相对应的逻辑处理,动态的向容器中添加对象等;

我们知道,在Spring中,我们使用@Component @Controller @Service @Repository四个常见的业务注解时,当扫描包扫描带有这些注解的类时,这些类会被动态的加载到Spring的容器中;而通过ImportBeanDefinitionRegistrar这个接口,我们可以实现自定义注解,从而使Spring扫描时将扫描到带有我们自定义的注解的类也添加到Spring容器中进行管理;

我们先来试一下根据容器中的对象动态添加对象到Spring;

我们新建一个类 Address.class,不带任何修饰(也就是不交给Spring管理),然后我们新写一个类实现ImportBeanDefinitionRegistrar,然后把这个类Import到一个带有Configuration的类中;

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 假设存在 customer 对象,则再加载一个myAddress
        if (registry.containsBeanDefinition("customer")){
            registry.registerBeanDefinition("myAddress", new RootBeanDefinition(Address.class));
        }
    }

}

配置类

@Configuration
@Import({MyImportBeanDefinitionRegistrar.class})
public class MyConfiguration {

    @Bean
    public Customer customer(){
        return new Customer("无根生", 124);
    }

}

在编写一个测试类

public class Main {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConfiguration.class);
        String[] beanDefinitionNames = context.getBeanDefinitionNames();
        for (int i = 0; i < beanDefinitionNames.length; i++) {
            System.out.println(beanDefinitionNames[i]);
            if (beanDefinitionNames[i].equals("myAddress")){
                System.out.println("地址类:" + context.getBean("myAddress"));
            }
        }
    }

}

输入如下所示:

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalRequiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
myConfiguration
customer
myAddress
地址类:Address()

可以看到,我们在Spring容器中已经成功的拿到了一个名字并且获取到这个类成功打印了出来;

刚才还说了,我们可以自定义注解,然后让Spring去扫描我们带有指定注解的类,让BeanFactory创建出来这个Bean然后添加到Spring容器中;

1.7、实现FactoryBean接口 + @Component

直接上代码

package com.example.bean.factoryBean;

import com.example.bean.Customer;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.stereotype.Component;

@Component
public class MyFactoryBean implements FactoryBean {

    public Object getObject() throws Exception {
        return new Customer();
    }

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

    public boolean isSingleton() {
        return false;
    }
}

配置类MyConf

@Configuration
@ComponentScan("com.example.bean.factoryBean")
public class MyConf {

}

测试类如下:

public class Main {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConf.class);
        Object obj = context.getBean("myFactoryBean");
        if (obj instanceof MyFactoryBean){
            System.out.println((MyFactoryBean)obj);
        }
        if (obj instanceof Customer){
            System.out.println((Customer)obj);
        }
        System.out.println("----------------------------------");
        Object obj1 = context.getBean("&myFactoryBean");
        if (obj1 instanceof MyFactoryBean){
            System.out.println((MyFactoryBean)obj1);
        }
        if (obj1 instanceof Customer){
            System.out.println((Customer)obj1);
        }
    }

}

在测试类中,我们可以看到,我们通过名称来获取对象,程序运行输出结果会是怎样的呢;

输出结果:

Customer(userName=null, age=0)
----------------------------------
com.example.bean.factoryBean.MyFactoryBean@68c72235

可以看到,第一次输出的居然不是MyFactoryBean这个对象,而是在程序中创建出来的Customer对象,而当我们需要获取真正的MyFactoryBean对象是,需要在前面添加 & 符号;

在Spring中对于BeanFactoryFactoryBean而言,用一句话通俗易懂的讲就是:BeanFactory是负责创建Bean对象的,而FactoryBean就是由BeanFactory创建出来的Bean;

不管是BeanFactory还是FactoryBean,它们都是Bean;我们只需要记住FactoryBean是由BeanFactory创建出来的,就会容易理解很多了;

1.8、 @Conditional 和Condition接口创建Bean

@Conditional是Spring4新提供的注解,它的作用是按照一定的条件进行判断,满足条件给容器注册bean。 我们可以接收配置文件中的配置信息,当配置信息满足什么条件时才做什么处理;

我们先定义一个配置文件myApplication.properties,在里面添加键值对信息

# 安全开关是否开启
security.isOpen=true

接下来我们对写一个类实现Condition,在类中对配置文件中的值进行判断,满足条件做对应的处理;代码如下:

/**
 * 类 名: MyCondition1
 * 描 述: 当满足什么条件时才给Spring容器注册Bean
 */
public class MyCondition1 implements Condition {
    /**
     * 当配置文件中包含有配置文件security.isOpen并且配置文件的值为true时才向Spring中注入Bean
     */
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment environment = context.getEnvironment();
        if (environment.containsProperty("security.isOpen") && environment.getProperty("security.isOpen").equals("true")){
            return true;
        }
        return false;
    }
}

/**
 * 类 名: MyCondition1
 * 描 述: 当不满足什么条件时才给Spring容器注册Bean
 */
public class MyCondition2 implements Condition {

    /**
     * 当配置文件中包含有配置文件security.isOpen并且配置文件的值不为true时才向Spring中注入Bean
     */
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment environment = context.getEnvironment();
        if (environment.containsProperty("security.isOpen") && environment.getProperty("security.isOpen").equals("true")){
            return false;
        }
        return true;
    }
}

然后我们编辑一个配置类,在配置类中进行类的注入操作;

@Configuration
@PropertySource("myApplication.properties")
public class MyConf {
    /**
     * 当配置文件中的指定值为真时注册
     * @return
     */
    @Bean
    @Conditional(MyCondition1.class)
    public Customer customer1(){
        return new Customer("张楚岚", 21);
    }

    /**
     * 当配置文件中的指定值不为真时注册
     * @return
     */
    @Bean
    @Conditional(MyCondition2.class)
    public Customer customer2(){
        return new Customer("冯宝宝", 18);
    }
}

根据上面的结论,在当前配置文件中,当配置文件的值为true时,Spring容器中应该是有一个名字为customer1的Bean,如果不为true,则Spring容器中应该有一个名字为customer2的Bean;

我们编写一个测试类看一下运行效果:

public class Main {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConf.class);
        String[] names = context.getBeanDefinitionNames();
        for (int i = 0; i < names.length; i++) {
            System.out.println(names[i]);
        }
    }
}

输出结果如下:

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalRequiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
myConf
customer1

可见,此时配置文件为true,customer1被成功加载到Spring容器中了;

二:Spring中Bean创建时的属性

2.1、@Lazy 懒加载

在Spring创建Bean时默认采用的是立即加载;简单一点理解就是在项目启动的时候就将所有的需要被管理的Bean都创建好放入到容器中集中管理;它的好处在于利于排查bug,项目启动时将Bean创建,如果有问题会立即反馈给用户,不足就是在项目启动时需要加载所有的Bean,相对会占用内存很多并且启动速度也会慢很多;

而懒加载则相反,懒加载是当需要使用这个Bean的时候才会去创建它。它的好处就是节省内存,并且启动速度快,不足之处在于不利于排查bug,很多问题需要在使用的时候才会发现;

我们来简单了解一下这个懒加载;我们修改一下前面的Customer类用例;添加有参构造和无参构造的输出;

public Customer() {
    System.out.println("Customer无参构造被触发了。。。。");
}

public Customer(String userName, int age) {
    System.out.println("Customer有参构造被触发了。。。。");
    this.userName = userName;
    this.age = age;
}

然后我们编写一个配置类,如下:

@Configuration
public class MyConf {

    @Bean
    public Customer customer1(){
        // 无参构造
        return new Customer();
    }

    @Lazy
    @Bean
    public Customer customer2(){
        // 有参构造
        return new Customer("张楚岚", 21);
    }
}

在上面配置类中,我们创建了两个Bean,分别是customer1 和 customer2,对customer2我这边做的是懒加载,采用的是有参构造;我们再编写一个测试类看一下程序的走向;

@Configuration
public class MyConf {

    @Bean
    public Customer customer1(){
        // 无参构造
        return new Customer();
    }

    @Lazy
    @Bean
    public Customer customer2(){
        // 有参构造
        return new Customer("张楚岚", 21);
    }
}

程序输出结果:

Customer无参构造被触发了。。。。
测试懒加载...........Lazy.........sta01
测试懒加载...........Lazy.........sta02

Customer有参构造被触发了。。。。
可以看得出来,在Spring初始化的时候就调用的无参构造,也就是customer1的立即加载,而当最后从容器中获取Bean的时候,才输出了有参构造的输出,说明懒加载的在使用的时候才去实例化的

2.2、socope Bean的作用域及在IOC中的存在周期

Bean的作用域范围可划分为5个级别,分别是:

2.2.1:singleton :单例Bean,容器启动就产生、容器销毁就销毁,生命周期最长的一种bean。

2.2.2:global session :web容器启动就产生、web容器销毁就销毁,MVC中的Bean是这种;

2.2.3:session :会话bean,会话开始产生、会话结束销毁。

2.2.4:prototype : 瞬时bean:用完就销毁,也就是多例Bean

SpringBean注入注解讲解

我们用代码演示一下单例Bean和多例Bean

public class Main {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConf.class);
        System.out.println(context.getBean("customer1") == context.getBean("customer1"));
        System.out.println(context.getBean("customer2") == context.getBean("customer2"));
    }
}

输出结果: true false

可以看得出来,customer1是单例的,两次获取的是同一个对象,而customer2是多例的,两次获取的是不同的对象;

三:Spring中对象的注入方式

3.1:XML中进行对象的注入(此处不做演示)

3.2:@Autowired注解

@Autowired注解是Spring原声注解(常见理解是根据类型自动注入),支持required=false,当bean为空的时候也不会报错。

由于根据类型注入的时候在多态的时候,同一个类型会有多个实现类,此时可以根据注入的属性的属性名进行名称注入;比如有一个接口CustomerService,有2个实现类,分别是CustomerServiceImpl01和CustomerServiceImpl02;

此时我可以指定注入的属性的名称,Spring会自动根据名称进行注入;如:

/**
 * CustomerService 会有2个
 * 此时自动根据名称customerServiceImpl01进行注入
 * 默认类名首字母小写,如果有自定义名称的话就是自定义的那个名称
 */
@Autowired
private CustomerService customerServiceImpl01;
/**
 * CustomerService 会有2个
 * 此时自动根据名称customerServiceImpl02进行注入
 */
@Autowired
private CustomerService customerServiceImpl02;

同时,@Autowired还可以和@Qualifier(“beanName”)注解一起组合使用实现根据名称注入的效果;

3.3:@Resource注解

@Resource注解是java原声的,是根据名称进行注入。它是由JSR-250规范定义的注解。如果没有指定name属性,当注解写在字段上时,默认取字段名进行安装名称查找,如果注解写在setter方法上默认取属性名进行装配。

3.4:@Inject 注解

第三方插件进行注入,@Inject是默认按照类型匹配的;目前还没有接触过这一类的注解,暂不做谈论;

上一篇:SpringBean加载过程中,循环依赖的问题(一)


下一篇:java中使用二进制进行权限控制