课程介绍

Spring是一款功能强大的框架,主要优势是简化开发框架整合

  • 简化开发:Spring框架中提供了两个大的核心技术,分别是:
    • IOC
    • AOP
      • 事务处理
    • IoC和AOP是Spring的重要知识点,事务处理可以简化项目中的事务管理,也是一大亮点
  • 框架整合:Spring整合了市面几乎所有主流框架,如Hibernate、MyBatis、Struts2等

综上,对Spring的学习,主要学习四块内容:

  1. IOC
  2. 整合Mybatis(IOC的具体应用)
  3. AOP
  4. 声明式事务(AOP的具体应用)

Spring相关概念

Spring发展史

Spring发展至今早已形成了一种开发的生态圈,提供了若干个项目,这些项目组合起来便被称为Spring全家桶


可以到Spring官方网站查看具体图标的含义,这里重点关注Spring FrameworkSpringBootSpringCloud:

  • Spring Framework:Spring框架,是Spring中最早最核心的技术,也是所有其他技术的基础。
  • SpringBoot:Spring是来简化开发,而SpringBoot是来帮助Spring在简化的基础上能更快速进行开发。
  • SpringCloud:这个是用来做分布式之微服务架构的相关开发。

除了这三个,还有还有很多其他的技术,也比较流行,如SpringDataSpringSecurity等,这里学习的Spring其实指的是Spring Framework

接下来介绍Spring Framework是怎么来的:

  • IBM(IT公司-国际商业机器公司)在1997年提出了EJB思想,早期的JAVAEE开发大都基于该思想。
  • Rod Johnson(Java和J2EE开发领域的专家)在2002年出版的Expert One-on-One J2EE Design and Development,书中有阐述在开发中使用EJB该如何做。
  • Rod Johnson在2004年出版的Expert One-on-One J2EE Development without EJB,书中提出了比EJB思想更高效的实现方案,并且在同年将方案进行了具体的落地实现,这个实现就是Spring1.0。
  • 随着时间推移,版本不断更新维护,目前最新的是Spring5
    • Spring1.0是纯配置文件开发
    • Spring2.0为了简化开发引入了注解开发,此时是配置文件加注解的开发方式
    • Spring3.0已经可以进行纯注解开发,使开发效率大幅提升,我们的课程会以注解开发为主
    • Spring4.0根据JDK的版本升级对个别API进行了调整
    • Spring5.0已经全面支持JDK8,所以学习期间最好用JDK8版本
  • 本节介绍了Spring家族与Spring的发展史,需要我们重点掌握的是:
    • Spring其实是Spring家族中的Spring Framework
    • Spring Framework是Spring家族中其他框架的底层基础,学好Spring可以为其他Spring框架的学习打好基础

Spring系统架构

  • SpringFramework是Spring其他项目的根基。
  • Spring Framework的发展也经历了很多版本的变更,每个版本都有相应的调整
  • Spring Framework的5版本目前没有最新的架构图,而最新的是4版本,所以接下来主要研究的是4的架构图
  1. 核心层
    • Core Container:核心容器,这个模块是Spring最核心的模块,其他的都需要依赖该模块
  2. AOP层
    • AOP(Aspect Oriented Programming):面向切面编程,它依赖核心层容器,目的是在不改变原有代码的前提下对其进行功能增强
    • Aspects:AOP是思想,Aspects是对AOP思想的具体实现
  3. 数据层
    • Data Access:数据访问,Spring全家桶中有对数据访问的具体实现技术
    • Data Integration:数据集成,Spring支持整合其他的数据层解决方案,比如Mybatis
    • Transactions:事务,Spring中事务管理是Spring AOP的一个具体实现,也是后期学习的重点内容
  4. Web层
    • 这一层的内容将在SpringMVC框架具体学习
  5. Test层
    • Spring主要整合了Junit来完成单元测试和集成测试

Spring学习路线

  1. Spring的IOC/DI
  2. Spring的AOP
  3. AOP的具体应用,事务管理
  4. IOC/DI的具体应用,整合Mybatis

Spring核心概念

这部分主要包括IOC/DIIOC容器Bean

目前的问题

分析代码在编写中的问题:

  1. 业务层需要调用数据层的方法,就需要在业务层new数据层的对象
  2. 如果数据层的实现类发生变化,那么业务层的代码也需要跟着改变,发生变更后,都需要进行编译打包和重部署

    编写代码存在的问题:耦合度偏高,一个类发生变化,其他类也要跟着修改

1
2
3
4
5
6
7
8
public class BookServlet extends BookServlet {
//这个地方是写死了的
private BookDao bookDao = new BookDaoImpl();

public void save() {
bookDao.save();
}
}

假如BookDaoImpl2比BookDaoImpl1好用,我们想更换数据层实现类,那么业务层的代码也得跟着变,耦合度太高了

1
2
3
4
5
public class BookDaoImpl1 implements BookDao {
public void save() {
System.out.println("book dao save ... 1");
}
}

1
2
3
4
5
public class BookDaoImpl2 implements BookDao {
public void save() {
System.out.println("book dao save ... 2");
}
}

  • 主要耦合度高的原因是在业务层中直接new了数据层的实现类,如果我们不new对象,只声明一下,数据层跟业务层不就解耦了吗?
    • 答案显然不行,因为不new对象此时BookDao是一个空引用,强行调用save()方法会报空指针异常,所以现在主要问题是如何做到我们不创建对象,但运行时数据层对象被创建,且被赋值给业务层,这该如何实现?
  • 针对该问题,Spring提出的解决方案:使用对象时,程序中不再主动new对象,而让Spring为我们提供并管理这个对象

IOC/DI

  • IOC(Inversion of Control)控制反转

    • 什么是控制反转?

      • 控制反转是一种更宽泛的设计模式或架构原则,能够用来降低代码代码之间的耦合度,符合依赖倒置原则。
      • 控制反转的核心是:将对象的创建权交出去,将对象和对象之间关系的管理权交出去,有第三方容器来负责创建与维护
    • Spring和IOC之间的关系是什么呢?

      • Spring技术对IOC思想进行了实现
      • Spring提供了一个容器,称为IOC容器,用来充当IOC思想中的”外部”
      • IOC思想中的别人[外部]指的就是Spring的IOC容器
    • IOC容器的作用以及内部存放的是什么?
      • IOC容器负责对象的创建、初始化等一系列工作,其中包含了数据层和业务层的类对象
      • 被创建或被管理的对象在IOC容器中统称为Bean
    • 当IOC容器中创建好service和dao对象后,程序就能正确执行了吗?
      • 不行,因为service运行需要依赖dao对象
      • IOC容器中虽然有service和dao对象,但此时service对象和dao对象是两个单独的类,没有任何关系,需要把dao对象交给service,也就是说要绑定service和dao对象之间的关系才行
      • 像这种在容器中建立对象与对象之间的绑定关系就要用到DI(Dependency Injection)依赖注入.
  • DI(Dependency Injection)依赖注入
    • 什么是依赖注入?
      • 在容器中建立bean与bean之间的依赖关系的整个过程,称为依赖注入
        • 业务层要用数据层的类对象,以前是自己new
        • 现在自己不new了,靠别人[外部其实指的就是IOC容器]来给注入进来
    • IOC容器中哪些bean之间要建立依赖关系呢?
      • 这个需要程序员根据业务需求提前建立好关系,如业务层需要依赖数据层,service就要和dao建立依赖关系
    • 我们会发现Spring容器中IOC和AOP这两个概念的最终目标就是:充分解耦,具体实现靠:
      • 使用IOC容器管理bean(IOC)
      • 在IOC容器内将有依赖关系的bean进行关系绑定(DI)
      • 最终结果为:使用对象时不仅可以直接从IOC容器中获取,并且获取到的bean已经绑定了所有的依赖关系。

核心概念小结

  1. 什么是IOC/DI思想?
    • IOC:控制反转,控制反转的是对象的创建权,由Spring为我们创建并管理对象
    • DI:依赖注入,绑定对象与对象之间的依赖关系,即给对象的属性赋值
  2. 什么是IOC容器?
    • Spring创建了一个容器用来存放所创建的对象,这个容器就叫IOC容器
  3. 什么是Bean?
    • 容器中所存放的一个个对象就叫Bean

入门案例

IOC入门案例

需求:将BookServiceImpl和BookDaoImpl交给Spring管理,并从容器中获取对应的bean对象调用其方法

  1. 创建Maven项目
  2. 添加Spring需要的依赖,仅仅使用IOC功能则引入Spring-context这个jar包即可
    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
  3. 创建BookDao,BookDaoImpl,BookService和BookServiceImpl四个类
    1
    2
    3
    public interface BookDao {
    public void save();
    }
    1
    2
    3
    4
    5
    public class BookDaoImpl implements BookDao {
    public void save() {
    System.out.println("book dao save ...");
    }
    }
    1
    2
    3
    public interface BookService {
    public void save();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    public class BookServiceImpl implements BookService {
    // 这里不考虑依赖注入,仅仅学习IOC相关内容,所以手动new了一个BookDaoImpl对象
    private BookDao bookDao = new BookDaoImpl();
    public void save() {
    System.out.println("book service save ...");
    bookDao.save();
    }
    }
  4. 创建Spring配置文件

    在resource目录下新建->XML配置文件->Spring配置

  5. 在配置文件中完成Bean的配置
    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"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--
    id属性标示给bean起名字
    class属性写类的全路径,得是具体的实现类而不是接口,要靠这个造对象
    还可以写name属性,作用和id一样,可以用来给对象起别名
    -->
    <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl"/>
    <bean id="bookService" class="com.blog.service.impl.BookServiceImpl"/>
    </beans>

    Bean定义时,id属性和class属性是必须的,其他属性都是可选的,比如name属性
    Bean的id属性在同一个配置文件不能重复

  6. 获取IOC容器
    使用Spring提供的接口完成IoC容器的获取
    1
    2
    3
    4
    5
    6
    7
    public class App {
    // ClassPathXmlApplicationContext是使用配置文件的方式获取IOC容器
    public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

    }
    }
  7. 从容器中获取对象并调用方法
    1
    2
    3
    4
    5
    6
    7
    public class App {
    public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    BookService bookService = (BookService) context.getBean("bookService");
    bookService.save();
    }
    }
  8. 运行程序
    测试结果:

    book service save …
    book dao save …

对Ioc的入门案例已经完成,但在上文BookServiceImpl中的bookDao对象是手动new的,耦合度依旧较高,解决这个问题便需要下面的DI(依赖注入)

DI入门案例

需求:基于IOC入门案例,建立好service和dao之间的依赖关系,使用DI完成Dao层的注入

  1. 删除service层new对象的行为
    1
    2
    3
    4
    5
    6
    7
    8
    public class BookServiceImpl implements BookService {
    //private BookDao bookDao = new BookDaoImpl();
    private BookDao bookDao;
    public void save() {
    System.out.println("book service save ...");
    bookDao.save();
    }
    }
  2. 在业务层提供需要注入的属性的set方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class BookServiceImpl implements BookService {
    private BookDao bookDao;

    public void save() {
    System.out.println("book service save ...");
    bookDao.save();
    }

    public void setBookDao(BookDao bookDao) {
    this.bookDao = bookDao;
    // 添加该语句检验set方法是否被调用
    System.out.println("set方法被调用啦");
    }
    }
  3. 在配置文件中使用<property>标签完成依赖注入
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <?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="bookDao" class="com.blog.dao.impl.BookDaoImpl"/>

    <!--主要变化在这里-->
    <bean id="bookService" class="com.blog.service.impl.BookServiceImpl">
    <!--配置server与dao的关系-->
    <!--
    property标签表示配置当前bean的属性
    name属性表示配置哪一个具体的属性(这里是配置bookService的bookDao属性)
    ref属性表示参照哪一个bean(需要当前配置文件中配置的bean的id)
    -->
    <property name="bookDao" ref="bookDao"></property>
    </bean>
    </beans>

    在property标签中使用了两个bookDao

    • name=”bookDao”,这里是Service层中的属性名,用是让Spring的IOC容器在获取到名称后,将首字母大写,前面加set找对应的setBookDao()方法进行对象注入
    • ref=”bookDao”,这里是Dao层中的bean的id,用来参照当前配置文件中配置的bean的id,将其注入到Service层的bookDao属性中
  4. 运行程序
    测试结果:

    set方法被调用啦
    book service save …
    book dao save …

IOC相关内容

Bean基础配置

  • 接下来主要说明Bean基础配置Bean别名配置以及Bean的作用范围配置

    id和class

    bean标签的功能、使用方式以及id和class属性的作用,通过下面这张图简单描述

id和class属性都是必须的,id属性是bean的标识符,class属性是bean的类全路径

name

