摘要

了解一下Spring框架,并且学习Spring的控制反转IOC(通过工厂来间接获取对象)、依赖注入DI(在IOC创建对象时,通过配置间接传入数据)

正文

Spring官网

Spring学习资料

Spring学习目录

  1. Spring的IOC和DI
  2. Spring的AOP
  3. Spring的事务控制
  4. 源码

一、概念

Spring Framework 是一个开源的Java SE/EE 全栈(full-stack)的应用程序框架,以 IoC(Inverse of Control,控制反转)和 AOP(Aspect Oriented Programming,面向切面编程)为内核,还能整合开源世界的众多著名的第三方框架和类库。

Spring的源码是经典学习范例,对java的设计模式灵活运用,有能力可以研究。

作用

  1. 方便解耦,简化开发
  2. AOP编程支持
  3. 声明式事务的支持
  4. 方便程序的测试
  5. 方便集成各种优秀框架
  6. 降低JavaEE Api的使用难度

二、耦合

耦合是指程序间的依赖关系,包括类之间的依赖方法间的依赖

解耦就是降低程序间的依赖关系

实际开发中做到:编译期不依赖,运行时才依赖

下面举个例子

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
public class JdbcDemo01 {
    public static void main(String[] args) throws Exception {
        //1.注册驱动
        //这种写法算是一种优化,依赖的只是一个字符串,找不到就是没有
        //去掉依赖,编译仍然可以通过
        Class.forName("com.mysql.cj.jdbc.Driver");
        //这种写法存在耦合问题,依赖的是一个类文件,如果去掉依赖,编译不通过
//        DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
        //2.获取连接
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false", "root", "root");
        //3.获取操作数据库的预处理对象
        PreparedStatement ps = conn.prepareStatement("select * from account");
        //4.执行sql,得到结果集
        ResultSet rs = ps.executeQuery();
        //5.封装结果集
        while(rs.next()){
            System.out.println(rs.getString("name"));
        }
        //6.释放资源
        rs.close();
        ps.close();
        conn.close();
    }
}

对于传统的开发工程,我们可以参照下面这种结构,ui调用service,service调用dao,其中如果没有dao的实现类,那么就不能new对象,整个工程就会报错,无法编译,这就是存在耦合问题。

2.1 工厂模式解耦

为了降低耦合,我们可以通过工厂模式,读取配置文件来创建对象,从而降低耦合度。

1.png

对于类之间依赖,解耦的思路

  1. 通过工厂模式,使用反射来创建对象,避免使用new关键字。
    • 不能像上面那样添加字符串,那就写死了,如果mysql换成oracle,岂不是又得改一遍?所以需要配合文件
  2. 通过读取配置文件来获取要创建的对象全限定类名

创建生产bean工厂

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
29
30
31
32
33
34
public class BeanFactory  {
    private static Properties props;
    //使用静态代码块,为Properties对象赋值
    static{
        try {
            props=new Properties();
            InputStream is = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
            props.load(is);
        } catch (Exception e) {
           throw new ExceptionInInitializerError("读取配置文件失败!");
        }
    }

    /**
     * 创建Bean
     * @param beanName
     * @param <T>
     * @return 返回值可以是泛型,也可以是Object对象,我这里用泛型,<T>表示声明
     */
    public static <T>T getBean(String beanName){
        T bean = null;
        String beanPath=props.getProperty(beanName);
        try {
            bean = (T) Class.forName(beanPath).newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return bean;
    }
}

然后将其他获取bean的new方法,都换成通过工厂来获取

java
1
2
3
//如下
AccountService as = (AccountService)BeanFactory.getBean("accountService");
AccountDao accountDao= (AccountDao) BeanFactory.getBean("accountDao");

通过这么处理,即使删除掉了AccountDaoImpl,那么也是可以编译的,不报错,但是运行时,会有找不到文件的异常弹出。

2.png

工厂模式中也存在问题,进行如下修改。

AccountDaoImpl.java

java
1
2
3
4
5
6
7
8
public class AccountDaoImpl implements AccountDao {
    private int i=1;
    @Override
    public void saveAccount() {
        i++;
        System.out.println("保存数据"+i);
    }
}

Client.java

java
1
2
3
4
5
6
7
8
public class Client {
    public static void main(String[] args) {
        for(int i=0;i<10;i++){
            AccountService as = (AccountService)BeanFactory.getBean("accountService");
            as.saveAccount();
        }
    }
}

但是每次输出的结果都是,“保存数据2”,可知,每执行一次getBean都创建了AccountService对象,这个对象是多例的。

多例对象,会在内存中被创建多次,执行效率就低了;单例对象,在内存中只会被创建一次,就比如Servlet,执行效率高。

在该工厂中,其实用单例或者多例都可以,那从效率考虑,肯定是用单例对象更好。

2.2 工厂生产单例对象

对工厂进行修改,在BeanFactory加载时,实例化对象作为value,存储到map中,获取时直接通过map获取,这样就到做到单例了。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class BeanFactory {
    //定义一个Properties对象
    private static Properties props;

    //定义一个Map,用于存放我们要创建的对象。我们把它称之为容器
    private static Map<String,Object> beans;

    //使用静态代码块为Properties对象赋值
    static {
        System.out.println("静态代码块执行..");
        try {
            props = new Properties();
            InputStream is = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
            props.load(is);
            //实例化容器
            beans = new HashMap<String,Object>();
            //取出配置文件中所有的Key
            Enumeration<Object> keys = props.keys();
            //遍历枚举
            while (keys.hasMoreElements()){
                String key = keys.nextElement().toString();
                String beanPath = props.getProperty(key);
//                Object value = Class.forName(beanPath).newInstance();
                //jdk9以后的实例化推荐写法
                Object value = Class.forName(beanPath).getDeclaredConstructor().newInstance();
                beans.put(key,value);
            }
        }catch(Exception e){
            throw new ExceptionInInitializerError("初始化properties失败!");
        }
        System.out.println("静态代码块执行完毕!");
    }

    /**
     * 根据bean的名称获取对象
     * @param beanName
     * @return
     */
    public static Object getBean(String beanName){
        Object o = beans.get(beanName);
        System.out.println("获取"+beanName+"..此时beans的大小:"+beans.size()+"..此时要获取的内容:"+o.toString());
        return o;
    }

}

注意

这个地方卡了我快7个小时,最后也没整明白。我把疑问放到b站了。

在使用过程中,成员变量直接调用BeanFactory获取AccountDao(或者默认构造方法中进行实例)时,都会导致静态代码块未执行完毕,就执行静态方法。但是在java12的环境下,直接异常,而在java8下,就能运行,虽然结果不是预期那样。

最后,我将获取AccountDao放到重写的方法中,才有了预期的结果。但我纳闷的是,为什么不能直接成员变量实例化呢?

下面是最后的解决方案,只需要修改此处即可。

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class AccountServiceImpl implements AccountService {
    //    private AccountDao accountDao=new AccountDaoImpl();
    //直接在后面实例化,或者在默认构造方法中,进行实例化,会出现问题。具体原因为啥,咱不清楚。
    private AccountDao accountDao;

    @Override
    public void saveAccount() {
        //在方法里面进行实例化
        accountDao = (AccountDaoImpl) BeanFactory.getBean("accountDao");
        accountDao.saveAccount();
    }
}

三、IOC

3.1 控制反转IOC

直接通过new,获取对象,这种方式是主动的,它可以直接控制获取的是什么对象。

java
1
AccountDao accountDao=new AccountDaoImpl();

通过上面解耦的例子,我们获取对象,是向工厂获取,这种方式是被动的,他的控制在于getBean后面传递的参数。

java
1
AccountDao accountDao=(AccountDaoImpl)BeanFactory.getBean("accountDao");

这种被动接收的方式获取对象的思想就是控制反转,带来的好处,就是降低了程序间的依赖关系,也就是耦合。

3.png

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来降低计算机代码之间的耦合度

其中最常见的方式

