spring之IOC、DI

1、概述

在传统的开发过程中,创建对象(单例多例对象)、初始化属性等操作都是由我们自己来进行创建。

但是这种使用存在着耦合现象,也就是强依赖关系,所以看看spring中给我们提供的更好的方式来进行解决。

2、入门程序

public interface UserDao {
    void save();
}

对应的实现类:

public class UserDaoImpl implements UserDao {

    public UserDaoImpl() {
        System.out.println("默认使用的是无参构造方法使用创建对象");
    }

    @Override
    public void save() {
        System.out.println("i can save ............");
    }
}

配置对应的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">


    <!--
    创建这个类的对象,map中的key为userDao
    将创建这个类的控制权交给spring框架,让spring框架来创建对应的对象之后,然后来对其进行管理
    我们只需要在使用的时候获取得到这个对象即可。
    那么在这里需要关注的是:
            1、什么时候创建的?            我们自定义化操作时候是在Tomcat加载实际创建的,那么这里又是如何来进行创建的?
            2、如何来对属性进行初始化?      如何来对属性进行初始化操作(如果是我们自己来操作,是赋予了默认值)
            3、单例bean和多例bean的管理?   之前在web框架中反复提到过的事情
                    scope="singleton" 表示的是默认创建单例bean,单例bean将会由spring全权管理,包括创建和销毁;
                    scope="prototype" 表示的是每次获取的时候都会创建一个新的bean出来,但是spring只是来帮助我们创建,不负责管理,销毁交给JVM来进行管理

                    
    -->
    <bean id="userDao" class="com.guang.user.dao.impl.UserDaoImpl" scope="singleton"/>
    <!--<bean id="userDao" class="com.guang.user.dao.impl.UserDaoImpl" scope="prototype"/>-->
</beans>

编写测试类:

public class TestUserDao {
    @Test
    public void testGetUserDao(){
        ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        // 因为这里也不知道我们要来存储什么样的数据类型,所以通用使用Object来进行存储而已,在需要获取得到的时候转换,强制类型转换也是可以的
        // UserDao userDao = classPathXmlApplicationContext.getBean("userDao", UserDao.class);
        // userDao.save();
    }
}

1、bean的声明周期

通过对xml文件中对于scope属性的赋值,可以知道对于单例的来说,在创建对象的时候,首先会使用默认的无参构造方法来进行创建对象

对于无参构造方法来说,这种是最常用的。因为框架不可能知道对于我们开发人员来说,我们会对构造函数中的变量赋值的变量类型是何种类型,所以使用这种原始的方式来创建对象是最合适的。

对于scope属性来说,取值有五种,如下所示:

取值 说明
singleton 默认,表示单例的,一个Spring容器里,只有一个该bean对象
prototype 多例的,一个Spring容器里,有多个该bean对象
request web项目里,Spring创建的bean对象将放到request域中:一次请求期间有效
session web项目里,Spring创建的bean对象将放到session域中:一次会话期间有效
globalSession web项目里,应用在Portlet环境/集群环境;如果没有Portlet/集群环境,那么globalSession相当于session(新版本中已删除)

但是在我们最常见的使用过程中,无非是两种。singleton和prototype

那么我们来进行测试一下,bean的声明周期:

如果是singleton,那么对应的bean的声明周期,其创建的控制权交付给框架创建,然后放入到容器中来给容器来做处理;在容器销毁的时候将会销毁容器中的bean;

如果是prototype,其创建的控制权交付给框架创建,但是其后续的声明周期不会再由spring框架来进行管理,而是JVM来进行管理。

最直接的一点观察就是:

对于单实例对象来说,其创建时机在于容器加载阶段将bean进行初始化;

而对于多实例对象来说,其创建时机在于每次获取得到bean对象的时候进行创建;

对应的demo测试如上所示:

如果是单实例对象,那么在容器进行加载的时候,控制台会显示出来对应bean中无参构造中的输出方法;

对于多实例对象来说,在容器进行加载的时候不会立刻加载对应的bean到对象中来,只会在首次获取得到的时候才会在控制台显示出无参构造函数中的输出信息:

默认使用的是无参构造方法使用创建对象

2、初始化和销毁方法

在bean在进行创建的时候,会对其中的属性来进行初始化;在bean销毁的时候,会执行对应的销毁方法。而这一步,都可以在bean标签中来进行设置一下。

在bean中创建出对应的初始化和销毁方法:

public class UserDaoImpl implements UserDao {

    public UserDaoImpl() {
        System.out.println("默认使用的是无参构造方法使用创建对象");
    }


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

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


    @Override
    public void save() {
        System.out.println("i can save ............");
    }
}

然后在对应的xml中的bean标签中标注一下,使用的方法:

<bean id="userDao" class="com.guang.user.dao.impl.UserDaoImpl" scope="prototype" init-method="init" destroy-method="destroy"/>

这个时候对单例和多例对象也是有影响的。对于单例对象来说,在创建的时候会执行对应的构造方法和初始化方法,在容器销毁的时候会执行对应的销毁方法;对于多例对象来说,在容器进行加载的时候不会执行构造方法和初始化方法,在容器销毁的时候也不会执行销毁方法。

对应的测试代码如下所示:

public class TestUserDao {    @Test    public void testGetUserDao(){        ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");        classPathXmlApplicationContext.close();    }}

其实对于初始化和销毁方法还有另外几种方式。后面来进行补充

3、实例化bean的几种方式