name属性的作用和id属性一样,可以用来给bean起别名,name属性可以省略

  1. 配置别名
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl"/>
    <!--别名可以有多个,使用逗号,分号,空格进行分隔都可以-->
    <bean id="bookService" name="service1 service2 service3" class="com.blog.service.impl.BookServiceImpl">
    <property name="bookDao" ref="bookDao"></property>
    </bean>
    </beans>
  2. 根据别名来获取Bean对象
    1
    2
    3
    4
    5
    6
    7
    8
    public class App {
    public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    //此处根据bean标签的id属性和name属性的任意一个值来获取bean对象
    BookService bookService = (BookService) context.getBean("service2");
    bookService.save();
    }
    }
  3. 运行程序
    测试结果:

    set方法被调用啦
    book service save …
    book dao save …

  • Bean依赖注入的ref属性的值也可以是name里的别名,但是建议使用Id值来注入
  • 在调用getBean方法时,传入一个不存在的名称,就会报错NoSuchBeanDefinitionException,此时就要检查是哪一边的名称写错了

scope

关于bean的作用范围是bean属性配置的一个重点内容。
bean的scope有两个取值:

  • singleton:单例(默认)
  • prototype:非单例
  • 验证对象是否为单例

    验证思路:获取同一个Bean两次,打印这两次的地址,如果地址相同则说明是单例

    代码实现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class App {
    public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    //我这里使用了别名,其实还是同一个bean
    BookService bookService2 = (BookService) context.getBean("service2");
    BookService bookService3 = (BookService) context.getBean("service3");
    System.out.println(bookService2);
    System.out.println(bookService3);
    }
    }
    输出结果发现地址值一样,则证明默认创建的为单例对象

    com.blog.service.impl.BookServiceImpl@25bbe1b6
    com.blog.service.impl.BookServiceImpl@25bbe1b6

配置Bean为非单例

正常情况下,Spring为我们创建的对象都是单例的,如果想要创建非单例的对象,需要我们在配置文件中配置scope属性

  • 在Spring的配置文件中,修改<bean>的scope属性
    1
    2
    3
    <bean id="bookService" name="service1 service2 service3" class="com.blog.service.impl.BookServiceImpl" scope="prototype">
    <property name="bookDao" ref="bookDao"></property>
    </bean>
    此时重新验证,便会发现地址值不一致。

Scope总结

  • 为什么Spring创建Bean默认为单例?
    • Bean默认是单例意味着Spring的IOC容器只会存有该类的一个对象
    • 对象只有一个有效避免了对象的频繁创建和销毁,达到了Bean对象的复用,性能更高
  • Bean对象是单例的,是否会产生线程安全问题?
    • 如果对象是有状态对象,即该对象有成员变量可以用来存储数据的,因为所有请求线程共用一个bean对象,所以会存在线程安全问题。
    • 如果对象是无状态对象,即该对象没有成员变量没有进行数据存储的,因方法中的局部变量在方法调用完成后会被销毁,所以不会存在线程安全问题。
  • 哪些bean对象适合交给容器进行管理?
    • 表现层对象(controller)
    • 业务层对象(service)
    • 数据层对象(dao)
    • 工具对象(util)
  • 哪些bean对象不适合交给容器进行管理?
    • 封装实例的域对象(domain,pojo),因为会引发线程安全问题,所以不适合。

      Bean的实例化

  • 我们了解了通过配置文件来实例化Bean对象,但是容器是如何为我们创建对象的呢?
    • 这需要研究一下Bean的实例化过程,这部分主要解决两部分内容
      • Bean是如何创建的
      • 实例化Bean的三种方式,构造方法,静态工厂和实例工厂

        需要明确的事,Bean本质上就是一个对象,对象在new的时候是使用构造方法来创建的,所以创建Bean也是使用构造方法完成的

        构造方法实例化

  • 在之前的Dao实现类中为无参构造添加一句话,方便观察结果。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class BookDaoImpl implements BookDao {
    public void save() {
    System.out.println("book dao save ...");
    }

    public BookDaoImpl() {
    System.out.println("book dao constructor is running ...");
    }
    }
    输出结果:

    book dao constructor is running …
    com.blog.service.impl.BookServiceImpl@25bbe1b6

  • 将构造器私有化后,继续进行测试
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class BookDaoImpl implements BookDao {
    public void save() {
    System.out.println("book dao save ...");
    }

    private BookDaoImpl() {
    System.out.println("book dao constructor is running ...");
    }
    }

运行程序,发现将构造器私有化,仍然能访问到构造方法来实例化Bean,这说明Spring底层通过反射调用构造方法来实例化Bean对象

book dao constructor is running …
com.blog.service.impl.BookServiceImpl@25bbe1b6

静态工厂实例化

  • 创建一个工厂类,在工厂类中提供一个静态方法,用来创建Bean对象

    1
    2
    3
    4
    5
    public class BookDaoFactory {
    public static BookDao getBookDao() {
    return new BookDaoImpl();
    }
    }
  • 修改App运行类,通过工厂来获得对象

    1
    2
    3
    4
    5
    6
    7
    public class App {
    public static void main(String[] args) {
    //通过静态工厂创建对象
    BookDao bookDao = BookDaoFactory.getBookDaoImpl();
    bookDao.save();
    }
    }

    运行结果:

    book dao save …

  • 这样只是通过静态工厂创建对象,但没有将对象交给Spring容器进行管理,如何交给Spring容器管理呢?

具体实现步骤如下:

  1. 在Spring配置文件中修改BookDao的Bean
    1
    <bean id="bookDao" class="com.blog.factory.BookDaoFactory" factory-method="getBookDaoImpl"/>

    将class改为工厂类的全类名
    添加factory-method属性,指定工厂创建类的方法名

  2. 在App运行类,像之前一样从IOC容器中获得对象
    1
    2
    3
    4
    5
    6
    7
    public class App {
    public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    BookDao bookDao = (BookDao) context.getBean("bookDao");
    bookDao.save();
    }
    }
  3. 这样就能正常运行了,运行结果和之前一样,反而更加麻烦了,哪这样做的意义是什么呢?

    book dao save …

  4. 在工厂的静态方法中,除了创建对象我们还可以在这里进行其他的业务操作
    1
    2
    3
    4
    5
    6
    7
    public class BookDaoFactory {
    public static BookDao getBookDaoImpl() {
    System.out.println("book dao factory setup ...");//模拟必要的业务操作
    //这里可以加一大堆业务逻辑
    return new BookDaoImpl();
    }
    }

这是静态工厂实例化,这种方式一般用来兼容早期老系统,目前使用较少,了解为主

实例工厂和FactoryBean

修改BookDaoFactory类,这里和静态工厂的工厂类不同的是此处不是静态方法,而是普通方法

1
2
3
4
5
6
7
8
public class BookDaoFactory {
//唯一的区别就是去掉的static
public BookDao getBookDaoImpl() {
System.out.println("book dao factory setup ...");//模拟必要的业务操作
//这里还可以加一大堆业务逻辑
return new BookDaoImpl();
}
}

修改App运行类,通过工厂来获得对象
1
2
3
4
5
6
7
8
public class App {
public static void main(String[] args) {
//因为不是静态方法了,所以需要实例工厂创建对象
BookDaoFactory bookDaoFactory = new BookDaoFactory();
BookDao bookDao = bookDaoFactory.getBookDaoImpl();
bookDao.save();
}
}

运行结果:

book dao factory setup … 这个是模拟业务逻辑的输出,无视掉就行
book dao save …

将实例工厂交给Spring管理:

  1. 在Spring配置文件中修改bookDao的bean
    1
    2
    <bean id="bookDaoFactory" class="com.blog.factory.BookDaoFactory"/>
    <bean id="bookDao" factory-bean="bookDaoFactory" factory-method="getBookDaoImpl"/>

    实例化工厂运行的顺序是:

    • 创建实例化工厂对象,对应的是第一行配置
    • 调用对象中的方法来创建bean,对应的是第二行配置
      • factory-bean:工厂的实例对象
      • factory-method:工厂对象中的具体创建对象的方法名
  • 配置后便可以在App中从Ioc容器中获取Bean对象
    1
    2
    3
    4
    5
    6
    7
    public class App {
    public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    BookDao bookDao = (BookDao) context.getBean("bookDao");
    bookDao.save();
    }
    }

实例工厂的配置过程还是比较复杂的,且耦合度较高,所以Spring为了简化这种配置方式,提供了一种叫FactoryBean的接口来简化开发。

FactoryBean具体使用步骤:

  1. 创建一个BookDaoFactoryBean类,实现FactoryBean接口,并重写其中的方法
    该接口是一个泛型接口,泛型参数为该工厂创建的Bean对象的类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class BookDaoFactoryBean implements FactoryBean<BookDao> {

    public BookDao getObject() throws Exception {
    return new BookDaoImpl();
    }

    public Class<?> getObjectType() {
    return BookDao.class;
    }
    }
  2. 在Spring的配置文件修改bookDao的bean,修改class属性为工厂的全类名
    1
    <bean id="bookDao" class="com.blog.factory.BookDaoFactoryBean"></bean>
  3. App运行类不用做任何修改,直接运行,结果如下

    book dao save …

这种方式在Spring整合其他框架时会用到,所以需要理解并掌握。

  • 查看源码会发现,FactoryBean接口提供了三个方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 该方法用于创建Bean对象并返回
    T getObject() throws Exception;

    // 该方法用于返回Bean对象的类型
    Class<?> getObjectType();

    // 该方法用于设置Bean对象是否为单例,默认为单例
    default boolean isSingleton() {
    return true;
    }
  • 其中方法一和方法二必须重写,方法三因为有默认值,所以可以不重写

Bean的实例化小结

  • Bean如何被创建
    • Spring通过反射机制调用构造方法,因此即使构造方法为私有的,Spring依旧能正常创建Bean
  • Spring的Ioc实例化对象的三种方式:
    • 无参构造方法(常用)
    • 静态工厂(了解)
    • 实例工厂(了解)
      • FactoryBean(实用)

无参构造方法系统默认会提供,但一旦重写了构造方法,系统就不会再提供默认的无参构造方法,因此在使用时需要注意,如果重写了构造方法,一定要把无参构造方法也写出来。

Bean的生命周期

对于Bean的生命周期,主要围绕Bean生命周期控制来学习

  • 什么是Bean的生命周期?
    • Bean对象从创建到销毁的整个过程
  • Bean生命周期控制是什么?
    • Bean创建后到销毁前做的事情
  • 我们研究的是如何在Bean创建之后和销毁之前将我们需要执行的操作添加进去

生命周期设置

具体的两个控制阶段:

  • Bean创建之后,如果想要添加内容,比如用来初始化需要用到的资源
  • Bean销毁之前,如果想要添加内容,比如用来释放资源
  1. 创建初始化和销毁方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class BookDaoImpl implements BookDao {
    public void save() {
    System.out.println("book dao save ...");
    }

    // 在类中添加两个方法,方法名不唯一
    public void init() {
    System.out.println("init ... ");
    }
    public void destroy() {
    System.out.println("destroy ... ");
    }
    }
  2. 在配置文件中添加初始化和销毁方法

    1
    <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl" init-method="init" destroy-method="destroy"></bean>

    init-method:指定初始化方法
    destroy-method:指定销毁方法

  3. 运行程序

    init …
    book dao save …

从运行结果来看,init已经成功执行,但destroy方法没有执行,这是为什么?

  • 因为Spring的IoC容器运行在JVM中,运行main方法后,开始启动JVM,Spring加载配置文件生成IOC容器,从容器获取bean对象,然后调用方法。main方法执行结束后,JVM就退出了,但此时IOC中的Bean还没销毁,所以就还没调用对应的destroy方法

知道出现问题的原因,该如何解决这个问题呢?接着往下看

Close关闭容器

  • ApplicationContext中没有close方法,他的子类提供了close方法,所以需要将ApplicationContext更换为ClassPathXmlApplicationContext,然后调用close()方法关闭容器
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class App {
    public static void main(String[] args) {
    //ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    BookDao bookDao = (BookDao) context.getBean("bookDao");
    bookDao.save();
    context.close();
    }
    }
  • 此时运行程序就能看到destroy正常输出

    init …
    book dao save …
    destroy …

注册钩子关闭容器

  • 在容器未关闭之前,提前设置好回调函数,让JVM在退出之前回调此函数来关闭容器
  • 调用context的registerShutdownHook()方法
    1
    2
    3
    4
    5
    6
    7
    8
    public class App {
    public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    BookDao bookDao = (BookDao) context.getBean("bookDao");
    bookDao.save();
    context.registerShutdownHook();
    }
    }

    注意:registerShutdownHook和close方法一样,在ApplicationContext中也没有

  • 运行后,查询打印结果

    init …
    book dao save …
    destroy …

  • close和registerShutdownHook该选哪个?

    • 相同点: 这两种都能用来关闭容器
    • 不同点: close()是在调用的时候关闭容器,registerShutdownHook()是在JVM退出前调用关闭容器。
    • 那么registerShutdownHook()方法可以在任意位置调用,下面的代码中将其放在了第二行,仍能正常输出,但要是将其换成close()方法,则会报错BeanFactory not initialized or already closedclosed
      1
      2
      3
      4
      5
      6
      7
      8
      public class App {
      public static void main(String[] args) {
      ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
      context.registerShutdownHook();
      BookDao bookDao = (BookDao) context.getBean("bookDao");
      bookDao.save();
      }
      }
  • 开发中用哪个?
    • 答案是两个都不用
    • 该方式添加初始化和销毁方法即需要更改配置文件还需要编码,实现步骤较复杂,不建议使用
    • Spring提供了两个接口来实现生命周期的控制,好处是不需要再配置init-methoddestroy-method