  • 依赖注入(Dependency Injection,简称DI)
  • 依赖查找(Dependency Lookup)

在实际开发中如果自己实现控制反转,会消耗大量时间精力,所以,我们可以通过spring来实现ioc。

第一个spring项目

从spring官网的中,下载spring5.3.4的源码

4.png

在maven的pom中,添加spring-context的依赖,就会出现spring框架所需要的的核心jar包。

5.png

至于为什么要添加spring-context的依赖,而不是添加别的依赖。通过下图的依赖关系,我们可以知道,spring-context依赖了其他的jar包,所以我们在导入spring-context的时候,maven会将其他关联的一并导入。

6.png

创建xml配置文件spring.xml。格式参考官网,ctrl+f直接搜索xmlns即可。

xml
1
2
3
4
5
6
7
8
9
<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--把对象的创建交给spring管理-->
    <bean id="accountService" class="top.meethigher.demo03.service.impl.AccountServiceImpl"></bean>
    <bean id="accountDao" class="top.meethigher.demo03.dao.impl.AccountDaoImpl"></bean>
</beans>

Spring可以通过ApplicationContext下的实现类,来实现读取xml配置,获取核心容器,从而获取bean对象。常用的实现类如下

  • ClassPathXmlApplicationContext:加载类路径下的配置文件,要求配置文件必须在类路径下
  • FileSystemXmlApplicationContext:加载磁盘任意路径下的配置文件(必须有访问权限)
  • AnnotationConfigApplicationContext:读取注解
java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class SpringClient {
    /**
     * 获取Spring的ioc核心容器,并根据id获取对象
     * @param args
     */
    public static void main(String[] args) {
        //1.获取核心容器对象
        ApplicationContext ac=new ClassPathXmlApplicationContext("spring.xml");
//        ApplicationContext ac = new FileSystemXmlApplicationContext("D:\\Develop\\JavaDev\\Spring-notes\\src\\main\\resources\\spring.xml");
        //2.根据id获取bean对象
        AccountServiceImpl accountService = (AccountServiceImpl) ac.getBean("accountService");
        AccountDaoImpl accountDao = (AccountDaoImpl) ac.getBean("accountDao");

        System.out.println(accountService);
        System.out.println(accountDao);
    }
}

3.2 创建对象

创建对象时机

核心容器的两个接口引发的问题

  • ApplicationContext
    • 它在创建核心容器时,创建对象采取的策略是,立即加载。也就是说,只要一读取配置文件,马上创建配置文件中的对象。
    • 适用单例对象
  • BeanFactory
    • 它在创建核心容器时,创建对象采取的策略是,延迟加载。也就是说,什么时候要获取对象,什么时候才开始创建对象。
    • 适用多例对象

对于这两个的测试,可以通过打断点来验证,具体步骤采用视频比较直观。

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
public class SpringClient {
    /**
     * 获取Spring的ioc核心容器,并根据id获取对象
     * @param args
     */
    public static void main(String[] args) {
        //=====ApplicationContext
        //1.获取核心容器对象
//        ApplicationContext ac=new ClassPathXmlApplicationContext("spring.xml");
////        ApplicationContext ac = new FileSystemXmlApplicationContext("D:\\Develop\\JavaDev\\Spring-notes\\src\\main\\resources\\spring.xml");
//        //2.根据id获取bean对象
//        AccountServiceImpl accountService = (AccountServiceImpl) ac.getBean("accountService");
//
//
//        System.out.println(accountService);


        //=====BeanFactory,延迟加载
        Resource resource=new ClassPathResource("spring.xml");
        BeanFactory factory = new XmlBeanFactory(resource);
        AccountService accountService = (AccountService) factory.getBean("accountService");
        System.out.println(accountService);
    }
}

但是在实际开发中,还是ApplicationContext使用用的更多,也更智能,他可以根据配置文件来决定是否要延迟加载。

BeanFactory只是当做一个演示。通过下面的关系图,可以知道其实BeanFactory属于底层接口,所以有些功能也并不完善,并不推荐用。

7.png

获取bean三种方式

Spring对bean的管理细节