  • 无参构造方法实例化,默认的:让Spring调用bean的无参构造,生成bean实例对象给我们
  • 工厂静态方法实例化:让Spring调用一个工厂类的静态方法,得到一个bean实例对象
  • 工厂非静态方法实例化(实例化方法):让Spring调用一个工厂对象的非静态方法,得到一个bean实例对象

第一种方式无须多说,这种是最常见的一种方式。上面的代码中也对这里做了详细说明。

下面看一下第二种和第三种方式

  • 工厂静态方法实例化:
public class StaticFactory{    public static UserDao createUserDao(){        return new UserDaoImpl();    }}

对应的xml文件配置:

<bean id="userDao" class="com.guang.factory.StaticFactory" factory-method="createUserDao"></bean>
  • 工厂非静态方法实例化:
public class InstanceFactory{    public UserDao createUserDao(){        return new UserDaoImpl();    }}

对应的xml文件:

<!-- 先配置工厂 -->
<bean id="instanceFactory" class="com.guang.factory.InstanceFactory"></bean>

<!-- 再配置UserDao -->
<bean id="userDao" factory-bean="instanceFactory" factory-method="createUserDao"></bean>

3、依赖注入

上面只是解释了创建bean(几种方式)、bean的生命周期(单例多例)以及对应的初始化和销毁方法。

但是无法避免的是如何去解决类和类的依赖问题,类和类之间的依赖关系spring的解决方式是通过DI的方式来进行解决的。

来看看对应的代码实现:

service对应的接口:

public interface UserService {
    void save();
}

service接口对应的实现类:

public class UserServiceImpl implements UserService {

    private UserDao userDao;

/*    //提供userDao的get/set方法
    public UserDao getUserDao() {
        return userDao;
    }*/

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }


    @Override
    public void save() {
        System.out.println("UserServiceImpl-------------save方法");
        userDao.save();
        System.out.println(this.userDao);
    }
}

查看对应的dao接口:

public interface UserDao {
    void save();
}

dao接口对应的实现类:

public class UserDaoImpl implements UserDao {
    @Override
    public void save() {
        System.out.println("UserDaoImpl--------------save方法");
    }
}

对应的xml配置信息:

    <!--配置bean实例到对象中来-->
    <bean id="userDao" class="com.guang.spring.di.dao.impl.UserDaoImpl"/>

    <bean id="userService" class="com.guang.spring.di.service.impl.UserServiceImpl">
        <property name="userDao" ref="userDao"/>
    </bean>

对应的测试信息:

    @Test
    public void testSpringDI(){
        ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("classpath:application.xml");
        UserService userService = classPathXmlApplicationContext.getBean("userService", UserService.class);
        userService.save();
        classPathXmlApplicationContext.close();
    }

可以看到在xml文件中配置的信息,通过ref来引用对应的对象的时候,还需要在对应的被引入的类中创建出被引用对象的setXXX方法。

说明了在进行依赖注入的时候,这里spring进行DI进行注入的时候,采用的是使用set方法来给成员属性来进行注入的。

(这里就需要注意这里注入的原理了)

1、三种常见的注入方式

  • set方法注入
  • 构造方法注入(这种是spring后期所推荐使用的一种方式)
  • p名称空间注入

1、set方法注入

在类中提供需要注入的成员(依赖项)的set方法,在配置文件中注入属性的值

<bean id="" class="">
	<property name="属性名" value="属性值"></property>
    <property name="属性名" ref="bean的id"></property>
</bean>
  • property标签:用在bean标签内部,表示要给某一属性注入数据
    • name:属性名称
    • value:要注入的属性值,注入简单类型值
    • ref:要注入的属性值,注入其它bean对象

优势:创建bean对象时没有明确的限制,可以使用无参构造直接创建

缺点:如果某个成员必须有值,则获取对象时,有可能set方法未执行

这里的缺点就是需要被注入的对象中有属性没有进行赋值,如果是引用类型,那么将会导致这个后期的成员变量的引用成为空指针。

2、构造方法注入

在类中提供构造方法,构造方法的每个参数就是一个依赖项,通过构造方法给依赖项注入值。

<bean id="" class="">	<constructor-arg name="构造参数名称" value="构造参数的值"></constructor-arg>    <constructor-arg name="构造参数名称" ref="bean的id"></constructor-arg></bean>
  • name:构造参数的名称
  • type:构造参数的类型
  • index:构造参数的索引
  • value:要注入的值,注入简单类型值
  • ref:要注入的值,注入其它bean对象

优势:在获取bean对象时,注入数据是必须的操作,否则无法创建成功。

缺点:改变了bean对象的实例化方式,如果在创建对象时用不到这些数据,也必须要提供

但是在这里可以看到,优势是大于弊端的,所以使用这种构造注入的方式来进行结合是比较合适使用的。

3、p标签注入

p名称空间注入,本质仍然是set方法注入

在xml中引入p名称空间的约束

然后通过p:属性名称=""来注入简单数据、使用p:属性名称-ref=""注入其它bean对象,它的本质仍然是set方法注入

<beans xmlns="http://www.springframework.org/schema/beans"    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xmlns:p="http://www.springframework.org/schema/p"    xsi:schemaLocation="http://www.springframework.org/schema/beans    http://www.springframework.org/schema/beans/spring-beans.xsd">        <bean id="" class="" p:属性名="简单值" p:属性名-ref="bean的id"></bean>    </beans>

但是这种使用p标签来使用的是最少的。

4、推荐使用

构造方法注入

private final Xxx xxx;public Yyy(XXX xxx){    this.xxx = xxx;}

4、注解版使用

注解版是我们最常使用的一种方式,这种方式省去了我们来编写大量xml配置的方式,可以简化我们的开发效率。

使用注解版+配置版进行开发,这种事传统的SSM开发的主要流程方式之一

1、demo

还是按照上面的方式来进行描述开发:

dao接口开发:

public interface UserDao {    void save();}

dao接口对应的实现类:

@Repository
public class UserDaoImpl implements UserDao {