Spring提供的接口

需求:修改BookDaoImpl类,完成初始化和销毁方法的添加

修改BookServiceImpl类,添加两个接口InitializingBean,DisposableBean并实现接口中的两个方法afterPropertiesSetdestroy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BookServiceImpl implements BookService, InitializingBean, DisposableBean {
private BookDao bookDao;

public void save() {
System.out.println("book service save ...");
bookDao.save();
}

public void setBookDao(BookDao bookDao) {
System.out.println("set ... ");
this.bookDao = bookDao;
}
// 销毁方法,在容器关闭时调用
public void destroy() throws Exception {
System.out.println("service destroy ... ");
}
// 在属性注入完成后调用,即初始化方法
public void afterPropertiesSet() throws Exception {
System.out.println("service init ... ");
}
}

1
2
3
<bean id="bookService" class="com.blog.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"></property>
</bean>
  • init 是BookDao的初始化方法输出的
  • service init 是BookServiceImpl的初始化方法输出的
  • book dao save …是BookDao的保存方法输出的
  • service destroy 是BookServiceImpl的销毁方法输出的
  • destroy 是BookDao的销毁方法输出的

init …
service init …
book dao save …
service destroy …
destroy …

  • 小细节
    • 对于InitializingBean接口中的afterPropertiesSet方法,翻译过来为属性设置之后。
    • 对于BookServiceImpl来说,bookDao是它的一个属性,所以会先注入bookDao属性再初始化BookServiceImpl

没有调用bookService为什么输出结果中还会有service呢?
解惑:Ioc容器中所有的单例Bean的初始化和销毁都会在容器启动和关闭时运行,所以Service也会运行

IOC相关内容小结

  • Bean的基础配置
    • <id>标签和<class>标签必须添加且不能为空,<id>标签的值不能重复,<class>标签的值可以重复
    • <name>标签可以用来取别名,多个别名时使用逗号,分号或空格进行分隔
    • <scope>标签用来设置Bean的作用范围,默认值为单例,即获取同一个<id>的Bean对象,获取的对象都是同一个(即使类路径相同只有id不同,这两个也是独立的Bean对象)
  • Bean的实例化
    • Bean对象是Spring通过暴力反射为我们创建的,我们如果重写了构造方法,一定要把无参构造方法也写出来。
    • Spring的Ioc实例化对象的三种方式:
      • 无参构造方法(常用)
      • 静态工厂(了解)
      • 实例工厂(了解)
        • FactoryBean(实用)
  • Bean的生命周期
    • Spring为Bean的生命周期控制提供了两种方式:
      • 配置文件中添加init-methoddestroy-method属性
      • 类实现InitializingBeanDisposableBean接口
    • Bean的生命周期过程:
      • 初始化容器
        1. 创建对象
        2. 执行构造方法
        3. 执行属性注入(set …)
        4. 执行Bean的初始化方法(service init …)
      • 使用Bean
        • 执行Bean的业务方法(book dao save …)
      • 销毁容器
        • 执行Bean的销毁方法(service destroy …)
    • 关闭容器的两种方法:
      • 通过配置文件添加初始化和销毁方法需要设置关闭容器,否则destroy()方法不会正常执行
      • ConfigurableApplicationContext是ApplicationContext的子类,子类才有下面两种方法:
        • close()
        • registerShutdownHook()

DI相关内容

DI(依赖注入)通常用来为对象添加属性

  • 向类中传递数据的方式有哪些?
    • 构造方法
    • Set方法
  • Spring为我们提供了两种属性注入方式:
    • 构造器注入
      • 简单类型
      • 引用类型
    • Setter方法注入
      • 简单类型
      • 引用类型

Set注入

  • 前面就用到了set注入,快速回顾一下
  • 在Bean中定义引用类型的属性,并提供set方法
    1
    2
    3
    4
    5
    6
    public class BookServiceImpl implements BookService {
    private BookDao bookDao;
    public void setBookDao(BookDao bookDao) {
    this.bookDao = bookDao;
    }
    }
  • 在配置文件中使用property标签为属性赋值
    1
    2
    3
    <bean id="bookService" class="com.blog.service.impl.BookServiceImpl">
    <property name="bookDao" ref="bookDao"></property>
    </bean>

    回顾两个bookDao的含义:

    • name后的作用是让Spring的Ioc容器获取名称后,根据获取到的名称推测对应的set方法,使用此set方法为属性赋值
    • ref后的作用是让Spring在Ioc容器找到id为bookDao的Bean,并赋给bookDao属性

注入引用数据类型

需求:使用依赖注入为BookServiceImpl类中的BookDao属性赋值

1
2
3
public interface BookDao {
public void save();
}
1
2
3
4
5
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
}
1
2
3
public interface BookService {
public void save();
}
1
2
3
4
5
6
7
8
9
10
public class BookServiceImpl implements BookService {
private BookDao bookDao;
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}
  • 配置文件如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <?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="bookDao" class="com.blog.dao.impl.BookDaoImpl"></bean>

    <bean id="bookService" class="com.blog.service.impl.BookServiceImpl">
    <property name="bookDao" ref="bookDao"></property>
    </bean>
    </beans>
  • 运行App类,加载Ioc容器并获取service对象
    1
    2
    3
    4
    5
    6
    7
    public class App {
    public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    BookService bookService = (BookService) context.getBean("bookService");
    bookService.save();
    }
    }

    注入简单数据类型

    需求:为BookDaoImpl注入一些简单数据。

    思考:
  • 引用类型使用的是<property name="" ref=""/>,简单数据类型还是使用ref吗?
  • ref是指向Spring的IOC容器中的另一个bean对象的,对于简单数据类型,没有对应的bean对象,该如何配置呢?

    • 使用value来配置<property name="" value=""/>
  • 步骤一:定义属性并提供set方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class BookDaoImpl implements BookDao {
    private String dataBaseName;
    private int connectionCount;

    public void setDataBaseName(String dataBaseName) {
    this.dataBaseName = dataBaseName;
    }

    public void setConnectionCount(int connectionCount) {
    this.connectionCount = connectionCount;
    }

    public void save() {
    System.out.println("book dao save ..." + dataBaseName + "," + connectionCount);
    }
    }
  • 步骤二:在配置文件中注入配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl">
    <property name="dataBaseName" value="mysql"></property>
    <property name="connectionCount" value="100"></property>
    </bean>
    </beans>

    value:后面跟的是简单数据类型,对于参数类型,Spring在注入的时候会自动转换,但是不能写一个错误的类型,例如connectionCountint类型,你却给他传一个abc,这样的话,spring在将abc转换成int类型的时候就会报错。

  • 步骤三:运行程序,查看结果

    book dao save …mysql,100

构造器注入

环境准备

  • 修改BookDao、BookDaoImpl、UserDao、UserDaoImpl、BookService和BookServiceImpl类

    1
    2
    3
    public interface BookDao {
    public void save();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class BookDaoImpl implements BookDao {

    private String databaseName;
    private int connectionNum;

    public void save() {
    System.out.println("book dao save ...");
    }
    }
    1
    2
    3
    public interface UserDao {
    public void save();
    }
    1
    2
    3
    4
    5
    public class UserDaoImpl implements UserDao {
    public void save() {
    System.out.println("user dao save ...");
    }
    }
    1
    2
    3
    public interface BookService {
    public void save();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class BookServiceImpl implements BookService{
    private BookDao bookDao;

    public void setBookDao(BookDao bookDao) {
    this.bookDao = bookDao;
    }

    public void save() {
    System.out.println("book service save ...");
    bookDao.save();
    }
    }
  • 配置文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl"/>
    <bean id="bookService" class="com.blog.service.impl.BookServiceImpl">
    <property name="bookDao" ref="bookDao"/>
    </bean>
    </beans>
  • 运行类
    1
    2
    3
    4
    5
    6
    7
    public class App {
    public static void main( String[] args ) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    BookService bookService = (BookService) ctx.getBean("bookService");
    bookService.save();
    }
    }

构造器注入引用类型

需求:将BookServiceImpl类中的bookDao修改成使用构造器的方式注入。

  1. 将bookDao的setter方法删除掉
  2. 添加带有bookDao参数的构造方法
  3. 在applicationContext.xml中配置
  • 步骤一:删除setter方法并提供构造方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class BookServiceImpl implements BookService{
    private BookDao bookDao;

    public BookServiceImpl(BookDao bookDao) {
    this.bookDao = bookDao;
    }

    public void save() {
    System.out.println("book service save ...");
    bookDao.save();
    }
    }
  • 步骤二:配置文件中进行配置构造方式注入
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl"/>
    <bean id="bookService" class="com.blog.service.impl.BookServiceImpl">
    <constructor-arg name="bookDao" ref="bookDao"/>
    </bean>
    </beans>

    说明:在标签<constructor-arg>

    • name属性对应的值为构造函数中方法形参的参数名,必须要保持一致。
    • ref属性指向的是spring的IOC容器中其他bean对象。
  • 步骤三:运行程序,查看结果
    运行结果:

    book service save …
    book dao save …

构造器注入多个参数

需求:在BookDaoImpl中,使用构造函数注入databaseNameconnectionNum两个参数。
参考引用数据类型的注入,我们可以推出具体的步骤为:

  1. 提供一个包含这两个参数的构造方法
  2. 在applicationContext.xml中进行注入配置
  • 步骤一:添加多个简单属性并提供构造方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class BookDaoImpl implements BookDao {

    private String databaseName;
    private int connectionNum;

    public BookDaoImpl(String databaseName, int connectionNum) {
    this.databaseName = databaseName;
    this.connectionNum = connectionNum;
    }

    public void save() {
    System.out.println("book dao save ..." + databaseName + "," + connectionNum);
    }
    }
  • 步骤二:修改该配置文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl">
    <constructor-arg name="databaseName" value="mysql"></constructor-arg>
    <constructor-arg name="connectionNum" value="100"></constructor-arg>
    </bean>
    </beans>
  • 步骤三:运行程序:

    book service save …
    book dao save …mysql,100

存在的问题

  • <constructor-arg>标签内的name,必须与构造函数中的参数名一致,这两块存在紧耦合。
  • 解决该问题,需要提前说明一点,即这个参数名发生变化的情况并不多,所以上面的还是比较主流的配置方式,下面介绍的配置方式了解为主。
  • 方式一:删除name属性,添加type属性,根据类型注入
    • 这种方式可以解决构造函数形参名发生变化带来的耦合问题
    • 但是如果构造方法参数中有类型相同的参数,这种方式就不太好实现了
      1
      2
      3
      4
      <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl">
      <constructor-arg type="java.lang.String" value="mysql"></constructor-arg>
      <constructor-arg type="int" value="9421"></constructor-arg>
      </bean>
  • 方式二:删除type属性,添加index属性,根据索引下标注入,下标从0开始
    • 该方式能解决参数类型重复和参数名称问题
    • 但这又要求参数的顺序不能变,带来了另外的耦合问题
      1
      2
      3
      4
      <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl">
      <constructor-arg index="0" value="mysql"></constructor-arg>
      <constructor-arg index="1" value="9421"></constructor-arg>
      </bean>

如何选择使用哪种注入方式?

  1. 强制依赖时,使用构造器注入,使用set注入有概率不进行注入导致null对象出现
    • 强制依赖指对象创建时必须要注入参数
  2. 可选依赖使用set注入,灵活性强
    • 可选依赖指对象创建时,不一定需要注入参数
  3. Spring倡导使用构造器注入,第三方框架也大多采用构造器注入,因为这种方法较严谨
  4. 如果有必要,可以同时使用两种注入方法,强制依赖时使用构造器注入,可选依赖时使用set注入
  5. 实际开发需要根据实际情况来,如果是第三方程序没有提供set方法则只能使用构造器注入
  6. 自己开发模块时推荐使用set注入

一句话小结:自己开发时推荐优先使用set注入,使用第三方框架时,根据第三方程序提供的构造函数和set方法,进行选择

构造器注入小结

  • Set注入
    • 简单类型
      1
      2
      3
      <bean ...>
      <property name="" value=""/>
      </bean>
    • 引用类型
      1
      2
      3
      <bean ...>
      <property name="" ref=""/>
      </bean>
  • 构造器注入
    • 简单类型
      1
      2
      3
      <bean ...>
      <constructor-arg name="" index="" type="" value=""/>
      </bean>
    • 引用类型
      1
      2
      3
      <bean ...>
      <constructor-arg name="" index="" type="" ref=""/>
      </bean>
    • 根据类型注入
      1
      2
      3
      4
      <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl">
      <constructor-arg type="java.lang.String" value="mysql"></constructor-arg>
      <constructor-arg type="int" value="9421"></constructor-arg>
      </bean>
    • 根据索引下标注入
      1
      2
      3
      4
      <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl">
      <constructor-arg index="0" value="mysql"></constructor-arg>
      <constructor-arg index="1" value="9421"></constructor-arg>
      </bean>
  • 依赖注入的方式选择
    • 自己开发模块时推荐使用set注入
    • 第三方技术根据实际情况选择

自动配置

  • 无论构造器注入还是set注入,都需要修改配置文件,相对还是很麻烦,对此Spring为我们提供了自动配置功能
  • Ioc容器根据Bean所依赖的资源在容器中自动查找并注入到Bean的过程称为自动装配
  • 自动装配的方式:
    • 按类型
    • 按名称
    • 按构造方法
    • 不启用自动装配(忽略)

环境准备

修改BookDaoBookDaoImplBookServiceBookServiceImpl

1
2
3
public interface BookDao {
public void save();
}
1
2
3
4
5
6
7
8
9
public class BookDaoImpl implements BookDao {

private String databaseName;
private int connectionNum;

public void save() {
System.out.println("book dao save ...");
}
}
1
2
3
public interface BookService {
public void save();
}
1
2
3
4
5
6
7
8
9
10
11
12
public class BookServiceImpl implements BookService{
private BookDao bookDao;

public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}

public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}