  1. 创建bean的三种方式
  2. bean对象的作用范围
  3. bean对象的生命周期

第一种方式:使用默认构造函数创建。

在spring的配置文件中使用bean标签,配以id和class属性之后,且没有其他属性和标签时,采用的就是默认构造函数,创建bean对象。此时,若没有默认构造函数(比如只有一个有参构造函数),则对象无法创建。

第二种方式:使用普通工厂中的方法创建对象(使用某个类中的方法创建对象,并存入spring容器)

创建InstanceFactory

java
1
2
3
4
5
public class InstanceFactory {
    public AccountService getAccountService(){
        return new AccountServiceImpl();
    }
}

我们想将InstanceFactory中创建的AccountService对象存入spring容器,可以使用第二种方式。

xml
1
2
3
4
5
6
7
8
9
<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="instanceFactory" class="top.meethigher.demo04.factory.InstanceFactory"></bean>
    <!--通过factory-bean找到工厂的bean,然后调用他的factory-method生成对象,存为accountService-->
    <bean id="accountService" factory-bean="instanceFactory" factory-method="getAccountService"></bean>
</beans>

第三种方式:使用工厂中的静态方法创建对象(使用某个类的静态方法创建对象,并存入spring容器)

创建StaticFactory

java
1
2
3
4
5
public class StaticFactory {
    public static AccountService getAccountService(){
        return new AccountServiceImpl();
    }
}

使用第三种方式获取accountService

xml
1
2
3
4
5
6
7
<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="accountService" class="top.meethigher.demo04.factory.StaticFactory" factory-method="getAccountService"></bean>
</beans>

第二种和第三种方式,适合那些我们引入的jar包,我们无法通过修改源码的方式来提供默认构造函数

设置bean作用范围

在第二节的时候,我们通过修改工厂代码,来实现了生产单例对象,那么spring是不是单例对象呢,验证一下。

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class SpringClient04 {
    /**
     * 获取Spring的ioc核心容器,并根据id获取对象
     * @param args
     */
    public static void main(String[] args) {
        ApplicationContext ac=new ClassPathXmlApplicationContext("spring.xml");
        for(int i=0;i<10;i++){
            AccountService accountService = (AccountService) ac.getBean("accountService");
            System.out.println(accountService);//输出10次全是AccountServiceImpl@14a2f921
        }
    }
}

通过输出结果,可知,默认构造函数只走了一次,后面可知,获取10次对象都是同一个。

8.png

bean标签的scope属性,用于指定bean的作用范围(常用就是单例和多例)

  • singleton:单例(默认值)
  • prototype:多例。每调用一次getBean,就重新获取一个新的对象
  • request:作用于web应用的请求范围
  • session:作用于web应用的会话范围
  • global-session:作用于集群环境的会话范围(全局会话范围),当不是集群时,它就是session

在xml配置bean的时候,将scope配置为prototype,再运行代码的时候,就会发现,没getBean一次,就会创建一次对象,这就是多例。

9.png

集群就是,很多相互独立、高速互联的计算机组成的组。

就比如我们访问一个大型网站,登录时,会有验证码,我们集群里面挑选一台空闲的机器来存储生成验证码的session,但是,当我们去提交验证码时,若调用了另外一台机器,则该session就不会存在了。

那么我们可以通过配置global-session,让集群中的所有服务器共用一个session。

10.png

bean生命周期

单例对象

  • 创建:当容器创建时(解析配置文件时),创建对象
  • 服务:只要容器存在,对象就一直存在着
  • 销毁:容器销毁,对象消亡

AccountServiceImpl

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class AccountServiceImpl implements AccountService {

    public AccountServiceImpl() {
        System.out.println("AccountService对象已经创建了!");
    }

    public void saveAccount() {
        System.out.println("service中的saveAccount方法执行");
    }

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

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

测试类

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class SpringClient04 {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
        AccountService accountService = (AccountService) ac.getBean("accountService");
        System.out.println(accountService);

        //手动关闭容器,如果此处使用ApplicationContext是没有close方法的,原因是父类无法调用子类特有的方法
        ac.close();
    }
}

spring.xml

xml
1
2
3
4
5
6
7
<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean init-method="init" destroy-method="destroy" scope="singleton" id="accountService" class="top.meethigher.demo04.service.impl.AccountServiceImpl"></bean>
</beans>

输出结果如下

11.png

总结就是,单例对象的生命周期和容器相同。

多例对象

  • 创建:当我们使用对象时(可以通过断点的方式,进行测试),创建对象
  • 服务:对象使用过程中
  • 销毁:当对象长时间不使用时,且没有别的对象引用时,由java的垃圾回收器回收。

注意

spring通过配置文件中的scope,判断要创建单例还是多例对象。

如果是单例,则立即创建。

如果是多例,则延迟创建。

四、DI

4.1 依赖注入DI

控制反转(Inversion of Control)的作用是降低程序间的耦合(依赖关系)。依赖关系的管理,都交给了spring来维护。这种依赖关系的管理,就叫做依赖注入(Dependency Injection)

在当前类中需要用到的其他类的对象,由spring来提供,我们只需要在配置文件或注解中说明。

依赖注入的类型