    @Override
    public void save() {
        System.out.println("UserDaoImpl-----------save..........");
    }

}

service接口开发:

public interface UserService {
    void save();
}

service接口对应的实现类:

@AllArgsConstructor
@Service
public class UserServiceImpl implements UserService {

    // 在有参构造上面可以省略对应的@Autowired注解
    // @Autowired  直接从容器中来给该属性进行DI赋值操作
    private final UserDao userDao;
    
    @Override
    public void save() {
        System.out.println("UserServiceImpl------save............");
        Objects.requireNonNull(userDao,"userDao为空,无法进行下面的调用");
        userDao.save();
    }
}

最重要的一点是:spring如何得到将哪些class字节码文件加载到内存中来,使其成为容器中的组件?

那么这个时候需要我们来指定class文件所在的文件夹目录:

<context:component-scan base-package="com.guang.user"/>

然后编写测试类来进行测试:

    @Test
    public void save(){
        ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext("classpath:application.xml");
        UserService userService = (UserService) app.getBean("userServiceImpl");
        userService.save();
        app.close();
    }

2、成为bean注解介绍

注解 说明
@Component 用在类上,相当于bean标签
@Controller 用在web层类上,配置一个bean(是@Component的衍生注解)
@Service 用在service层类上,配置一个bean(是@Component的衍生注解)
@Repository 用在dao层类上,配置一个bean(是@Component的衍生注解)
  • @Component:类级别的一个注解,用于声明一个bean,使用不多
    • value属性:bean的唯一标识。如果不配置,默认以首字母小写的类名为id
  • @Controller, @Service, @Repository,作用和@Component完全一样,但更加的语义化,使用更多
    • @Controller:用于web层的bean
    • @Service:用于Service层的bean
    • @Repository:用于dao层的bean

如果没有明确的表示在哪一层,那么可以使用@Component注解。

1、配置bean注解
注解 说明
@Scope 相当于bean标签的scope属性
@PostConstruct 相当于bean标签的init-method属性
@PreDestroy 相当于bean标签的destory-method属性
2、配置bean的作用范围:
  • @Scope:配置bean的作用范围,相当于bean标签的scope属性。加在bean对象上

  • @Scope的常用值有:

  • singleton:单例的,容器中只有一个该bean对象

    • 何时创建:容器初始化时
    • 何时销毁:容器关闭时
  • prototype:多例的,每次获取该bean时,都会创建一个bean对象

    • 何时创建:获取bean对象时
    • 何时销毁:长时间不使用,垃圾回收
@Scope("prototype")
@Service("userService")
public class UserServiceImpl implements UserService{
    //...
}
3、配置bean生命周期方法
  • @PostConstruct是方法级别的注解,用于指定bean的初始化方法
  • @PreDestroy是方法级别的注解,用于指定bean的销毁方法
@Service("userService")
public class UserServiceImpl implements UserService {

    @PostConstruct
    public void init(){
        System.out.println("UserServiceImpl对象已经创建了......");
    }
    
    @PreDestroy
    public void destroy(){
        System.out.println("UserServiceImpl对象将要销毁了......");
    }
 