配置文件:
1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.blog.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
</bean>
</beans>

运行类:
1
2
3
4
5
6
7
public class App {
public static void main( String[] args ) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BookService bookService = (BookService) ctx.getBean("bookService");
bookService.save();
}
}

启用自动装配

  • 启用自动装配仅仅需要修改配置文件即可
    • 将手动装配的<property>标签删除
    • <bean>标签中添加autowire属性
    • 通过更改autowire属性为byName或者byType来控制根据名称装配还是根据类型装配
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

      <!-- <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl"/>-->
      <!-- 既然是按类型注入了,那么id写不写都无所谓了-->
      <bean class="com.blog.dao.impl.BookDaoImpl"/>
      <bean id="bookService" class="com.blog.service.impl.BookServiceImpl" autowire="byType"/>
      </beans>
  • 再次运行程序,结果如下,说明自动装配成功

    book service save …
    book dao save …

注意事项:

  • 在Spring中,自动装配默认使用根据类型set注入,因此注入属性的类中对应的set方法必须存在
  • 被注入的对象需要在Spring的Ioc容器管理
  • 根据类型注入,如果Ioc中找到多个类就会报NoUniqueBeanDefinitionException异常

自动装配小结

  • 如果按照名称去找对应的bean对象,找不到则注入Null
  • 当某一个类型在IOC容器中有多个对象,按照名称注入只找其指定名称对应的bean对象,不会报错
  • 两种方式介绍完后,以后用的更多的是根据类型注入。
  • 对于依赖注入,需要注意一些其他的配置特征:
    1. 自动装配用于引用类型依赖注入,不能对简单类型进行操作
    2. 使用按类型装配时(byType)必须保障容器中相同类型的bean唯一,推荐使用
    3. 使用按名称装配时(byName)必须保障容器中具有指定名称的bean,因变量名与配置耦合,不推荐使用
    4. 自动装配优先级低于setter注入与构造器注入,同时出现时自动装配配置失效

      集合注入

      集合中既可以装简单数据类型也可以装引用数据类型,对于集合,在Spring中该如何注入呢?
  • 常见的集合类型
    • 数组
    • List
    • Set
    • Map
    • Properties

环境准备

  • 修改BookDaoImpl类
    1
    2
    3
    public interface BookDao {
    public void save();
    }
    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
    public class BookDaoImpl implements BookDao {

    private int[] array;

    private List<String> list;

    private Set<String> set;

    private Map<String,String> map;

    private Properties properties;

    public void save() {
    System.out.println("book dao save ...");

    System.out.println("遍历数组:" + Arrays.toString(array));

    System.out.println("遍历List" + list);

    System.out.println("遍历Set" + set);

    System.out.println("遍历Map" + map);

    System.out.println("遍历Properties" + properties);
    }

    public void setArray(int[] array) {
    this.array = array;
    }

    public void setList(List<String> list) {
    this.list = list;
    }

    public void setSet(Set<String> set) {
    this.set = set;
    }

    public void setMap(Map<String, String> map) {
    this.map = map;
    }

    public void setProperties(Properties properties) {
    this.properties = properties;
    }
    }
  • 修改配置文件
    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 http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl"/>
    </beans>
  • 修改App运行类
    1
    2
    3
    4
    5
    6
    7
    public class App {
    public static void main( String[] args ) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    BookDao bookDao = (BookDao) ctx.getBean("bookDao");
    bookDao.save();
    }
    }
  • 接下来学习集合注入均在上面环境基础上完成,都是在bookDao的<bean>标签上使用<property进行注入

注入数组类型

1
2
3
4
5
6
7
<property name="array">
<array>
<value>100</value>
<value>200</value>
<value>300</value>
</array>
</property>

注入List类型

1
2
3
4
5
6
7
<property name="list">
<list>
<value>张三</value>
<value>ABC</value>
<value>123</value>
</list>
</property>

注入Set类型

1
2
3
4
5
6
7
8
<property name="set">
<set>
<value>100</value>
<value>200</value>
<value>ABC</value>
<value>ABC</value>
</set>
</property>

注入Map类型

1
2
3
4
5
6
7
<property name="map">
<map>
<entry key="探路者" value="马文"/>
<entry key="次元游记兵" value="恶灵"/>
<entry key="易位窃贼" value="罗芭"/>
</map>
</property>

注入Properties类型

1
2
3
4
5
6
7
<property name="properties">
<props>
<prop key="暴雷">沃尔特·菲茨罗伊</prop>
<prop key="寻血猎犬">布洛特·亨德尔</prop>
<prop key="命脉">阿杰·切</prop>
</props>
</property>

说明:

  • property标签使用set方式注入,构造方式注入constructor-arg标签内部也可以写<array><List>SetMapProps标签
  • List底层也是使用数组实现的,所以listarray标签可以混用
  • 集合中添加引用类型,只需要把<value>标签改为<ref>标签即可,但这种方式使用较少(引用类型默认为单例)

DI小结

  • 配置文件注入
    • 构造器注入(<constructor-arg>)
      • 根据类型注入
      • 根据名称注入
      • 根据下标注入
    • setter注入(property)
      • 基础数据类型:<value>
      • 引用数据类型:<ref>
  • 注入方式选择
    • 自己开发模块时推荐使用set注入
    • 第三方技术根据实际情况选择
  • 自动装配
    • 在配置文件中添加autowire属性,值为byType或byName
      • byType:根据类型自动装配
      • byName:根据名称自动装配
  • 集合注入
    • 数组:<array>
    • List:<list>
    • Set:<set>
    • Map:<map>
    • Properties:<props>

Ioc/DI管理第三方Bean

  • 前面的内容都是管理自己写的类,现在学习如何管理第三方的类

案例:数据源对象管理

该案例使用Druid数据源来配置学习

环境准备

  1. 创建Maven项目
  2. 添加需要的依赖
    1
    2
    3
    4
    5
    6
    7
    <dependencies>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
    </dependencies>
  3. resources目录下新建spring的配置文件
    1
    2
    3
    4
    5
    6
    <?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">

    </beans>
  4. 编写运行类

    1
    2
    3
    4
    5
    public class App {
    public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    }
    }

    实现Druid管理

    需求分析:使用Spring的Ioc容器管理Druid连接池对象

    1. 使用第三方技术,需要先在pom.xml文件添加依赖
    2. 在配置文件中将第三方类做成一个bean,让Ioc容器管理
    3. 数据库连接四要素:驱动、连接、用户名和密码,需要为这些属性赋值
    4. 从Ioc容器中获取bean对象,打印到控制台
  5. 导入Druid依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
    </dependency>
  6. 配置第三方bean

此时第三方bean使用set注入还是构造器注入便需要根据第三方类为我们提供的内容来判断
通过查看源码发现DruidDataSource只提供了两个构造器,显然不能使用构造方法注入

1
2
public DruidDataSource()
public DruidDataSource(boolean fairLock)

确定使用set注入后,在配置文件中添加DruidDataSource的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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">
<!--管理DruidDataSource对象-->
<bean class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:13306/spring_db"/>
<property name="username" value="root"/>
<property name="password" value="yourPassword"/>
</bean>
</beans>

  1. 从Ioc容器中获取bean对象
    1
    2
    3
    4
    5
    6
    7
    public class App {
    public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    DruidDataSource dataSource = context.getBean(DruidDataSource.class);
    System.out.println(dataSource);
    }
    }
  2. 运行程序
    打印以下结果说明第三方Bean成功交给Ioc容器管理

    {
    CreateTime:”2022-09-01 10:15:19”,
    ActiveCount:0,
    PoolingCount:0,
    CreateCount:0,
    DestroyCount:0,
    CloseCount:0,
    ConnectCount:0,
    Connections:[
    ]
    }

加载properties文件

  • 我们完成Druid数据源配置后又发现一些问题:
    • 数据源中用到一些固定的常量(如数据库连接四要素),而将这些值直接放到Spring的配置文件中不利于后期维护,因此选择将这些固定的常量提取到外部的properties配置文件中
    • 提取到properties文件的数据如何在Spring框架中读取这是接下来要解决的问题

Druid属性优化

需求:将数据库连接四要素数据提取到properties配置文件,Spring加载文件中的配置信息并根据信息完成属性注入

  1. 新建properties文件
  2. 将数据库连接需要的信息配置到文件中
  3. 在Spring配置文件中加载properties文件
  4. 使用加载到的值实现属性注入
  1. 准备properties配置文件
    新建jdbc.properties文件并添加对应的属性键值对
    1
    2
    3
    4
    jdbc.driverClass=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:13306/spring_db
    jdbc.username=root
    jdbc.password=password
  2. 开启context命名空间
    修改Spring配置文件,开启context命名空间
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <?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
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    ">
    </beans>
  3. 加载properties文件
    在配置文件中使用以下标签来加载文件
    1
    <context:property-placeholder location="jdbc.properties"/>
  4. 完成属性注入
    使用${}占位符来引用properties文件中的值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?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
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    ">
    <context:property-placeholder location="jdbc.properties"/>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${jdbc.driverClass}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
    </bean>
    </beans>
  • 此时就已经将properties文件中的值成功注入到了DruidDataSource对象中

注意事项:

  • 问题一:键值对的key为username等系统内置属性引发问题
    • 在properties中配置键值对的时候,如果key设置为username时,在运行后控制台打印该值发现打印的不是root,而是自己电脑的用户名
    • 原因:<context:property-placeholder/>标签会加载系统的环境变量,且环境变量会优先加载,下面的代码可以输出系统环境变量
      1
      2
      3
      4
      public static void main(String[] args) throws Exception{
      Map<String, String> env = System.getenv();
      System.out.println(env);
      }
    • 打印的结果是USERNAME=XXX[自己电脑用户名]
    • 解决方案:将ystem-properties-mode设置为NEVER,表示不加载系统环境变量,这样就可以解决上面的问题了,当然还有一个解决方案就是给属性加上前缀,避免使用username作为属性的key。
      1
      <context:property-placeholder location="jdbc.properties" system-properties-mode="NEVER"/>
  • 问题二:多个properties文件加载,该如何配置?
    • 修改applicationContext.xml
      1
      2
      3
      4
      5
      6
      7
      8
      <!--方式一 -->
      <context:property-placeholder location="jdbc.properties,jdbc2.properties" system-properties-mode="NEVER"/>
      <!--方式二-->
      <context:property-placeholder location="*.properties" system-properties-mode="NEVER"/>
      <!--方式三 -->
      <context:property-placeholder location="classpath:*.properties" system-properties-mode="NEVER"/>
      <!--方式四-->
      <context:property-placeholder location="classpath*:*.properties" system-properties-mode="NEVER"/>
      • 说明:
        • 方式一:可以实现,如果配置文件多的话,每个都需要配置
        • 方式二:*.properties代表所有以properties结尾的文件都会被加载,可以解决方式一的问题,但是不标准
        • 方式三:标准的写法,classpath:代表的是从根路径下开始查找,但是只能查询当前项目的根路径
        • 方式四:不仅可以加载当前项目还可以加载当前项目所依赖的所有项目的根路径下的properties配置文件

      小结

  • 如何开启context命名空间
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    ">
  • 如何加载properties配置文件

    1
    <context:property-placeholder location="" system-properties-mode="NEVER"/>
  • 如何在applicationContext.xml引入properties配置文件中的值

    • 使用占位符:${key}