  • 基本类型和String
  • bean类型(在配置文件中、注解中配置的bean)
  • 复杂类型/集合类型

注入的方式

  • 构造函数
  • set方法
  • 注解

构造函数注入

若是经常变化的数据,并不适用于构造函数注入的方式。

标签constructor-arg,放置在bean标签的内部。

五个属性

  1. 位置
    • index:指定构造函数中该索引位置的参数。参数索引的位置从0开始
    • name:指定构造函数中该名称的参数。常用√
    • type:指定要注入的数据的数据类型,该数据类型也是构造函数中某个或者某些参数的类型
  2. 传值
    • value:传入的参数的值,只适用于基本类型和String类型的数据
    • ref: 指定其他的bean类型,它指的就是ioc核心容器中出现过得bean对象

AccountServiceImpl

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class AccountServiceImpl implements AccountService {
    //若是经常变化的数据,并不适用于注入的方式
    private String name;
    private Integer age;
    private Date birthday;

    public AccountServiceImpl(String name, Integer age, Date birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }

    public void saveAccount() {
        System.out.println("service中的saveAccount方法执行--"+name+"--"+age+"--"+birthday);
    }
}

spring.xml

xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--构造函数注入-->
    <bean id="accountService" class="top.meethigher.demo05.service.impl.AccountServiceImpl">
        <constructor-arg index="0" value="白沉香"/>
        <constructor-arg index="1" value="18"/>
        <constructor-arg index="2" ref="time"/>
    </bean>
    <bean id="time" class="java.util.Date"></bean>
</beans>

测试类

java
1
2
3
4
5
6
7
public class SpringClient05 {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
        AccountService accountService = (AccountService) ac.getBean("accountService");
        accountService.saveAccount();
    }
}

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

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

☆Set方法注入

标签property,bean标签的内部。

三个属性

  • name:指定注入所调用的set方法名称
  • value:传入的参数的值,只适用于基本类型和String类型的数据
  • ref:指定其他的bean类型,它指的就是ioc核心容器中出现过得bean对象

AccountServiceImpl

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class AccountServiceImpl implements AccountService {
    //若是经常变化的数据,并不适用于注入的方式
    private String name;
    private Integer age;
    private Date birthday;

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public void saveAccount() {
        System.out.println("service中的saveAccount方法执行--"+name+"--"+age+"--"+birthday);
    }

}

spring.xml

xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--set方法注入-->
    <bean id="accountService" class="top.meethigher.demo05.service.impl.AccountServiceImpl">
        <property name="name" value="白沉香"></property>
        <property name="age" value="18"></property>
        <property name="birthday" ref="time"></property>
    </bean>
    <bean id="time" class="java.util.Date"></bean>
</beans>

测试类跟构造函数注入的一样。

弊端:如果有某个成员必须有值,则set方法可能没有执行。

优势:改变了bean对象的实例化方式,在创建对象时,没有明确的限制。

复杂类型注入

用于给List结构(单列)注入的标签

  • list
  • array
  • set

用于Map结构(双列)注入的标签

  • map
  • props

总结:结构相同,标签可以互换

AccountServiceImpl2

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
29
30
31
32
33
34
public class AccountServiceImpl2 implements AccountService {
    private String[] myStrs;
    private List<String> myList;
    private Set<String> mySet;
    private Map<String, String> myMap;
    private Properties props;

    public AccountServiceImpl2(Set<String> mySet, Map<String, String> myMap) {
        this.mySet = mySet;
        this.myMap = myMap;
    }

    public void setMyStrs(String[] myStrs) {
        this.myStrs = myStrs;
    }

    public void setMyList(List<String> myList) {
        this.myList = myList;
    }


    public void setProps(Properties props) {
        this.props = props;
    }

    @Override
    public void saveAccount() {
        System.out.println("myStrs=" + Arrays.toString(myStrs) +
                "\nmyList=" + myList +
                "\nmySet=" + mySet +
                "\nmyMap=" + myMap +
                "\nprops=" + props);
    }
}

spring.xml

xml
 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
<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="accountService" class="top.meethigher.demo05.service.impl.AccountServiceImpl2">
        <!--构造函数注入-->
        <constructor-arg name="mySet">
            <set>
                <value>白沉香</value>
                <value>小舞</value>
                <value>胡列娜</value>
            </set>
        </constructor-arg>
        <constructor-arg name="myMap">
            <map>
                <entry key="小舞" value="18000"></entry>
            </map>
        </constructor-arg>
        <!--set方法注入-->
        <property name="myStrs">
            <array>
                <value>白沉香</value>
                <value>小舞</value>
                <value>胡列娜</value>
            </array>
        </property>
        <property name="myList">
            <list>
                <value>白沉香</value>
                <value>小舞</value>
                <value>胡列娜</value>
            </list>
        </property>
        <property name="props">
            <props>
                <prop key="小舞">180000</prop>
                <prop key="白沉香">18</prop>
                <prop key="胡列娜">22</prop>
            </props>
        </property>
    </bean>
</beans>

4.2 注解

spring既可以通过xml配置,也可以通过注解来配置。本章的1到3的例子,都是基于xml。这节讲注解来实现以上功能。

如果使用注解,那么需要在xml配置扫描当前包下文件所有的注解。

xml的约束可以去官方文档找,进入core,关键字搜索xmlns:context即可。

xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    
    <!--扫描top.meethigher.demo06下的所有注解-->
    <context:component-scan base-package="top.meethigher.demo06"></context:component-scan>
</beans>

注意

注解中,如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略