    //......
}
4、依赖注入的注解
注解 说明
@Autowired 相当于property标签的ref
@Qualifier 结合@Autowired使用,用于根据名称注入依赖
@Resource 相当于@Autowired + @Qualifier
@Value 相当于property标签的value
5、注入bean对象
  • @Autowired:用于byType注入bean对象,按照依赖的类型,从Spring容器中查找要注入的bean对象
    • 如果找到一个,直接注入
    • 如果找到多个,则以变量名为id,查找bean对象并注入
    • 如果找不到,抛异常
  • @Qualifier:是按id注入,但需要和@Autowired配合使用。
  • @Resource:(是jdk提供的)用于注入bean对象(byName注入),相当于@Autowired + @Qualifier

绝大多数情况下,只要使用@Autowired注解注入即可

使用注解注入时,不需要set方法了

6、注入普通值
  • @Value:注入简单类型值,例如:基本数据类型和String
@Service("userService")public class UserServiceImpl implements UserService{        @Value("zhangsan")//直接注入字符串值    private String name;        //从properties文件中找到key的值,注入进来    //注意:必须在applicationContext.xml中加载了properties文件才可以使用    @Value("${properties中的key}")    private String abc;        //...}

5、纯注解开发

这是主流的开发方式,和上面的注解开发还不一样,这里是完全使用注解来进行开发。

但是在SSM阶段,这种方式还是不可取的,而到了springboot开发阶段,这种使用纯注解版的才可以完全发挥出来其威力。

1、几种注解

注解 说明
@Configuration 被此注解标记的类,是配置类
@ComponentScan 用在配置类上,开启注解扫描。使用basePackage属性指定扫描的包
@PropertySource 用在配置类上,加载properties文件。使用value属性指定properties文件路径
@Import 用在配置类上,引入子配置类。用value属性指定子配置类的Class
@Bean 用在配置类的方法上,把返回值声明为一个bean。用name/value属性指定bean的id

使用@Configuration注解标注的类来代替原来的xml配置文件,使之成为核心配置类,@Configuration是@Component的衍生注解;

使用@ComponentScan注解(和xml对比的话),这个应该在配置类上;

使用@PropertiesSource注解,加载properties文件中的属性到value属性上;

使用@Import注解,加载其他的配置类到当前配置文件中;类似xml中引入另外一个xml配置文件;

使用@Bean注解,和在xml文件使用标签是一样的原理;

2、配置类上的注解

  • @Configuration

  • @ComponentScan配置组件注解扫描

    • basePackages或者value属性:指定扫描的基本包
  • @PropertySource用于加载properties文件

    • value属性:指定propertis文件的路径,从类加载路径里加载
  • @Import用于导入其它配置类

    • Spring允许提供多个配置类(模块化配置),在核心配置类里加载其它配置类
    • 相当于xml中的<import resource="模块化xml文件路径"/>标签
@Configuration
@ComponentScan(basePackages="com.guang")
@PropertySource("classpath:db.properties")
@Import(JdbcConfig.class)
public class AppConfig{
    
}
  • 模块配置类:JdbcConfig。

被加载的模块配置类,可以不用再添加@Configuration注解

3、声明bean

  • @Bean注解:方法级别的注解
    • 用于把方法返回值声明成为一个bean,作用相当于<bean>标签
    • 可以用在任意bean对象的方法中,但是通常用在@Configuration标记的核心配置类中
  • @Bean注解的属性:
    • value属性:bean的id。如果不设置,那么方法名就是bean的id

实例如下:

@Configuration
public class AppConfig {
    @Bean
    public UserService myService() {
        return new UserServiceImpl();
    }
}

1、bean的依赖注入

  • @Bean注解的方法可以有任意参数,这些参数即是bean所需要的依赖,默认采用byType方式注入
  • 可以在方法参数上增加注解@Qualifier,用于byName注入

实例如下所示:

@Configurationpublic class AppConfig {    @Bean("myService")    public UserService myService(UserDao userDao) {        //通过set方法把myDao设置给MyService        UserService myService = new UserServiceImpl();        myService.setMyDao(userDao);        return myServie;    }        /**     * 方法参数上使用@Qualifer注解,用于byName注入     * 		@Qualifer:可以独立用在方法参数上用于byName注入     */    @Bean("myService2")    public UserService myService2(@Qualifier("myDao2") UserDao userDao){        return new UserServiceImpl(userDao);    }}

4、小结

  • 配置类上要加注解@Configuration
  • 要开启组件扫描,在配置类上@ComponentScan("com.guang")
  • 如果要把jar包里的类注册成bean:
    • 在配置类里加方法,方法上加@Bean,会把方法返回值注册bean对象
@Configuration@ComponentScan("com.guang")public class AppConfig{        @Bean(value="person", initMethod="init", destroyMethod="destroy")    @Scope("prototype")    public Person p(){        Person p = new Person();        p.setName("jack");        p.setAge(20);        return p;    }        //byType注入依赖    @Bean    public QueryRunner runner(DataSource dataSource){        return new QueryRunner(dataSource);    }        //byName注入依赖:注入名称为ds的bean对象    @Bean    public QueryRunner runner1(@Qualifier("ds") DataSource dataSource){        return new QueryRunner(dataSource);    }    }
  • 如果要引入外部的properties文件,在配置类上加@PropertySource("classpath:xxx.properties")
@Configuration@PropertySource("classpath:jdbc.properties") //引入properties文件public class AppConfig{        //把properties里的数据注入给依赖项(成员变量)    @Value("${jdbc.driver}")    private String driver;        @Bean    public DataSource ds(){        ComboPooledDataSource ds = new ComboPooledDataSource();        //直接使用成员变量,它的值已经注入了        ds.setDrvierClass(drvier);        ....        return ds;    }}
  • 引入模块配置类,在配置类上使用@Import(子配置类.class)
@Configuration@PropertySource("classpath:jdbc.properties") //引入properties文件@Import(ServiceConfig.class)public class AppConfig{}//子配置类public class ServiceConfig{    //可以引入properties里的值(只要这个properties被引入过)	@Value("${jdbc.driver}")    private String driver;}

6、扩展

准备环境

1. 创建Module,引入依赖

<dependencies>
    	<!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
    	<!-- Spring整合Junit -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
    	<!-- Junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    	<!-- snakeyaml,用于解析yaml文件的工具包 -->
        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
            <version>1.25</version>
        </dependency>

		<!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
    	<!-- c3p0连接池 -->
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>
    </dependencies>

2. 创建核心配置类

  • com.guang包里创建核心配置类AppConfig
@Configuration@ComponentScan("com.guang")public class AppConfig {    }

3. 创建单元测试类

  • src\main\testcom.guang.test包里创建单元测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class SpringAnnotationTest {

    /**
     * 把IoC容器注入进来,在测试方法里要使用
     */
    @Autowired
    private ApplicationContext app;

}

@ComponentScan

BeanName生成策略

说明
  • 默认的BeanName生成策略:

    • 如果注册bean时指定了id/name,以配置的id/name作为bean的名称
    • 如果没有指定id/name,则以类名首字母小字作为bean的名称
  • 在模块化开发中,多个模块共同组成一个工程。

    • 可能多个模块中,有同名称的类,按照默认的BeanName生成策略,会导致名称冲突。
    • 这个时候可以自定义beanname生成策略解决问题
  • @ComponentScannameGenerator,可以配置自定义的BeanName生成策略,步骤:

    1. 创建Java类,实现BeanNameGenerator接口,定义BeanName生成策略

    2. 在注解@ComponentScan中,使用nameGenerator指定生成策略即可

示例
  1. com.guang.beans里创建一个Java类:Course如下
//定义bean的名称为c@Component("c")public class Course {    private String courseName;    private String teacher;	//get/set...    //toString}
  1. com.guang.beanname里创建Java类,实现BeanNameGenerator接口,定义BeanName生成策略
public class MyBeanNameGenerator implements BeanNameGenerator {    /**     * 生成bean名称。当Spring容器扫描到类之后,会调用这个方法,获取bean的名称     *     * @param definition bean的定义信息     * @param registry bean的注册器,用于管理容器里的bean,可以:     *                 注册新的bean;查询某个bean;删除bean 等等     * @return bean的名称     */    @Override    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {        //获取bean的全限定类名        String beanClassName = definition.getBeanClassName();        //获取bean的类名(去掉包名,只要类名)        String shortName = ClassUtils.getShortName(beanClassName);        //把bean命名为:my+类名        return "my" + shortName;    }}
  1. 在注解@ComponentScan中,使用nameGenerator指定生成策略
@Configuration@ComponentScan(        basePackages = "com.guang",        nameGenerator = MyBeanNameGenerator.class)public class AppConfig {}
  1. 编写测试类,测试获取bean
    /**     *  Bean命名规则     *      如果定义了bean的名称,以定义的名称为准     *      如果没有定义bean的名称,默认情况下:     *          类名首字母小写作为bean的名称(id)。比如:Course,名称为course     *          如果类名前2位都是大写,则仍然使用原类名。比如:URL,名称仍然是URL     *     * 自定义BeanName命名策略:     *      1. 创建类,实现BeanNameGenerator,自定义名称生成策略     *      2. 在核心配置类的@ComponentScan里,使用nameGenerator指定生成策略名称     *     * 如果使用了自定义的命名策略,则bean的原本的名称将会失效     */    @Test    public void testBeanName(){        //获取Course类型的bean的名称        String[] names = app.getBeanNamesForType(Course.class);        for (String name : names) {            System.out.println(name);        }    }

扫描规则过滤器

说明
  • @ComponentScan默认的扫描规则:
    • 扫描指定包里的@Component及衍生注解(@Controller,@Service,@Repository)配置的bean
  • @ComponentScan注解也可以自定义扫描规则,来包含或排除指定的bean。步骤:
    1. 创建Java类,实现TypeFilter接口,重写match方法
      • 方法返回boolean。true表示匹配过滤规则;false表示不匹配过滤规则
    2. 使用@ComponentScan注解的属性,配置过滤规则:
      • includeFilter:用于包含指定TypeFilter过滤的类,符合过滤规则的类将被扫描
      • excludeFilter:用于排除指定TypeFilter过滤的类,符合过滤规则的类将被排除
示例1-根据注解过滤
@Configuration@ComponentScan(        basePackages = "com.guang",        nameGenerator = MyBeanNameGenerator.class,        //使用Component注解标注的类不扫描        excludeFilters = @ComponentScan.Filter(            	//过滤类型:根据注解进行过滤                type = FilterType.ANNOTATION,            	//要过滤的注解是:Component                classes = Component.class        ))public class AppConfig {}
示例2-根据指定类过滤
@Configuration@ComponentScan(        basePackages = "com.guang",        nameGenerator = MyBeanNameGenerator.class,        //Course及其子类、实现类不扫描        excludeFilters = @ComponentScan.Filter(                type = FilterType.ASSIGNABLE_TYPE,                classes = Course.class        ))public class AppConfig {}
示例3-自定义过滤
  1. 创建Java类
//类上有@Component@Componentpublic class BeanTest {	//类里有单元测试方法    @Test    public void test1(){        System.out.println("---------");    }}
  1. 编写过滤器,实现TypeFilter接口,重写match方法
public class TestFilter implements TypeFilter {    /**     * 如果被扫描的类是单元测试类(类里有单元测试方法),则排除掉不要     *     * @param metadataReader 被扫描的类的读取器,可以读取类的字节码信息、类的注解信息     * @param metadataReaderFactory 读取器的工厂对象     * @return 是否匹配。如果被扫描的类是单元测试类,则匹配,     */    @Override    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {        //获取被扫描的类上的注解信息        AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();        //获取被扫描的类里,使用@Test注解标记的方法        Set<MethodMetadata> annotatedMethods = annotationMetadata.getAnnotatedMethods(Test.class.getName());        //如果有被@Test标记的方法,返回true;否则,返回false        return annotatedMethods!=null && annotatedMethods.size()>0;    }}
  1. 使用注解@ComponentScan,配置过滤规则
@Configuration@ComponentScan(        basePackages = "com.guang",        nameGenerator = MyBeanNameGenerator.class,        //单元测试类不扫描(使用自定义的过滤器,排除掉 过滤出来的单元测试类)        excludeFilters = @ComponentScan.Filter(                type = FilterType.CUSTOM,                classes = TestFilter.class        ))public class AppConfig {}
  1. 测试
    @Test    public void testScan(){        //获取不到BeanTest,因为这个类里有单元测试方法,被我们自定义的过滤器给排除掉了        BeanTest beanTest = app.getBean(BeanTest.class);        System.out.println(beanTest);    }

@PropertySource

yml配置文件介绍

  • 大家以前学习过的常用配置文件有xmlproperties两种格式,但是这两种都有一些不足:
    • properties
      • 优点:键值对的格式,简单易读
      • 缺点:不方便表示复杂的层级
    • xml
      • 优点:层次结构清晰
      • 缺点:配置和解析语法复杂
  • springboot采用了一种新的配置文件:yaml(或yml),它综合了xmlproperties的优点。
    • yaml are't markup language => yaml
    • 使用空格表示层次关系:相同空格的配置项属于同一级
    • 配置格式是key:空格value,键值对之间用:空格表示
  • yaml文件示例:
jdbc:
	driver: com.mysql.jdbc.Driver # 注意:英文冒号后边必须有一个空格
	url: jdbc:mysql:///spring
	username: root
	password: root
	
jedis:
	host: localhost
	port: 6379

使用@PropertySource加载yml

说明
  • @PropertySource可以使用factory属性,配置PropertySourceFactory,用于自定义配置文件的解析
  • 步骤:
    1. 创建yaml文件:application.yml
    2. 导入依赖snakeyaml,它提供了解析yml文件的功能
    3. 创建Java类,实现PropertySourceFactory接口,重写createPropertySource方法
    4. 使用@PropertySource注解,配置工厂类
示例
  1. resources目录里创建yaml文件:application.yml
jdbc:
	driver: com.mysql.jdbc.Driver # 注意:英文冒号后边必须有一个空格
	url: jdbc:mysql:///spring
	username: root
	password: root
	
jedis:
	host: localhost
	port: 6379
  1. pom.xml增加导入依赖snakeyaml,它提供了解析yml文件的功能
<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.25</version>
</dependency>
  1. 创建Java类,实现PropertySourceFactory接口,重写createPropertySource方法
public class YamlSourceFactory implements PropertySourceFactory {
    /**
     * 解析yaml配置文件
     * @param name 名称
     * @param resource 配置文件EncodedResource对象 
     * @return PropertySource
     */
    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
        //1. 创建yaml解析的工厂
        YamlPropertiesFactoryBean factoryBean = new YamlPropertiesFactoryBean();
        //2. 设置要解析的资源内容
        factoryBean.setResources(resource.getResource());
        //3. 把资源文件解析成Properties对象
        Properties properties = factoryBean.getObject();
        //4. 把properties封装成PropertySource对象并返回
        return new PropertiesPropertySource("application", properties);
    }
}
  1. 使用@PropertySource注解,配置工厂类
@Configuration("appConfig")@PropertySource(    value = "classpath:application.yml",     factory = YamlSourceFactory.class)public class AppConfig {    @Value("${jdbc.driver}")    private String jdbcDriver;    @Value("${jedis.host}")    private String jedisHost;    public void showProps(){        System.out.println("jdbc.driver: " + this.jdbcDriver);        System.out.println("jedis.host: " + this.jedisHost);    }}
  1. 测试
@Testpublic void testProps(){    AppConfig appConfig = app.getBean(AppConfig.class);    appConfig.showProps();}

@Import

注册bean的方式

如果要注解方式配置一个bean,可以如下方式:

  • 在类上使用@Component,@Controller, @Service, @Repository:只能用于自己编写的类上,jar包里的类不能使用(比如ComboPooledDataSource)
  • 在方法上使用@Bean:把方法返回值配置注册成bean到IoC容器,通常用于注册第三方jar里的bean
  • 在核心配置类上使用@Import
    • @Import(类名.class),注册的bean的id是全限定类名
    • @Import(自定义ImportSelector.class):把自定义ImportSelector返回的类名数组,全部注册bean
    • @Import(自定义ImportBeanDefinitionRegister.class):在自定义ImportBeanDefinitionRegister里手动注册bean

ImportSelector导入器

示例1-直接导入注册bean
@Configuration@Import(Catalog.class) //导入Catalog注册beanpublic class AppConfig {    @Value("${jdbc.driver}")    private String jdbcDriver;    @Value("${jedis.host}")    private String jedisHost;    public void showProps(){        System.out.println("jdbc.driver: " + this.jdbcDriver);        System.out.println("jedis.host: " + this.jedisHost);    }}
示例2-使用ImportSelector注册bean
//导入Catalog, 并把MyImportSelector选择的全限定类名也注册bean
@Import({Catalog.class, MyImportSelector.class})
@Configuration
public class AppConfig {
    @Value("${jdbc.driver}")
    private String jdbcDriver;

    @Value("${jedis.host}")
    private String jedisHost;

    public void showProps(){
        System.out.println("jdbc.driver: " + this.jdbcDriver);
        System.out.println("jedis.host: " + this.jedisHost);
    }
}

示例3-ImportSelector的高级使用
说明
  • springboot框架里有大量的@EnableXXX注解,底层就是使用了ImportSelector解决了模块化开发中,如何启动某一模块功能的问题
  • 例如:
    • 我们开发了一个工程,要引入其它的模块,并启动这个模块的功能:把这个模块的bean进行扫描装载

  • 步骤:
    1. 创建一个Module:module_a
      1. 在包com.a里创建几个Bean
      2. 创建核心配置文件AConfig,扫描com.a
      3. 定义一个ImportSelector:AImportSelector,导入AConfig
      4. 定义一个注解@EnableModuleA
    2. 在我们的Module的核心配置文件AppConfig上增加注解:@EnableModuleA
      1. 引入module_a的坐标
      2. 测试能否获取到Module a里的bean
第一步:创建新Module:module_a
<!-- module_a的坐标 -->
<groupId>com.guang</groupId>
    <artifactId>spring_module_a</artifactId>
    <version>1.0-SNAPSHOT</version>

  1. 在包com.a.beans里创建类 DemoBeanA

    @Componentpublic class DemoBeanA {}
    
  2. 创建核心配置类AConfig

@Configuration@ComponentScan("com.a")public class AConfig {}
  1. 创建导入器:创建Java类,实现ImportSelector接口,重写selectImports方法
public class AImportSelector implements ImportSelector {    /**     * @param importingClassMetadata。被@Import注解标注的类上所有注解的信息     */    @Override    public String[] selectImports(AnnotationMetadata importingClassMetadata) {        return new String[]{AConfig.class.getName()};    }}
  1. 定义注解@EnableModuleA
@Import(AImportSelector.class)@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface EnableModuleA {}
第二步:引入module_a,启动模块a
  1. 在我们自己的工程pom.xml里增加依赖
<dependency>    <groupId>com.guang</groupId>    <artifactId>spring_module_a</artifactId>    <version>1.0-SNAPSHOT</version></dependency>
  1. 在我们自己的工程核心配置类上,使用注解@EnableModuleA启动模块a
@Configuration@EnableModuleApublic class AppConfig {}
  1. 在我们自己的工程里测试
   @Test    public void testBean(){        //在我们自己的工程里,可以获取到module_a里的bean        DemoBeanA demoBeanA = app.getBean(DemoBeanA.class);        System.out.println(demoBeanA);    }

ImportBeanDefinitionRegister注册器

说明
  • ImportBeanDefinitionRegister提供了更灵活的注册bean的方式

  • AOP里的@EnableAspectJAutoProxy就使用了这种注册器,用于注册不同类型的代理对象

  • 步骤:

    1. 创建注册器:

      创建Java类,实现ImportBeanDefinitionRegister接口,重写registerBeanDefinitions方法

    2. 在核心配置类上,使用@Import配置注册器

示例
  1. 创建类com.other.Other
public class Other {
    public void show(){
        System.out.println("Other.show....");
    }
}
  1. 创建注册器
public class CustomImportBeanDefinitionRegister implements ImportBeanDefinitionRegistrar {
    /**
     * @param importingClassMetadata 当前类的注解信息
     * @param registry 用于注册bean的注册器
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //获取bean定义信息
        BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition("com.other.Other").getBeanDefinition();
        //注册bean,方法参数:bean的id,bean的定义信息
        registry.registerBeanDefinition("other", beanDefinition);
    }
}
  1. 在核心配置类上,使用@Import配置注册器
@Configuration
@Import({CustomImportBeanDefinitionRegister.class})
public class AppConfig {

}
  1. 测试
@Test
public void testImportRegister(){
    //获取扫描范围外,使用ImportBeanDefinitionRegister注册的bean
    Other other = app.getBean("other",Other.class);
    other.showOther();
}

@Conditional

说明

  • @Conditional加在bean上,用于选择性的注册bean:
    • 符合Condition条件的,Spring会生成bean对象 存储容器中
    • 不符合Condition条件的,不会生成bean对象
  • 示例:
    • 有一个类Person(姓名name,年龄age)
    • 如果当前操作系统是Linux:就创建Person(linus, 62)对象,并注册bean
    • 如果当前操作系统是Windows:就创建Persion(BillGates, 67)对象,并注册bean
  • 步骤
    1. 创建Person类
    2. 创建两个Java类,都实现Condition接口:
      • WindowsCondition:如果当前操作系统是Windows,就返回true
      • LinuxCondition:如果当前操作系统是Linux,就返回true
    3. 在核心配置类里创建两个bean
      • 一个bean名称为bill,加上@Conditional(WindowsCondition.class)
      • 一个bean名称为linus,加上@Conditional(LinuxCondition.class)

示例

  1. 创建Person类
public class Person {    private String name;    private Integer age;    public Person() {    }    public Person(String name, Integer age) {        this.name = name;        this.age = age;    } 	//get/set...    //toString...}
  1. 创建两个Java类,实现Condition接口
public class WindowsCondition implements Condition {    @Override    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {		//从当前运行环境中获取操作系统名称        String osName = context.getEnvironment().getProperty("os.name");		//判断是否是Windows系统;如果是,返回true;否则返回false        return osName.contains("Windows");    }}
public class LinuxCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		//从当前运行环境中获取操作系统名称
        String osName = context.getEnvironment().getProperty("os.name");
		//判断是否是Linux系统;如果是,返回true;否则返回false
        return osName.contains("Linux");
    }
}
  1. 核心配置类
@Configuration
public class AppConfig {
	/**
	 * 如果操作系统是Windows,则注册bean对象:id为bill
	 */
    @Bean
    @Conditional(WindowsCondition.class)
    public Person bill(){
        return new Person("Bill Gates", 67);
    }

    /**
     * 如果操作系统是Linux,注册bean对象:id为linus
     */
    @Bean
    @Conditional(LinuxCondition.class)
    public Person linus(){
        return new Person("Linux", 60);
    }
}
  1. 测试
@Test
public void testCondition(){
    ApplicationContext app = new AnnotationConfigApplicationContext(AppConfig.class);
    Person person = app.getBean(Person.class);
    System.out.println(person);
}

执行一次单元测试方法之后,按照以下方式,可以通过JVM参数的方式,设置os.name的参数值。

设置之后,再次执行单元测试方法

Conditional的扩展注解

@Conditional在springboot里应用非常多,以下列出了一些@Conditional的扩展注解:

  • @ConditionalOnBean:当容器中有指定Bean的条件下进行实例化。
  • @ConditionalOnMissingBean:当容器里没有指定Bean的条件下进行实例化。
  • @ConditionalOnClass:当classpath类路径下有指定类的条件下进行实例化。
  • @ConditionalOnMissingClass:当类路径下没有指定类的条件下进行实例化。
  • @ConditionalOnWebApplication:当项目是一个Web项目时进行实例化。
  • @ConditionalOnNotWebApplication:当项目不是一个Web项目时进行实例化。
  • @ConditionalOnProperty:当指定的属性有指定的值时进行实例化。
  • @ConditionalOnExpression:基于SpEL表达式的条件判断。
  • @ConditionalOnJava:当JVM版本为指定的版本范围时触发实例化。
  • @ConditionalOnResource:当类路径下有指定的资源时触发实例化。
  • @ConditionalOnJndi:在JNDI存在的条件下触发实例化。
  • @ConditionalOnSingleCandidate:当指定的Bean在容器中只有一个,或者有多个但是指定了首选的Bean时触发实例化。

@Profile

说明
  • 在开发中,我们编写的工程通常要部署不同的环境,比如:开发环境、测试环境、生产环境。不同环境的配置信息是不同的,比如:数据库配置信息;如果每次切换环境,都重新修改配置的话,会非常麻烦,且容易出错
  • 针对这种情况,Spring提供了@Profile注解:可以根据不同环境配置不同的bean,激活不同的配置
    • @Profile注解的底层就是@Conditional
  • 例如:
    • 定义三个数据源:
      • 开发环境一个DataSource,使用@Profile配置环境名称为dev
      • 测试环境一个DataSource,使用@Profile配置环境名称为test
      • 生产环境一个DataSource,使用@Profile配置环境名称pro
    • 在测试类上,使用@ActiveProfiles激活哪个环境,哪个环境的数据源会生效
      • 实际开发中有多种方式可以进行激活,这里演示一个单元测试类里是怎样激活的
示例
  1. pom.xml中增加导入依赖mysql驱动, c3p0
<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <dependency>
        <groupId>c3p0</groupId>
        <artifactId>c3p0</artifactId>
        <version>0.9.1.2</version>
    </dependency>
    
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.2.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>
  1. 在配置类里创建三个数据源,并配置@Profile
@Configuration
public class AppConfig {

    @Bean
    @Profile("dev")
    public DataSource devDataSource() throws PropertyVetoException {
        System.out.println("dev  开发环境的");
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setDriverClass("com.mysql.jdbc.Driver");
        dataSource.setJdbcUrl("jdbc:mysql:///devdb");
        dataSource.setUser("root");
        dataSource.setPassword("root");
        dataSource.setMaxPoolSize(20);
        return dataSource;
    }

    @Bean
    @Profile("test")
    public DataSource testDataSource() throws PropertyVetoException {
        System.out.println("test 测试环境的");
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setDriverClass("com.mysql.jdbc.Driver");
        dataSource.setJdbcUrl("jdbc:mysql:///testdb");
        dataSource.setUser("root");
        dataSource.setPassword("root");
        dataSource.setMaxPoolSize(100);
        return dataSource;
    }

    @Bean
    @Profile("pro")
    public DataSource proDataSource() throws PropertyVetoException {
        System.out.println("pro 生产环境的");
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setDriverClass("com.mysql.jdbc.Driver");
        dataSource.setJdbcUrl("jdbc:mysql:///prodb");
        dataSource.setUser("root");
        dataSource.setPassword("root");
        dataSource.setMaxPoolSize(200);
        return dataSource;
    }
}
  1. 在测试类上,使用@ActiveProfiles激活哪个环境,哪个环境的数据源会生效

    或者使用JVM参数-Dspring.profiles.active=dev

@ActiveProfiles("dev") //激活dev环境
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class SpringTest {
    
    @Autowired
    private ApplicationContext app;

    @Test
    public void testProfile(){
        DataSource dataSource = app.getBean(DataSource.class);
        System.out.println(dataSource);
    }
}

posted @ 2022-02-19 17:27  写的代码很烂  阅读(40)  评论(0编辑  收藏  举报