核心容器

此处学习的核心容器,可以简单的理解为ApplicationContext,即前面App应用类创建的对象,接下来从结果问题来入手学习该容器的内容:

  • 如何创建容器
  • 创建好容器如何获得Bean对象
  • 容器类的层次结构是什么
  • BeanFactory是什么

环境准备

  • 创建Maven项目
  • 添加Spring的依赖
    1
    2
    3
    4
    5
    6
    7
    <dependencies>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
    </dependencies>
  • 新建Spring配置文件
    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
    http://www.springframework.org/schema/beans/spring-beans.xsd
    ">
    <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl"/>
    </beans>
  • 添加BookDao和BookDaoImpl类
    1
    2
    3
    public interface BookDao {
    public void save();
    }
    1
    2
    3
    4
    5
    public class BookDaoImpl implements BookDao {
    public void save() {
    System.out.println("book dao save ..." );
    }
    }
  • 创建运行类
    1
    2
    3
    4
    5
    6
    7
    public class App {
    public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    BookDao bookDao = (BookDao) ctx.getBean("bookDao");
    bookDao.save();
    }
    }

    容器

    容器的创建方式

  • 案例中创建ApplicationContext方式如下
  • 这种方式翻译为:类路径下的XML配置文件
    1
    ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
  • Spring还提供了一种绝对路径的创建方式
    1
    ApplicationContext ctx = new FileSystemXmlApplicationContext("D:\xxx/xxx\applicationContext.xml");
  • 这种方式能实现,但项目位置一旦发生变化代码就要跟着改,增加耦合度,所以不推荐使用

    获取Bean的三种方式

  • 方式一:获得bean后强转
  • 该方式存在的问题在于每次获取Bean后需要进行强转操作
    1
    BookDao bookDao = (BookDao) ctx.getBean("bookDao");
  • 方式二:指定获取Bean的类型
  • 该方式在调用方法时需要指定Bean的类型,避免了强转操作
    1
    BookDao bookDao = ctx.getBean("bookDao", BookDao.class);
  • 方式三:直接传Bean的类型
    1
    BookDao bookDao = ctx.getBean(BookDao.class);
  • 该方式类似按类型注入。必须确保Ioc容器中该类型对应的Bean对象只能有一个

BeanFactory

容器的最上级父接口就是BeanFactory,使用BeanFactory也可以创建Ioc容器

1
2
3
4
5
6
7
8
public class AppForBeanFactory {
public static void main(String[] args) {
Resource resources = new ClassPathResource("applicationContext.xml");
BeanFactory bf = new XmlBeanFactory(resources);
BookDao bookDao = bf.getBean(BookDao.class);
bookDao.save();
}
}

为了更好的看出BeanFactoryApplicationContext之间的区别,在BookDaoImpl添加如下构造函数

1
2
3
4
5
6
7
8
public class BookDaoImpl implements BookDao {
public BookDaoImpl() {
System.out.println("constructor");
}
public void save() {
System.out.println("book dao save ..." );
}
}

如果不去获取bean对象,打印会发现:

  • BeanFactory是延迟加载,只有在获取bean对象的时候才会去创建
  • ApplicationContext是立即加载,容器加载的时候就会创建bean对象
  • ApplicationContext要想成为延迟加载,只需要将lazy-init设为true
    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 http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl" lazy-init="true"/>
    </beans>

核心容器总结

容器相关

  • BeanFactory是IoC容器的顶层接口,初始化BeanFactory对象时,Bean对象不会被加载,只有获取Bean对象时才会创建
  • ApplicationContext接口是Spring容器的核心接口,初始化时bean立即加载
  • ApplicationContext接口提供基础的bean操作相关方法,通过其他接口扩展其功能
  • ApplicationContext接口常用初始化类
    • ClassPathXmlApplicationContext(常用)
    • FileSystemXmlApplicationContext

      bean相关

      依赖注入

Ioc/DI注解开发

Spring的Ioc/DI对应的使用配置文件开发使用起来还是比较复杂的,所以Spring为我们提供了注解开发,Spring对注解支持的版本历程:

  • 2.0版开始支持注解
  • 2.5版注解功能趋于完善
  • 3.0版支持纯注解开发

关于注解开发,主要学习两部分内容注解开发定义Bean纯注解开发
注解开发定义bean用的是2.5版提供的注解,纯注解开发用的是3.0版提供的注解。

环境准备

  • 创建一个Maven项目
  • 添加Spring的依赖
    1
    2
    3
    4
    5
    6
    7
    <dependencies>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
    </dependencies>
  • 新建Spring配置文件
    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 http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl"/>
    </beans>
  • 添加BookDao和BookDaoImpl类
    1
    2
    3
    public interface BookDao {
    public void save();
    }
    1
    2
    3
    4
    5
    public class BookDaoImpl implements BookDao {
    public void save() {
    System.out.println("book dao save ..." );
    }
    }
  • 创建运行类
    1
    2
    3
    4
    5
    6
    7
    public class App {
    public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    BookDao bookDao = (BookDao) ctx.getBean("bookDao");
    bookDao.save();
    }
    }

    注解开发定义Bean

  1. 删除原有的XML配置
    将配置文件中的<bean>标签删除掉
    1
    <bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl"/>
  2. 在Dao上添加注解
    在BookDaoImpl类上添加@Component注解
    1
    2
    3
    4
    5
    6
    @Component("bookDao")
    public class BookDaoImpl implements BookDao {
    public void save() {
    System.out.println("book dao save ...");
    }
    }

    注意:@Component注解不可以添加在接口上,因为接口是无法创建对象的。

  3. 配置Spring的注解包扫描
    仅仅在类上添加@Component注解是不够的,还需要配置Spring的注解包扫描,否则Spring容器不会扫描到该类,Spring没有扫描到自然不会添加到Ioc容器中管理。
    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
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    ">
    <context:component-scan base-package="com.blog"/>
    </beans>

    说明:component-scan

    • component:组件,Spring将管理的bean视作自己的一个组件
    • scan:扫描
    • base-package指定Spring框架扫描的包路径,它会扫描指定包及其子包中的所有类上的注解。
    • 包路径越多如:com.blog.dao.impl,扫描的范围越小速度越快
    • 包路径越少如:com.blog,扫描的范围越大速度越慢
    • 一般扫描到项目的组织名称即Maven的groupId下如:com.blog即可。
  4. 运行程序

    book dao save …

  5. service上添加注解
    在BookServiceImpl类上也添加@Component交给Spring框架管理
    1
    2
    3
    4
    5
    6
    @Component
    public class BookServiceImpl implements BookService {
    public void save() {
    System.out.println("book service save ...");
    }
    }
  6. 运行程序
    在App类中从IOC容器中获取BookServiceImpl对应的bean对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class App {
    public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    //按照名称获取bean
    BookDao bookDao = (BookDao) context.getBean("bookDao");
    //按照类型获取bean
    BookService bookService = context.getBean(BookService.class);
    bookDao.save();
    bookService.save();
    }
    }
    执行结果:

    book dao save …
    book service save …

    说明:

    • BookServiceImpl类没有起名称,所以在App中是按照类型来获取bean对象
    • @Component注解如果不起名称,会有一个默认值就是当前类名首字母小写,所以也可以按照名称获取,如
      1
      BookService bookService = (BookService) context.getBean("bookServiceImpl");
    • 对于@Component注解,还衍生出了其他三个注解@Controller@Service@Repository
    • 通过查看源码会发现:这三个注解和@Component注解的作用是一样的,为什么要衍生出这三个呢?
      • 这是方便我们后期在编写类的时候能很好的区分出这个类是属于表现层、业务层还是数据层的类。

    纯注解开发

    上面已经可以使用注解来配置Bean,但仍然需要使用配置文件并添加包扫描,Spring在3.0版本推出了纯注解开发,使用Java类来代替配置文件。

    纯注解开发包括:

    • 使用@Configuration注解,代替配置文件
    • 使用@ComponentScan注解,代替包扫描
    • 使用@Component注解,代替Bean定义

    实现步骤

  7. 创建配置类
    创建SpringConfig专门用作配置类
    1
    2
    public class SpringConfig {
    }
  8. 标识该类为配置类
    在配置类上添加@Configuration注解,标识为配置类,用来替代applicationContext.xml
    1
    2
    3
    @Configuration
    public class SpringConfig {
    }
  9. 使用包扫描注解
    原本配置文件中就需要添加包扫描,现在使用注解@ComponentScan替换<context:component-scan base-package=""/>
    1
    2
    3
    4
    @Configuration
    @ComponentScan("com.blog")
    public class SpringConfig {
    }
  10. 创建运行类并执行
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class AppForAnnotation {
    public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
    BookDao bookDao = (BookDao) context.getBean("bookDao");
    bookDao.save();
    BookService bookService = context.getBean(BookService.class);
    bookService.save();
    }
    }
  11. 执行结果
    可以看到两个对象可以被成功获取

    book dao save …
    book service save …

小结