比如@Component(value="accountService"),可以省略写成@Component("accountService")

常用注解

常用的11个注解,分为四类

  • 创建对象

    • @Component,相当于xml的bean
      • 作用:用于把当前类,通过反射创建对象,存入spring容器中(容器是个key-value结构,对象作为存入容器,还需要一个key)
      • 位置:当前的类名上
      • value属性:相当于bean的id(容器key-value中的键),当我们不写时,他的默认值是当前首字母小写的类名
      • 下面的Controller、Service、Repository作用跟属性与Component一样,相当于Component的没有扩展的子类。但是,他们是spring为我们提供的明确三层使用的注解,使我们的三层对象更加清晰。
    • @Controller:一般用于表现层
    • @Service:一般用于业务层
    • @Repository:一般用于持久层
  • 注入数据:Autowired、Qualifier、Resource只能注入其他bean类型的数据,无法注入基本类型和String类型的数据。

    • @Autowired
      • 作用:自动按照类型注入,只要容器中有唯一的bean对象类型和要注入的变量类型匹配,就可以注入成功
      • 位置:变量上或者方法上
      • 注意:使用注解时,set方法就不是必需的了,可以理解成该注解本身已经通过set方法来赋值了。
      • 如果ioc容器中,没有任何bean的类型和要注入的变量类型匹配,则报错。
      • 如果ioc容器中,有多个类型匹配时,就选择id与变量名相同的bean,没有则报错。
    • @Qualifier
      • 作用:在按照类型注入的基础上,再按照名称注入。**它在给类成员变量注入时,不能单独使用,但是在给方法参数注入时,可以单独使用。**可以参照本章下面的代码,和CRUD案例中的代码
      • 位置:变量上或者方法上
      • value属性:指定注入bean的id
    • @Resource
      • 作用:直接按照bean的id注入,它可以独立使用。就相当于@Autowired和@Qualifier
      • 位置:变量上或者方法上
      • name属性:用于指定bean的id
    • @Value
      • 作用:用于注入基本类型和String类型的数据
      • 位置:变量上或者方法上
      • value属性:用于指定数据的值。他可以使用spring中的spel(也就是spring中的el表达式)
      • spel写法:${表达式}。其实jsp、mybatis、spring中el表达式格式都一样,就看它是写的位置。
  • 作用范围

    • @Scope

      • 作用:用于指定bean的作用范围

      • value属性:指定范围的取值,常用取值:singleton(单例,默认值)、prototype(多例)

  • 声明周期:这个只需要了解即可。

    • @PreDestroy
      • 作用:用于指定销毁方法
    • @PostConstruct
      • 作用:用于指定初始化方法

以上三个注解,Resource、PreDestroy、PostConstruct不是spring提供的,是java提供的,所以我们需要在pom中引入javax.annotation。

如果要注入复杂/集合类型,只能使用xml

xml
1
2
3
4
5
<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
</dependency>

使用注解

首先要扫描指定包下的所有注解,配置xml。跳到3.4

AccountServiceImpl

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
29
30
@Component(value = "accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    @Qualifier("accountDao1")
//    @Resource(name="accountDao1"),相当于以上两个,该注解由java提供,8以上需要自己引入
    private AccountDao accountDao;
    @Value("20")//spel表达式暂不演示
    private Integer num;

    public AccountServiceImpl() {
        System.out.println("对象已经创建了!"+"num="+num+",accountDao="+accountDao);
    }

    @Override
    public void saveAccount() {
        accountDao.saveAccount();
        System.out.println(num);
    }

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

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

AccountDaoImpl1

java
1
2
3
4
5
6
7
8
//该类用来测试@Autowired,类型匹配不唯一时,如何处理
@Component("accountDao1")
public class AccountDaoImpl1 implements AccountDao {
    @Override
    public void saveAccount() {
        System.out.println("保存数据1");
    }
}

AccountDaoImpl

java
1
2
3
4
5
6
7
@Component("accountDao")
public class AccountDaoImpl implements AccountDao {
    @Override
    public void saveAccount() {
        System.out.println("保存数据");
    }
}

测试类

java
1
2
3
4
5
6
7
8
9
public class SpringClient06 {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
        AccountServiceImpl accountService = (AccountServiceImpl) ac.getBean("accountService");
        System.out.println(accountService);
        accountService.saveAccount();
        ac.close();//测试@PreDestroy注解
    }
}

以上述代码为例,来了解一下@Autowired的内部原理。

首先,通过xml的配置,扫描包下的所有bean的注解,然后存入spring容器。

12.png

当我们给类中的变量加上@Autowired时,他就会根据当前的类型,去springioc容器中找该类型。

如果唯一,那就进行注入。

如果不唯一,那就需要根据当前的变量名称,来寻找springioc容器中的key,寻找到了,就进行注入,否则报错。

java
1
2
private AccountDao accountDao=null;
权限修饰符 数据类型    变量名     数据类型

由此为了方便使用,才有了@Qualifier,以及更方便地@Resource

五、CRUD案例

环境

  • spring
  • c3p0:以前没用过
  • dbutils:以前没用过

在创建一个三层架构的项目时,其实有更方便的创建步骤

  1. domain实体类
  2. service业务层:创建接口、创建一系列方法、创建实现类
  3. dao持久层:创建接口,将service接口中的方法复制到dao接口,创建实现类
  4. 表现层(此处可以通过单元测试来模拟表现层)

5.1 基于XML

Account

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
29
30
31
32
33
34
35
36
37
38
public class Account implements Serializable {
    private Integer id;
    private String name;
    private Float money;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Float getMoney() {
        return money;
    }

    public void setMoney(Float money) {
        this.money = money;
    }

    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", money=" + money +
                '}';
    }
}