纯注解开发的主要内容包括:

  • @Component注解可以用来定义bean
  • @Configuration注解用于设定当前类为配置类
  • @ComponentScan注解用于设定扫描路径,该注解只能添加一次,多个数据需要使用数组格式
    1
    @ComponentScan({com.blog.service","com.blog.dao"})
  • 读取Spring核心配置文件初始化容器对象切换为Java配置类初始化容器对象
    1
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
名称@Configuration
类型类注解
位置类定义上方
作用设置该类为spring配置类
属性value(默认):定义bean的id
名称@ComponentScan
类型类注解
位置类定义上方
作用设置spring配置类扫描路径,用于加载使用注解格式定义的bean
属性value(默认):扫描路径,此路径可以逐层向下扫描
  • 记住@Component@Controller@Service@Repository这四个注解
  • applicationContext.xml中<context:component-san/>的作用是指定扫描包路径,注解为@ComponentScan
  • @Configuration标识该类为配置类,使用类替换applicationContext.xml文件
  • ClassPathXmlApplicationContext是加载XML配置文件
  • AnnotationConfigApplicationContext是加载配置类

注解开发Bean的作用范围和生命周期

  • 使用注解能够实现Bean的管理,根据前面学习的内容,将Bean作用范围(Scope)Bean生命周期(init和destroy)也替换为注解实现

Bean的作用范围

  • 前面提到Spring为我们创建的Bean默认是单例,使用注解创建也不例外,如果想创建多例Bean,只需要在类上添加@scope注解即可
    1
    2
    3
    4
    5
    6
    7
    @Component("bookDao")
    @Scope("prototype")
    public class BookDaoImpl implements BookDao {
    public void save() {
    System.out.println("book dao save ...");
    }
    }
    知识点:@scope
名称@Scope
类型类注解
位置类定义上方
作用设置该类创建对象的作用范围,可用于设置创建出的bean是否为单例对象
属性value(默认):定义bean作用范围,默认值singleton(单例),可选值prototype(非单例)

Bean的生命周期

  • 在BookDaoImpl中添加两个方法,init和destroy(方法名任意)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Component("bookDao")
    public class BookDaoImpl implements BookDao {

    public BookDaoImpl() {
    System.out.println("construct ... ");
    }

    public void save() {
    System.out.println("book dao save ...");
    }

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

    public void destroy() {
    System.out.println("destroy ... ");
    }
    }
  • 使用注解来标注初始化方法和销毁方法只需要在对应的方法上添加@PostConstruct@PreDestroy注解
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Component("bookDao")
    public class BookDaoImpl implements BookDao {

    public BookDaoImpl() {
    System.out.println("construct ... ");
    }

    public void save() {
    System.out.println("book dao save ...");
    }

    @PostConstruct // 在构造方法之后执行,替换 init-method
    public void init() {
    System.out.println("init ... ");
    }

    @PreDestroy // 在销毁方法之前执行,替换 destroy-method
    public void destroy() {
    System.out.println("destroy ... ");
    }
    }

    需要注意的是destroy依旧只有在容器关闭时才执行,所以需要手动调用close方法registerShutdownHook方法来关闭容器

JDK8版本以上,如果找不到@PostConstruct@PreDestroy注解,需要导入下面的jar包

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

找不到的原因是,从JDK9以后jdk中的javax.annotation包被移除了,这两个注解刚好就在这个包中。

名称@PostConstruct
类型方法注解
位置方法上
作用设置该方法为初始化方法
属性
名称@PreDestroy
类型方法注解
位置方法上
作用设置该方法为销毁方法
属性

小结

配置文件中的bean标签中的

  • id对应@Component("")@Controller("")@Service("")@Repository("")
  • scope对应@scope()
  • init-method对应@PostConstruct
  • destroy-method对应@PreDestroy

注解开发依赖注入

Spring为了使用注解简化开发,并没有提供构造函数注入、setter注入对应的注解,只提供了自动装配的注解实现。

环境准备

  • 创建Maven项目
  • 添加Spring依赖
  • 添加配置类SpringConfig
    1
    2
    3
    4
    @Configuration
    @ComponentScan("com.blog")
    public class SpringConfig {
    }
  • 添加BookDao、BookDaoImpl、BookService、BookServiceImpl类
    1
    2
    3
    public interface BookDao {
    public void save();
    }
    1
    2
    3
    4
    5
    6
    @Repository
    public class BookDaoImpl implements BookDao {
    public void save() {
    System.out.println("book dao save ..." );
    }
    }
    1
    2
    3
    public interface BookService {
    public void save();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Service
    public class BookServiceImpl implements BookService {
    private BookDao bookDao;
    public void setBookDao(BookDao bookDao) {
    this.bookDao = bookDao;
    }
    public void save() {
    System.out.println("book service save ...");
    bookDao.save();
    }
    }
  • 创建运行类App
    1
    2
    3
    4
    5
    6
    7
    public class App {
    public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    BookService bookService = ctx.getBean(BookService.class);
    bookService.save();
    }
    }

此时这些环境准备好后,直接运行依旧会爆出异常,因为还没有提供配置为BookService中的BookDao赋值,所以BookDao对象为null

按类型注入(注解)

  • 在BookServiceImpl类的bookDao属性上添加@Autowired注解
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Service
    public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;

    // public void setBookDao(BookDao bookDao) {
    // this.bookDao = bookDao;
    // }

    public void save() {
    System.out.println("book service save ...");
    bookDao.save();
    }
    }

    注意:

    • @Autowired可以写在属性上,也可也写在setter方法上,最简单的处理方式是写在属性上并将setter方法删除掉
    • 为什么setter方法可以删除呢?
      • 自动装配基于反射设计创建对象并通过暴力反射为私有属性进行设值
      • 普通反射只能获取public修饰的内容,暴力反射除了获取public修饰的内容还可以获取private修改的内容,所以此处无需提供setter方法
  • Autowired按照类型注入,当对应BookDao接口有多个实现类时,比如添加BookDaoImpl2
    1
    2
    3
    4
    5
    6
    @Repository
    public class BookDaoImpl2 implements BookDao {
    public void save() {
    System.out.println("book dao save ...2");
    }
    }
  • 此时再次运行App,就会报错NoUniqueBeanDefinitionException
  • 根据之前的学习,此时应当按照名称注入
  • 先给两个Dao类分别起个名称
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Repository("bookDao")
    public class BookDaoImpl implements BookDao {
    public void save() {
    System.out.println("book dao save ..." );
    }
    }
    @Repository("bookDao2")
    public class BookDaoImpl2 implements BookDao {
    public void save() {
    System.out.println("book dao save ...2" );
    }
    }
  • 突然发现此时就能注入成功,这是为什么?

关键点:@Autowired的装配规则

  1. 先按类型(byType)装配
    Spring 会查找容器中所有实现了 BookDao 接口的 Bean。
  2. 如果找到多个候选 Bean(类型相同)
    此时 Spring 会进入第二步——按名称(byName)匹配
    • Spring 会拿被注入属性的名称(这里是 userServiceImplB
    • 去和 Bean 的 id / name 进行匹配。
  3. 匹配成功则注入成功
    如果有一个 Bean 的名字和属性名一致,就会被选中注入。
  4. 如果还是没有匹配成功
    则会抛出异常:NoUniqueBeanDefinitionException

按名称注入(注解)

当根据类型在容器中找到多个bean,注入参数的属性名又和容器中bean的名称不一致,这个时候该如何解决,就需要使用到@Qualifier来指定注入哪个名称的bean对象。@Qualifier注解后的值就是需要注入的bean的名称。

1
2
3
4
5
6
7
8
9
10
11
@Service
public class BookServiceImpl implements BookService {
@Autowired
@Qualifier("bookDao1")
private BookDao bookDao;

public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}

注意:@Qualifier不能独立使用,必须和@Autowired一起使用

简单类型注入

  • Spring提供的自动装配只能对引用类型进行装配,不过Spring也为我们提供了简单类型装配的注解——@Value
  • 使用@Value注解为name属性赋值

    1
    2
    3
    4
    5
    6
    7
    8
    @Repository
    public class BookDaoImpl implements BookDao {
    @Value("Stephen")
    private String name;
    public void save() {
    System.out.println("book dao save ..." + name);
    }
    }
  • 看到这个注解的使用方式,可以会有疑问,都直接在属性上标了那为什么不直接赋值呢?因为可以在注解中使用占位符读取外部的properties配置文件

读取properties配置文件

@Value一般会被用在从properties配置文件中读取内容进行使用,具体如何实现?

  • 步骤一:准备一个properties文件
    1
    name=Stephen
  • 步骤二:使用注解加载properties配置文件
    在配置类上添加@PropertySource注解

    1
    2
    3
    4
    5
    @Configuration
    @ComponentScan("com.blog")
    @PropertySource("jdbc.properties")
    public class SpringConfig {
    }
  • 步骤三:使用@Value读取配置文件中的内容

    1
    2
    3
    4
    5
    6
    7
    8
    @Repository
    public class BookDaoImpl implements BookDao {
    @Value("${name}")
    private String name;
    public void save() {
    System.out.println("book dao save ..." + name);
    }
    }
  • 步骤四:运行程序
    运行App类,查看运行结果,说明配置文件中的内容已经被加载

book service save …
book dao save …Stephen

注意:

  • 如果读取的properties配置文件有多个,可以使用@PropertySource来指定多个
    1
    @PropertySource({"jdbc.properties","xxx.properties"})
    @PropertySource注解属性中不支持使用通配符*,运行会报错
    1
    @PropertySource({"*.properties"})
    @PropertySource注解属性中可以把classpath:加上,代表从当前项目的根路径找文件
    1
    @PropertySource({"classpath:jdbc.properties"})

名称@Autowired
类型属性注解 或 方法注解(了解) 或 方法形参注解(了解)
位置属性定义上方 或 标准set方法上方 或 类set方法上方 或 方法形参前面
作用为引用类型属性设置值
属性required:true/false,定义该属性是否允许为null
名称@Qualifier
类型属性注解 或 方法注解(了解)
位置属性定义上方 或 标准set方法上方 或 类set方法上方
作用为引用类型属性指定注入的beanId
属性value(默认):设置注入的beanId
名称@Value
类型属性注解 或 方法注解(了解)
位置属性定义上方 或 标准set方法上方 或 类set方法上方
作用为 基本数据类型 或 字符串类型 属性设置值
属性value(默认):要注入的属性值
名称@PropertySource
类型类注解
位置类定义上方
作用加载properties文件中的属性值
属性value(默认):设置加载的properties文件对应的文件名或文件名组成的数组

IOC/DI使用注解管理第三方Bean

  • 自己开发的类可以通过添加注解快速管理,但第三方类我们无法在类中添加注解,此时该如何解决?此时Spring为我们提供了一个全新的注解——@Bean

    环境准备

  • 创建Maven项目
  • 添加Spring依赖
    1
    2
    3
    4
    5
    6
    7
    <dependencies>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
    </dependencies>
  • 添加配置类SpringConfig
    1
    2
    3
    @Configuration
    public class SpringConfig {
    }
  • 添加BookDao、BookDaoImpl类
    1
    2
    3
    public interface BookDao {
    public void save();
    }
    1
    2
    3
    4
    5
    6
    @Repository
    public class BookDaoImpl implements BookDao {
    public void save() {
    System.out.println("book dao save ..." );
    }
    }
  • 创建运行类App

    1
    2
    3
    4
    5
    public class App {
    public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    }
    }

    管理第三方Bean

    在上述搭建的环境中,完成对Druid数据源的管理

  • 步骤一:导入对应的jar包

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
    </dependency>
  • 步骤二:在配置类中添加一个方法
    注意该方法的返回值就是要创建的Bean对象类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Configuration
    public class SpringConfig {
    public DataSource dataSource() {
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost:13306/spring_db");
    dataSource.setUsername("root");
    dataSource.setPassword("PASSWORD");
    return dataSource;
    }
    }
  • 步骤三:在方法上添加@Bean注解
    @Bean注解的作用是将方法的返回值作为一个Spring管理的bean对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Configuration
    public class SpringConfig {
    @Bean
    public DataSource dataSource() {
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost:13306/spring_db");
    dataSource.setUsername("root");
    dataSource.setPassword("PASSWORD");
    return dataSource;
    }
    }
  • 步骤四:从IOC容器中获取对象并打印
    1
    2
    3
    4
    5
    6
    7
    public class App {
    public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    DataSource dataSource = ctx.getBean(DataSource.class);
    System.out.println(dataSource);
    }
    }

输出如下

{
CreateTime:”2022-09-02 10:36:33”,
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}

  • 至此使用@Bean来管理第三方bean的案例就已经完成。
  • 如果有多个bean要被Spring管理,直接在配置类中多写几个方法,方法上添加@Bean注解即可。

引入外部配置类

如果把所有的第三方bean都配置到Spring的配置类SpringConfig中,虽然可以,但是不利于代码阅读和分类管理,所有我们就想能不能按照类别将这些bean配置到不同的配置类中?

那么对于数据源的bean,我们可以把它的配置单独放倒一个JdbcConfig类中

1
2
3
4
5
6
7
8
9
10
11
public class JdbcConfig {
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:13306/spring_db");
dataSource.setUsername("root");
dataSource.setPassword("PASSWORD");
return dataSource;
}
}

那现在又有了一个新问题,这些配置类如何能被Spring配置类加载到,并创建DataSource对象在IOC容器中?
针对这个问题,有两个解决方案:

使用包扫描引入

  • 步骤一:在Spring的配置类上添加包扫描
    注意要将JdbcConfig类放在包扫描的地址下
    1
    2
    3
    4
    @Configuration
    @ComponentScan("com.blog.config")
    public class SpringConfig {
    }
  • 步骤二:在JdbcConfig上添加@Configuration注解
    JdbcConfig类要放入到com.blog.config包下,这样才能被Spring的配置类扫描到
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Configuration
    public class JdbcConfig {
    @Bean
    public DataSource dataSource(){
    DruidDataSource ds = new DruidDataSource();
    ds.setDriverClassName("com.mysql.jdbc.Driver");
    ds.setUrl("jdbc:mysql://localhost:3306/spring_db");
    ds.setUsername("root");
    ds.setPassword("root");
    return ds;
    }
    }
  • 步骤三:运行程序
    仍然可以获取到bean对象并输出到控制台

    {
    CreateTime:”2022-09-02 10:52:50”,
    ActiveCount:0,
    PoolingCount:0,
    CreateCount:0,
    DestroyCount:0,
    CloseCount:0,
    ConnectCount:0,
    Connections:[
    ]
    }

这种方式虽然能够扫描到,但是不能很快的知晓都引入了哪些配置类(因为把包下的所有配置类都扫描了),所以这种方式不推荐使用。

使用@Import引入

方案一实现起来有点小复杂,Spring早就想到了这一点,于是又给我们提供了第二种方案。
这种方案可以不用加@Configuration注解,但是必须在Spring配置类上使用@Import注解手动引入需要加载的配置类

  • 步骤一:去除JdbcConfig类上的注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class JdbcConfig {
    @Bean
    public DataSource dataSource() {
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost:13306/spring_db");
    dataSource.setUsername("root");
    dataSource.setPassword("PASSWORD");
    return dataSource;
    }
    }
  • 步骤二:在Spring配置类中引入

    1
    2
    3
    4
    @Configuration
    @Import(JdbcConfig.class)
    public class SpringConfig {
    }

注意:

  • 扫描注解可以移除
  • @Import参数需要的是一个数组,可以引入多个配置类。
  • @Import注解在配置类中只能写一次
  • 步骤三:运行程序
    依然能获取到bean对象并打印控制台

    {
    CreateTime:”2022-09-02 11:02:12”,
    ActiveCount:0,
    PoolingCount:0,
    CreateCount:0,
    DestroyCount:0,
    CloseCount:0,
    ConnectCount:0,
    Connections:[
    ]
    }

名称@Bean
类型方法注解
位置方法定义上方
作用设置该方法的返回值作为spring管理的bean
属性value(默认):定义bean的id
名称@Import
类型类注解
位置类定义上方
作用导入配置类
属性value(默认):定义导入的配置类类名, 当配置类有多个时使用数组格式一次性导入多个配置类

为第三方Bean注入资源

前面学习到使用@Value搭配${}来注入简单数据,以及使用@Autowired来注入引用数据,对于第三方bean,如何为它注入属性呢?

简单数据类型

  • 注入简单数据类型的方法跟自己定义的bean一样,使用@Value搭配${}来注入即可
    1
    2
    3
    4
    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:13306/spring_db
    jdbc.username=root
    jdbc.password=PASSWORD.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @PropertySource("jdbc.properties")
    public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource() {
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setDriverClassName(driver);
    dataSource.setUrl(url);
    dataSource.setUsername(username);
    dataSource.setPassword(password);
    return dataSource;
    }
    }

    引用数据类型

    需求:假设在构建DataSource对象时,需要用到BookDao对象,该如何注入BookDao对象呢?

  • 对第三方注入引用数据类型的对象则更加方便,只需要为方法添加需要注入的Bean作为参数即可,容器会根据类型自动装配
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Bean
    public DataSource dataSource(BookDao bookDao) {
    bookDao.save();
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setDriverClassName(driver);
    dataSource.setUrl(url);
    dataSource.setUsername(username);
    dataSource.setPassword(password);
    return dataSource;
    }

    注解开发总结

    Spring整合

    Spring整合MyBatis

    准备环境

  • 步骤一:准备数据库表
    MyBatis是用来操作数据库表的,所以先来创建库和表
1
2
3
4
5
6
7
8
9
10
11
create database spring_db character set utf8;
use spring_db;
create table tbl_account(
id int primary key auto_increment,
name varchar(35),
money double
);
INSERT INTO tbl_account(`name`,money) VALUES
('Tom',2800),
('Jerry',3000),
('Jhon',3100);
  • 步骤二:创建项目导入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <dependencies>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.46</version>
    </dependency>
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.6</version>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
    </dependency>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
    </dependencies>
  • 步骤三:
    根据表创建模型类

    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
    public class Account {
    private Integer id;
    private String name;
    private Double money;

    public Account() {
    }

    public Account(Integer id, String name, double money) {
    this.id = id;
    this.name = name;
    this.money = 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 double getMoney() {
    return money;
    }

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

    @Override
    public String toString() {
    return "Account{" +
    "id=" + id +
    ", name='" + name + '\'' +
    ", money=" + money +
    '}';
    }
    }
  • 步骤四:创建Dao接口(在之前是Mapper接口,且要配置一个对应的xml文件,不过这里没涉及到复杂的sql语句,所以没配置xml文件,采用注解开发)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface AccountDao {

@Insert("insert into tbl_account(name, money) VALUES (#{name}, #{money})")
void save(Account account);

@Delete("delete from tbl_account where id = #{id}")
void delete(Integer id);

@Update("update tbl_account set `name` = #{name}, money = #{money}")
void update(Account account);

@Select("select * from tbl_account")
List<Account> findAll();

@Select("select * from tbl_account where id = #{id}")
Account findById(Integer id);
}
  • 步骤五:创建Service接口和实现类

  • AccountService

1
2
3
4
5
6
7
8
9
10
11
public interface AccountService {
void save(Account account);

void delete(Integer id);

void update(Account account);

List<Account> findAll();

Account findById(Integer id);
}
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
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;

public void save(Account account) {
accountDao.save(account);
}

public void delete(Integer id) {
accountDao.delete(id);
}

public void update(Account account) {
accountDao.update(account);
}

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

public Account findById(Integer id) {
return accountDao.findById(id);
}
}
  • 步骤六:添加jdbc.properties文件

    1
    2
    3
    4
    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:13306/spring_db
    jdbc.username=root
    jdbc.password=PASSWORD.
  • 步骤七:添加Mybatis核心配置文件

    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
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
    <!--读取外部properties配置文件-->
    <properties resource="jdbc.properties"></properties>
    <!--别名扫描的包路径-->
    <typeAliases>
    <package name="com.blog.domain"/>
    </typeAliases>
    <!--数据源-->
    <environments default="mysql">
    <environment id="mysql">
    <transactionManager type="JDBC"></transactionManager>
    <dataSource type="POOLED">
    <property name="driver" value="${jdbc.driver}"></property>
    <property name="url" value="${jdbc.url}"></property>
    <property name="username" value="${jdbc.username}"></property>
    <property name="password" value="${jdbc.password}"></property>
    </dataSource>
    </environment>
    </environments>
    <!--映射文件扫描包路径-->
    <mappers>
    <package name="com.blog.dao"></package>
    </mappers>
    </configuration>
  • 步骤八:编写应用程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class App {
    public static void main(String[] args) throws IOException {
    // 1. 创建SqlSessionFactoryBuilder对象
    SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
    // 2. 加载mybatis-config.xml配置文件
    InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
    // 3. 创建SqlSessionFactory对象
    SqlSessionFactory factory = sqlSessionFactoryBuilder.build(inputStream);
    // 4. 获取SqlSession
    SqlSession sqlSession = factory.openSession();
    // 5. 获取mapper
    AccountDao mapper = sqlSession.getMapper(AccountDao.class);
    //6. 执行方法进行查询
    Account account = mapper.findById(2);
    System.out.println(account);
    //7. 释放资源
    sqlSession.close();
    }
    }
  • 步骤九:运行程序,结果如下

    Account{id=2, name=’Jerry’, money=3000.0}

思路分析

Mybatis的基础环境我们已经准备好了,接下来就得分析在上述的内容中,哪些对象可以交给Spring来管理?

  • Mybatis程序核心对象分析
    从图中可以获取到,真正需要交给Spring管理的是SqlSessionFactory
  • 整合Mybatis,就是将Mybatis用到的内容交给Spring管理,分析下配置文件

说明:

  • 第一部分读取外部properties配置文件,Spring有提供具体的解决方案@PropertySource,需要交给Spring
  • 第二部分起别名包扫描,为SqlSessionFactory服务的,需要交给Spring
  • 第三部分主要用于做连接池,Spring之前我们已经整合了Druid连接池,这块也需要交给Spring
  • 前面三部分一起都是为了创建SqlSession对象用的,那么用Spring管理SqlSession对象吗?回忆下SqlSession是由SqlSessionFactory创建出来的,所以只需要将SqlSessionFactory交给Spring管理即可。
  • 第四部分是Mapper接口和映射文件[如果使用注解就没有该映射文件],这个是在获取到SqlSession以后执行具体操作的时候用,所以它和SqlSessionFactory创建的时机都不在同一个时间,可能需要单独管理。

整合步骤

整合Spring与Mybatis大体需要做两件事,

  • 第一件:Spring要管理MyBatis中的SqlSessionFactory
  • 第二件:Spring要管理Mapper接口的扫描

那我们下面就开始来整合

  • 步骤一:项目中导入整合需要的jar包

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.0</version>
    </dependency>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
  • 步骤二:创建Spring的主配置类

1
2
3
4
5
6
//配置类注解
@Configuration
//包扫描,主要扫描的是项目中的AccountServiceImpl类
@ComponentScan("com.blog")
public class SpringConfig {
}
  • 步骤三:
    创建数据源的配置类
    在配置类中完成数据源的创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;

@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
  • 步骤四:主配置类中读properties并引入数据源配置类
1
2
3
4
5
6
@Configuration
@ComponentScan("com.blog")
@PropertySource("jdbc.properties")
@Import(JdbcConfig.class)
public class SpringConfig {
}
  • 步骤五:创建Mybatis配置类并配置SqlSessionFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyBatisConfig {

@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
//定义bean,SqlSessionFactoryBean,用于产生SqlSessionFactory对象
SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
//设置模型类的别名扫描
sqlSessionFactory.setTypeAliasesPackage("com.blog.domain");
//设置数据源
sqlSessionFactory.setDataSource(dataSource);
return sqlSessionFactory;
}
//定义bean,返回MapperScannerConfigurer对象
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.blog.dao");
return msc;
}
}

说明:

  • 使用SqlSessionFactoryBean封装SqlSessionFactory需要的环境信息
    img

  • SqlSessionFactoryBean是前面我们讲解FactoryBean的一个子类,在该类中将SqlSessionFactory的创建进行了封装,简化对象的创建,我们只需要将其需要的内容设置即可。

  • 方法中有一个参数为dataSource,当前Spring容器中已经创建了Druid数据源,类型刚好是DataSource类型,此时在初始化SqlSessionFactoryBean这个对象的时候,发现需要使用DataSource对象,而容器中刚好有这么一个对象,就自动加载了DruidDataSource对象。

  • sqlSessionFactory.setTypeAliasesPackage("com.blog.domain");,替换掉配置文件中的

1
2
3
<typeAliases>
<package name="com.blog.domain"/>
</typeAliases>
  • sqlSessionFactory.setDataSource(dataSource);,替换掉配置文件中的
1
2
3
4
5
6
7
8
9
10
11
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</dataSource>
</environment>
</environments>
  • 使用MapperScannerConfigurer加载Dao接口,创建代理对象保存到IOC容器中
    img

  • 这个MapperScannerConfigurer对象也是MyBatis提供的专用于整合的jar包中的类,用来处理原始配置文件中的mappers相关配置,加载数据层的Mapper接口类

  • MapperScannerConfigurer有一个核心属性basePackage,就是用来设置所扫描的包路径

  • 步骤六:主配置类中引入Mybatis配置类

1
2
3
4
5
6
@Configuration
@ComponentScan("com.blog")
@PropertySource("jdbc.properties")
@Import({JdbcConfig.class, MyBatisConfig.class})
public class SpringConfig {
}
  • 步骤七:编写运行类
    在运行类中,从IOC容器中获取Service对象,调用方法获取结果
1
2
3
4
5
6
7
8
public class App {
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
AccountService accountService = context.getBean(AccountService.class);
Account account = accountService.findById(1);
System.out.println(account);
}
}
  • 步骤八:运行程序

    Account{id=1, name=’Tom’, money=2800.0}

至此,Spring与Mybatis的整合就已经完成了,其中主要用到的两个类分别是:

  • SqlSessionFactoryBean
  • MapperScannerConfigurer

Spring整合JUnit

  • 步骤一:引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
  • 步骤二:编写测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//设置类运行器
@RunWith(SpringJUnit4ClassRunner.class)
//设置Spring环境对应的配置类
@ContextConfiguration(classes = {SpringConfig.class})//加载配置类
public class AccountServiceTest {
//支持自动装配注入bean
@Autowired
private AccountService accountService;

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

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

注意:

  • 单元测试,如果测试的是注解配置类,则使用@ContextConfiguration(classes = 配置类.class)
  • 单元测试,如果测试的是配置文件,则使用@ContextConfiguration(locations={配置文件名,...})
  • Junit运行后是基于Spring环境运行的,所以Spring提供了一个专用的类运行器,这个务必要设置,这个类运行器就在Spring的测试专用包中提供的,导入的坐标就是这个东西SpringJUnit4ClassRunner
  • 上面两个配置都是固定格式,当需要测试哪个bean时,使用自动装配加载对应的对象,下面的工作就和以前做Junit单元测试完全一样了

知识点1:@RunWith

AOP简介

Spring有两个核心的概念,一个是IOC/DI,一个是AOP

  • 前面学习的IOC\DI主要的内容是将对象的创建和管理交给了Spring容器,而AOP则是在不改变原有代码的基础上添加新的功能。

    什么是AOP?

  • AOP(Aspect Oriented Programming)面向切面编程,是一种编程范式,指导开发者如何组织程序结构

    AOP核心概念

  • 前面编写的BookDaoImpl类,设置了最简单的几个执行业务逻辑的代码,没有其他功能,现在体验一下使用AOP对代码进行升级
    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
    @Repository
    public class BookDaoImpl implements BookDao {
    public void save() {
    //记录程序当前执行执行(开始时间)
    Long startTime = System.currentTimeMillis();
    //业务执行万次
    for (int i = 0;i<10000;i++) {
    System.out.println("book dao save ...");
    }
    //记录程序当前执行时间(结束时间)
    Long endTime = System.currentTimeMillis();
    //计算时间差
    Long totalTime = endTime-startTime;
    //输出信息
    System.out.println("执行万次消耗时间:" + totalTime + "ms");
    }
    public void update(){
    System.out.println("book dao update ...");
    }
    public void delete(){
    System.out.println("book dao delete ...");
    }
    public void select(){
    System.out.println("book dao select ...");
    }
    }
    代码的内容很简单,就是测试一下万次执行的耗时
    当在App类中从容器中获取bookDao对象后,分别执行其save,delete,update和select方法后会有如下的打印结果

    book dao save …
    book dao save …
    book dao save …
    book dao save …
    book dao save …
    book dao save …
    执行万次消耗时间:79ms

    book dao delete …
    book dao delete …
    book dao delete …
    book dao delete …
    book dao delete …
    book dao delete …
    执行万次消耗时间:81ms

    book dao update …
    book dao update …
    book dao update …
    book dao update …
    book dao update …
    book dao update …
    执行万次消耗时间:63ms

    book dao select …

    疑问