AccountDao、AccountService、AccountServiceImpl省略,内容自己能想出来。

AccountDaoImpl

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class AccountDaoImpl implements AccountDao {
    private QueryRunner runner;

    public void setRunner(QueryRunner runner) {
        this.runner = runner;
    }

    @Override
    public List<Account> findAll() {
        try {
            return runner.query("select * from account", new BeanListHandler<Account>(Account.class));
        } catch (SQLException e) {
            //因为sqlexception是编译时期异常,必须处理。把他作为运行时异常,就可以不用处理了。
            throw new RuntimeException();
        }
    }

    @Override
    public Account findById(Integer id) {
        try {
            return runner.query("select * from account where id=?", new BeanHandler<Account>(Account.class), id);
        } catch (SQLException e) {
            throw new RuntimeException();
        }
    }


    @Override
    public void updateAccount(Account account) {
        try {
            runner.update("update account set name=?,money=? where id=?", account.getName(), account.getMoney(), account.getId());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void deleteAccount(Integer id) {
        try {
            runner.update("delete from account where id=?", id);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }


    /**
     * 重点:保存并返回id
     * @param account
     */
    @Override
    public void saveAccount(Account account) {
        try {
            runner.update("insert into account(name,money) values(?,?)", account.getName(), account.getMoney());
            //形参是对象时,实际传的是地址。改变形参会影响实参。
            account.setId(Integer.parseInt(runner.query("select last_insert_id() id",new MapHandler()).get("id").toString()));
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 重点:聚合查询
     * @return
     */
    @Override
    public int findCount() {
        try {
            // 法一
//            return Integer.parseInt(runner.query("select count(*) count from account", new MapHandler()).get("count").toString());
            // 法二
            return Integer.parseInt(runner.query("select count(*) from account",new ScalarHandler<>(1)).toString());
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException();

        }
    }
}

spring.xml

xml
 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
<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--配置service-->
    <bean id="accountService" class="top.meethigher.demo07.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"></property>
    </bean>
    <!--配置dao-->
    <bean id="accountDao" class="top.meethigher.demo07.dao.impl.AccountDaoImpl">
        <property name="runner" ref="runner"></property>
    </bean>
    <!--配置QueryRunner,避免多个用户使用同一个对象,我们应该将作用范围设置为多例-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <constructor-arg name="ds" ref="ds"></constructor-arg>
    </bean>
    <!--配置数据源-->
    <bean id="ds" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring?useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=Asia/Shanghai&amp;useSSL=false"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
    </bean>
</beans>

junit单元测试

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class ClientTest {
    private ClassPathXmlApplicationContext cac;
    private AccountService accountService;
    @Before
    public void setUp() throws Exception {
        cac = new ClassPathXmlApplicationContext("spring.xml");
        accountService = cac.getBean("accountService", AccountService.class);
    }

    @After
    public void tearDown() throws Exception {
        cac.close();
    }

    @Test
    public void testFindAll() {
        List<Account> all = accountService.findAll();
        System.out.println(all);
    }

    @Test
    public void testFindById() {
        Account byId = accountService.findById(1);
        System.out.println(byId);
    }

    @Test
    public void testUpdateAccount() {
        Account account = new Account();
        account.setId(4);
        account.setName("水冰儿");
        account.setMoney(7000f);
        accountService.updateAccount(account);
    }

    @Test
    public void testDeleteAccount() {
        accountService.deleteAccount(4);
    }

    /**
     * 重点:保存,并且返回自增的id
     */
    @Test
    public void testSaveAccount() {
        Account account = new Account();
        account.setName("水冰儿");
        account.setMoney(8000f);
        accountService.saveAccount(account);
        System.out.println(account);
    }

    /**
     * 重点:聚合查询
     */
    @Test
    public void testFindCount() {
        System.out.println(accountService.findCount());
    }

    /**
     * 测试传递对象(地址),形参改变,实参是否改变
     */
    @Test
    public void test() {
        Account account = new Account();
        m(account);
        System.out.println(account);
    }
    public void m(Account account){
        account.setId(1);
    }
}

在该过程中,有几点注意事项

  1. dbutils的使用。通过dbutils实现查询后保存id返回、实现聚合查询
  2. 形参是bean对象时(相当于传地址),改变形参,实参也会改变
  3. 编译期异常必须处理,运行时异常可以不用处理
  4. sql语句, select last_insert_id()
  5. 连接池应该使用多例,避免多个用户使用同一个对象引发问题。

5.2 基于XML与注解

引用的外部的类,需要xml来配置,但是自己写的类,是可以通过注解来配置了。

AccountDaoImpl

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {

    @Autowired
    private QueryRunner runner;


    @Override
    public List<Account> findAll() {
        try {
            return runner.query("select * from account", new BeanListHandler<Account>(Account.class));
        } catch (SQLException e) {
            //因为sqlexception是编译时期异常,必须处理。把他作为运行时异常,就可以不用处理了。
            throw new RuntimeException();
        }
    }

    @Override
    public Account findById(Integer id) {
        try {
            return runner.query("select * from account where id=?", new BeanHandler<Account>(Account.class), id);
        } catch (SQLException e) {
            throw new RuntimeException();
        }
    }


    @Override
    public void updateAccount(Account account) {
        try {
            runner.update("update account set name=?,money=? where id=?", account.getName(), account.getMoney(), account.getId());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void deleteAccount(Integer id) {
        try {
            runner.update("delete from account where id=?", id);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }


    /**
     * 重点:保存并返回id
     * @param account
     */
    @Override
    public void saveAccount(Account account) {
        try {
            runner.update("insert into account(name,money) values(?,?)", account.getName(), account.getMoney());
            //形参是对象时,实际传的是地址。改变形参会影响实参。
            account.setId(Integer.parseInt(runner.query("select last_insert_id() id",new MapHandler()).get("id").toString()));
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 重点:聚合查询
     * @return
     */
    @Override
    public int findCount() {
        try {
            // 法一
//            return Integer.parseInt(runner.query("select count(*) count from account", new MapHandler()).get("count").toString());
            // 法二
            return Integer.parseInt(runner.query("select count(*) from account",new ScalarHandler<>(1)).toString());
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException();

        }
    }
}

AccountServiceImpl

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
29
30
31
32
33
34
35
36
@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Resource(name="accountDao")
    private AccountDao accountDao;

    @Override
    public List<Account> findAll() {
        return accountDao.findAll();
    }

    @Override
    public Account findById(Integer id) {
        return accountDao.findById(id);
    }

    @Override
    public void saveAccount(Account account) {
        accountDao.saveAccount(account);
    }

    @Override
    public void updateAccount(Account account) {
        accountDao.updateAccount(account);
    }

    @Override
    public void deleteAccount(Integer id) {
        accountDao.deleteAccount(id);
    }

    @Override
    public int findCount() {
        return accountDao.findCount();
    }
}

spring.xml

xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="top.meethigher.demo08"></context:component-scan>

    <!--配置QueryRunner,避免多个用户使用同一个对象,我们应该将作用范围设置为多例-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <constructor-arg name="ds" ref="ds"></constructor-arg>
    </bean>
    <!--配置数据源-->
    <bean id="ds" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring?useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=Asia/Shanghai&amp;useSSL=false"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
    </bean>
</beans>

注意

  1. 注解中,引入的包的对象,需要在xml中设置(当然,也可以用更高级的注解来实现)
  2. 注解中,用到的内容,必须出现在spring容器中。不管你是通过注解,还是xml来让他保存到spring容器

5.3 新注解

基于完全注解

我们目前所用的注解,都是使用在我们自己写的类上。引入的第三方的代码,需要用xml配置。

然而,我们可以通过spring的高级注解来摆脱xml

  • @Configuration:放到类名之上
    • 作用:指定当前类是一个配置类
    • 注意:当配置类作为AnnotationConfigApplicationContext对象创建的参数时,可以不写。
  • @ComponentScan:放到类名之上
    • 作用:用于通过注解指定spring在创建容器时,要扫描的包。相当于在xml中,配置扫描包。
    • value属性:跟basePackages属性作用一样,用于指定创建容器时,要扫描的包。查看源码可知,跟basePackages属性作用一样。
  • @Bean:放到方法之上
    • 作用:用于把当前方法的返回值作为bean对象存入spring的ioc容器中
    • value属性:指定bean的id。不写时,默认值是当前方法的名称。查看源码可知,跟name属性作用一样
    • 当我们使用注解配置方法时,如果方法有参数,spring框架会去容器中查找有没有可用的bean对象,查找的方式和Autowired一样的。如果有唯一的类型匹配时,就找到了,如果有多个类型匹配时,就找bean的id跟参数名称一样的
  • @Scope:放到方法之上
    • 作用:设置当前方法返回对象的作用范围,单例or多例
    • value属性:singleton或者prototype
  • @Import:放到类名之上
    • 作用:用于导入其他的配置类。就相当于在JdbcConfig上添加@Configuration
    • value属性:要引入的配置的字节码,比如@Import(JdbcConfig.class)
  • @PropertySource:类名之上
    • 作用:用于指定properties文件的位置
    • value属性:文件的名称和路径。
      • classpath:表示类路径下。也可以写上具体路径,比如classpath:config/jdbc.properties
      • file:表示的是绝对路径

在上面的注解中,比如@ComponentScans或者@PropertySources这种,本质就是包含了多个@ComponentScan或者@PropertySource

通过上面的注解,我们就可以彻底摆脱xml。

xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="top.meethigher.demo08"></context:component-scan>

    <!--配置QueryRunner,避免多个用户使用同一个对象,我们应该将作用范围设置为多例-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <constructor-arg name="ds" ref="ds"></constructor-arg>
    </bean>
    <!--配置数据源-->
    <bean id="ds" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring?useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=Asia/Shanghai&amp;useSSL=false"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
    </bean>
</beans>

我们创建一个SpringConfiguration的配置类,对比着上面的xml,来配置我们的注解类。

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
29
30
@Configuration
@ComponentScan("top.meethigher.demo09")
public class SpringConfiguration {
    /**
     * 用于创建QueryRunner
     *
     * @param ds
     * @return
     */
    @Bean("runner")
    @Scope("prototype")
    public QueryRunner createQueryRunner(DataSource ds) {
        return new QueryRunner(ds);
    }

    @Bean("ds")
    public DataSource createDataSource() {
        ComboPooledDataSource ds = null;
        try {
            ds = new ComboPooledDataSource();
            ds.setDriverClass("com.mysql.cj.jdbc.Driver");
            ds.setJdbcUrl("jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false");
            ds.setUser("root");
            ds.setPassword("root");
        } catch (PropertyVetoException e) {
            throw new RuntimeException(e);
        }
        return ds;
    }
}

测试类

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class AnnotationClientTest09 {
    private AnnotationConfigApplicationContext  aac;
    private AccountService accountService;

    @Before
    public void setUp() throws Exception {
        aac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        accountService = aac.getBean("accountService", AccountService.class);
    }

    @After
    public void tearDown() throws Exception {
        aac.close();
    }

    @Test
    public void testFindAll() {
        List<Account> all = accountService.findAll();
        System.out.println(all);
    }
}

配置细节

当配置类作为AnnotationConfigApplicationContext对象创建的参数时,可以不写@Configuration。

如果我们将SpringConfiguration中的配置,单独提取出来,作为JdbcConfig时(下面用sc和jc来代替),有三种方式

  1. 父子关系:如果jc作为sc的下一级时,sc需要扫描jc的所在包,并且,jc必须要加@Configuration

  2. 兄弟关系:如果jc跟sc同级时,都可以不用加@Configuration,甚至不用扫描这两个类所在的包

  3. 父子关系:通过在sc加入@Import,也可以实现,跟第一种本质上是一样的

如果这块不理解的话,可以参考下面分别对应的三种方式。

第一种方式:父子关系

JdbcConfig

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
29
@Configuration
public class JdbcConfig {
    /**
     * 用于创建QueryRunner
     *
     * @param ds
     * @return
     */
    @Bean("runner")
    @Scope("prototype")
    public QueryRunner createQueryRunner(DataSource ds) {
        return new QueryRunner(ds);
    }

    @Bean("ds")
    public DataSource createDataSource() {
        ComboPooledDataSource ds = null;
        try {
            ds = new ComboPooledDataSource();
            ds.setDriverClass("com.mysql.cj.jdbc.Driver");
            ds.setJdbcUrl("jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false");
            ds.setUser("root");
            ds.setPassword("root");
        } catch (PropertyVetoException e) {
            throw new RuntimeException(e);
        }
        return ds;
    }
}

SpringConfiguration

java
1
2
3
4
5
//@Configuration 因为该类的字节码作为配置类的参数传入,所以可以省略,别的不能省略
@ComponentScan("top.meethigher.demo09")
public class SpringConfiguration {

}

测试类与上同。

第二种方式:JdbcConfig与SpringConfiguration作为兄弟关系。

如果在获取配置对象时,将所有配置类的字节码传入,那么,都可以省略@Configuration注解,甚至可以不用扫描配置类所在包了。

java
1
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class, JdbcConfig.class);

第三种方式:父子关系

SpringConfiguration

java
1
2
3
4
5
6
@Configuration
@ComponentScan("top.meethigher.demo09")
@Import(JdbcConfig.class)
public class SpringConfiguration {

}

JdbcConfig

java
1
// JdbcConfig去掉@Configuration之后的代码

提取数据到properties

首先创建properties

properties
1
2
3
4
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/spring?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
u=root
pw=root

注意,再spring配置中使用username,会默认用电脑的名称去登录,原因以及解决

在主配置类上,添加properties的类路径

java
1
2
3
4
5
@Configuration
@ComponentScan("top.meethigher.demo09")
@Import(JdbcConfig.class)
@PropertySource("classpath:jdbc.properties")
public class SpringConfiguration {}

在子配置类上,添加@Value,用的是el表达式,读取properties中的key

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
29
30
31
32
33
34
35
36
public class JdbcConfig {
    @Value("${driver}")
    private String driver;
    @Value("${url}")
    private String url;
    @Value("${u}")
    private String username;
    @Value("${pw}")
    private String password;
    /**
     * 用于创建QueryRunner
     *
     * @param ds
     * @return
     */
    @Bean("runner")
    @Scope("prototype")
    public QueryRunner createQueryRunner(DataSource ds) {
        return new QueryRunner(ds);
    }

    @Bean("ds")
    public DataSource createDataSource() {
        ComboPooledDataSource ds = null;
        try {
            ds = new ComboPooledDataSource();
            ds.setDriverClass(driver);
            ds.setJdbcUrl(url);
            ds.setUser(username);
            ds.setPassword(password);
        } catch (PropertyVetoException e) {
            throw new RuntimeException(e);
        }
        return ds;
    }
}

Qualifier解决方法重复

直接看代码吧

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class JdbcConfig {
    @Value("${driver}")
    private String driver;
    @Value("${url}")
    private String url;
    @Value("${username}")
    private String username;
    @Value("${password}")
    private String password;

    /**
     * 用于创建QueryRunner
     *
     * @param ds
     * @return
     */
    @Bean("runner")
    @Scope("prototype")
    public QueryRunner createQueryRunner(@Qualifier("ds1") DataSource ds) {
        return new QueryRunner(ds);
    }

    @Bean("ds1")
    public DataSource createDataSource() {
        ComboPooledDataSource ds = null;
        System.out.println("1执行");
        try {
            ds = new ComboPooledDataSource();
            ds.setDriverClass(driver);
            ds.setJdbcUrl(url);
            ds.setUser(username);
            ds.setPassword(password);
        } catch (PropertyVetoException e) {
            throw new RuntimeException(e);
        }
        return ds;
    }

    @Bean("ds2")
    public DataSource createDataSource1() {
        ComboPooledDataSource ds = null;
        System.out.println("2执行");
        try {
            ds = new ComboPooledDataSource();
            ds.setDriverClass(driver);
            ds.setJdbcUrl(url);
            ds.setUser(username);
            ds.setPassword(password);
        } catch (PropertyVetoException e) {
            throw new RuntimeException(e);
        }
        return ds;
    }
}

六、提取Spring静态工具类

阅读druid源码时,发现的一种写法

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
29
30
31
32
33
34
35
36
37
/**
 * @author linchtech
 * @date 2020-09-22 11:18
 **/
@Component
public class SpringContextUtils implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtils.applicationContext = applicationContext;
    }

    public static <T> T getBean(Class<T> clz) throws BeansException {
        return (T) applicationContext.getBean(clz);
    }

    public static boolean containsBean(String name) {
        return applicationContext.containsBean(name);
    }

    public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
        return applicationContext.isSingleton(name);
    }

    public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
        return applicationContext.getType(name);
    }

    public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
        return applicationContext.getAliases(name);
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
}