    • 对于计算万次执行消耗的时间只有save方法有,为什么delete和update方法也会有呢?
    • delete和update方法有,那为什么select方法为什么又没有呢?
    这个案例中使用的AOP使得在不改动原代码的前提下,增强了原代码的功能,这就是AOP
    Spring是如何实现AOP的呢?
  • 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
    • 在SpringAOP中,理解为能够被增强的方法
  • 切入点(Pointcut):真正要被增强的连接点
    • 在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法
      • 一个具体的方法:如com.blog.dao包下的BookDao接口中的无形参无返回值的save方法
      • 匹配多个方法:所有的save方法/所有的get开头的方法/所有以Dao结尾的接口中的任意方法/所有带有一个参数的方法
    • 连接点范围比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法不一定要被增强,所以可能不是切入点。
  • 通知(Advice):在切入点处执行的操作,也就是要增加的共性功能
    • 在SpringAOP中,功能最终以方法的形式呈现
  • 通知类:定义通知的类
  • 切面(Aspect):描述通知与切入点的对应关系。可以理解为切入点+通知

AOP入门案例

需求:在每个方法执行前添加一个功能:输出当前系统时间

环境准备

  • 创建Maven项目,并导入Spring依赖
    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.10.RELEASE</version>
    </dependency>
  • 添加BookDao和BookDaoImpl类

    1
    2
    3
    4
    public interface BookDao {
    public void save();
    public void update();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Repository
    public class BookDaoImpl implements BookDao {
    public void save() {
    System.out.println(System.currentTimeMillis());
    System.out.println("book dao save ...");
    }

    public void update() {
    System.out.println("book dao update ...");
    }
    }
  • 创建Spring依赖类

    1
    2
    3
    4
    @Configuration
    @ComponentScan("com.blog")
    public class SpringConfig {
    }
  • 编写App运行类
    1
    2
    3
    4
    5
    6
    7
    public class App {
    public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
    BookDao bookDao = context.getBean(BookDao.class);
    bookDao.update();
    }
    }

    AOP实现步骤

    需求分析:

    • 目前调用save方法,方法中能够输出当前系统时间,现在的目的是更新update方法,实现不改变update方法,添加打印时间的功能
      思路分析:
    • 导入坐标
    • 制作连接点(原始操作,Dao接口和实现类)
    • 制作共性功能(通知类和通知方法)
    • 定义切入点
    • 绑定切入点和通知的关系(切面)
  • 步骤一:添加依赖
    spring-context中已经导入了spring-aop,所以不需要再单独导入spring-aop
    导入AspectJ的jar包,AspectJ是AOP思想的一个具体实现,Spring有自己的AOP实现,但是相比于AspectJ来说比较麻烦,所以直接采用Spring整合ApsectJ的方式进行AOP开发。
1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
  • 步骤二:定义接口和实现类
    准备环境的时候已经完成了

  • 步骤三:定义通知类和通知
    通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印。
    类名和方法名没有要求,可以任意。

1
2
3
4
5
public class MyAdvice {
public void method(){
System.out.println(System.currentTimeMillis());
}
}
  • 步骤四:定义切入点
    BookDaoImpl中有两个方法,分别是update()save(),我们要增强的是update方法,那么该如何定义呢?
1
2
3
4
5
6
7
8
9
public class MyAdvice {
@Pointcut("execution(void com.blog.dao.impl.BookDaoImpl.update())")
private void pt() {
}

public void method() {
System.out.println(System.currentTimeMillis());
}
}

说明:

  • 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。
  • execution及后面编写的内容,之后我们会专门去学习。
  • 步骤五:制作切面
    切面是用来描述通知和切入点之间的关系,如何进行关系的绑定?
1
2
3
4
5
6
7
8
9
10
public class MyAdvice {

@Pointcut("execution(void com.blog.dao.BookDao.update())")
private void pt(){}

@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}

绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置

说明:@Before翻译过来是之前,也就是说通知会在切入点方法执行之前执行,除此之前还有其他四种类型,后面会讲。
那这里就会在执行update()之前,来执行我们的method(),输出当前毫秒值

  • 步骤六:将通知类配给容器并标识其为切面类
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
@Aspect
public class MyAdvice {

@Pointcut("execution(void com.blog.dao.impl.BookDaoImpl.update())")
private void pt() {
}

@Before("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
  • 步骤七:开启注解格式AOP功能
    使用@EnableAspectJAutoProxy注解
1
2
3
4
5
@Configuration
@ComponentScan("com.blog")
@EnableAspectJAutoProxy
public class SpringConfig {
}
  • 步骤八:运行程序
    这次我们再来调用update()
1
2
3
4
5
6
7
public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = context.getBean(BookDao.class);
bookDao.update();
}
}

控制台成功输出了当前毫秒值

1662367945787
book dao update …

名称@EnableAspectJAutoProxy
类型配置类注解
位置配置类定义上方
作用开启注解格式AOP功能
名称@Aspect
类型类注解
位置切面类定义上方
作用设置当前类为AOP切面类
名称@Before
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行
名称@Pointcut
类型方法注解
位置切入点方法定义上方
作用设置切入点方法
属性value(默认):切入点表达式

AOP工作流程

这一节我们主要讲解两个知识点:AOP工作流程AOP核心概念。其中核心概念是对前面核心概念的补充。

工作流程

AOP是基于Spring容器管理的bean做的增强,所以整个工作过程需要从Spring加载bean说起

  • 流程一:Spring容器启动

    • 容器启动就需要去加载bean,哪些类需要被加载呢?
    • 需要被增强的类,如:BookServiceImpl
    • 通知类,如:MyAdvice
    • 注意此时bean对象还没有创建成功
  • 流程二:读取所有切面配置中的切入点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Component
    @Aspect
    public class MyAdvice {
    @Pointcut("execution(void com.blog.dao.impl.BookDaoImpl.save())")
    private void ptx() {
    }

    @Pointcut("execution(void com.blog.dao.impl.BookDaoImpl.update())")
    private void pt() {
    }


    @Before("pt()")
    public void method() {
    System.out.println(System.currentTimeMillis());
    }
    }

    上面这个例子中有两个切入点的配置,但是第一个ptx()并没有被使用(该切入点没有被任何通知使用),所以不会被读取。

  • 流程三:初始化bean,判定bean对应的类中的方法是否匹配到任意切入点

    • 注意第一步在容器启动的时候,bean对象还没有被创建成功。
    • 要被实例化bean对象的类中的方法和切入点进行匹配
  • 匹配失败,创建原始对象,如UserDao

    • 匹配失败说明不需要增强,直接调用原始对象的方法即可。
  • 匹配成功,创建原始对象(目标对象)的代理对象,如:BookDao

    • 匹配成功说明需要对其进行增强
    • 对哪个类做增强,这个类对应的对象就叫做目标对象
    • 因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
    • 最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强
  • 流程四:获取bean执行方法

    • 获取的bean是原始对象时,调用方法并执行,完成操作
    • 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
  • 下面我们来验证一下容器中是否为代理对象

    • 如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
    • 如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身
  • 步骤一:修改App运行类,获取类的类型并输出

    1
    2
    3
    4
    5
    6
    7
    8
    public class App {
    public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
    BookDao bookDao = context.getBean(BookDao.class);
    System.out.println(bookDao);
    System.out.println(bookDao.getClass());
    }
    }
  • 步骤二:修改MyAdvice类,改为不增强
    将定义的切入点改为updatexxx,而BookDaoImpl类中不存在该方法,所以BookDao中的update方法在执行的时候,就不会被增强
    所以此时容器中的对象应该是目标对象本身。

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.blog.dao.impl.BookDaoImpl.updatexxx())")
private void pt() {
}

@Before("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
  • 步骤三:运行程序
    输出结果如下,确实是目标对象本身,符合我们的预期

    com.blog.dao.impl.BookDaoImpl@bcec361
    class com.blog.dao.impl.BookDaoImpl

  • 步骤四:修改MyAdvice类,改为增强
    将定义的切入点改为update,那么BookDao中的update方法在执行的时候,就会被增强
    所以容器中的对象应该是目标对象的代理对象

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.blog.dao.impl.BookDaoImpl.update())")
private void pt() {
}

@Before("pt()")
public void method() {
System.out.println(System.currentTimeMillis());
}
}
  • 步骤五:运行程序
    结果如下

    com.blog.dao.impl.BookDaoImpl@3d34d211
    class com.sun.proxy.$Proxy19

注意
不能直接打印对象,从上面两次结果中可以看出,直接打印对象走的是对象的toString方法,不管是不是代理对象,打印的结果都是一样的,原因是内部对toString方法进行了重写。

AOP核心概念

在上面介绍AOP的工作流程中,我们提到了两个核心概念,分别是:

  • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
  • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现

简单来说,目标对象就是要增强的类如:BookServiceImpl类对应的对象,也叫原始对象,不能说它不能运行,只能说它在运行的过程中对于要增强的内容是缺失的。

SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知如:MyAdvice中的method方法内容加进去,就实现了增强,这就是我们所说的代理(Proxy)。

AOP配置管理

AOP切入点表达式

  • 在入门案例就已经用过切入点表达式了,此处具体学习
    1
    @Pointcut("execution(void com.blog.dao.impl.BookDaoImpl.update())")
    对于AOP中切入点表达式,我们总共会学习三个内容,分别是语法格式通配符书写技巧

    语法格式

    明确:

    • 切入点:要进行增强的方法
    • 切入点表达式:要进行增强的方法的描述方式
    对切入点描述有两种方式:
  • 描述方式一:执行BookDao接口的无参数update方法
    1
    execution(void com.blog.dao.BookDao.update())
  • 描述方式二:执行BookDaoImpl类的无参数update方法
    1
    execution(void com.blog.dao.impl.BookDaoImpl.update())

对于切入点表达式的语法为:

  • 切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
    对于这个格式,不需要硬记,通过一个例子去理解它:

    1
    execution(public User com.blog.service.UserService.findById(int))
  • execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点

  • public:访问修饰符,还可以是public,private等,可以省略
  • User:返回值,写返回值类型
  • com.blog.service:包名,多级包使用点连接
  • UserService:类/接口名称
  • findById:方法名
  • int:参数,直接写参数的类型,多个类型用逗号隔开
  • 异常名:方法定义中抛出指定异常,可以省略

切入点表达式就是要找到需要增强的方法,所以它就是对一个具体方法的描述,但是方法的定义会有很多,所以如果每一个方法对应一个切入点表达式,想想这块就会觉得将来编写起来会比较麻烦,有没有更简单的方式呢?

  • 使用通配符

通配符

使用通配符的主要目的就是简化配置,例如:

  • *:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
    匹配com.blog包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法

    1
    execution(public * com.blog.*.UserService.find*(*))
  • ..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
    匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法

    1
    execution(public User com..UserService.findById(..))
  • +:专用于匹配子类类型
    这个使用率较低,描述子类的,*Service+,表示所有以Service结尾的接口的子类

1
execution(* *..*Service+.*(..))

下面来具体分析一下各种用法

  • 匹配接口,能匹配到
1
execution(void com.blog.dao.BookDao.update())
  • 匹配实现类,能匹配到

    1
    execution(void com.blog.dao.impl.BookDaoImpl.update())
  • 返回值任意,能匹配到

1
execution(* com.blog.dao.impl.BookDaoImpl.update())
  • 返回值任意,但是update方法必须要有一个参数,无法匹配,要想匹配需要在update接口和实现类添加参数
1
execution(* com.blog.dao.impl.BookDaoImpl.update(*))
  • 返回值为void,com包下的任意包三层包下的任意类的update方法,匹配到的是实现类,能匹配
1
execution(void com.*.*.*.*.update())
  • 返回值为void,com包下的任意两层包下的任意类的update方法,匹配到的是接口,能匹配
1
execution(void com.*.*.*.update())
  • 返回值为void,方法名是update的任意包下的任意类,能匹配
1
execution(void *..update())
  • 匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
1
execution(* *..*(..))
  • 匹配项目中任意包任意类下只要以u开头的方法,update方法能满足,能匹配
1
execution(* *..u*(..))
  • 匹配项目中任意包任意类下只要以e结尾的方法,update和save方法能满足,能匹配
1
execution(* *..*e(..))
  • 返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法
1
execution(void com..*())
  • 将项目中所有业务层方法的以find开头的方法匹配
1
execution(* com.blog.*.*Service.find*(..))
  • 将项目中所有业务层方法的以save开头的方法匹配
1
execution(* com.blog.*.*Service.save*(..))

书写技巧

对于切入点表达式的编写其实是很灵活的,那么在编写的时候,有没有什么好的技巧让我们用用:

  • 所有代码按照标准规范开发,否则以下技巧全部失效
  • 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了
  • 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述
  • 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
  • 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
  • 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
  • 方法名书写以动词进行精准匹配,名词采用*匹配,例如getById书写成getBy*selectAll书写成selectAll
  • 参数规则较为复杂,根据业务方法灵活调整
  • 通常不使用异常作为匹配规则