Spring


Spring

Spring 是一个开源框架,它由 Rod Johnson 创建。它是为了解决企业应用开发的复杂性而创建的。从简单性、可测试性和松耦合的角度而言,任何 Java 应用都可以从 Spring 中受益。Spring 是一个轻量级的控制反转(IoC)和面向切面(AoP)的容器框架。Spring 最初的出现是为了解决 EJB 臃肿的设计,以及难以测试等问题。Spring 为简化开发而生,让程序员只需关注核心业务的实现,尽可能的不再关注非业务逻辑代码(事务控制,安全日志等)。

如果仅仅想使用 Spring IoC 功能的话,利用 Maven 引入 Spring context 即可:

<dependencies>
    <!-- 引入spring框架 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>6.0.6</version>
    </dependency>

    <!-- 引入junit测试依赖 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>

    <!-- 引入log4j依赖 -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.20.0</version>
    </dependency>

    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j2-impl</artifactId>
        <version>2.19.0</version>
    </dependency>
    
    <!-- 引入web框架 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>6.0.6</version>
    </dependency>
</dependencies>

软件开发原则

OCP 原则

OCP 是软件七大开发原则当中最基本的一个原则 —— 开闭原则。即对扩展开放,对修改关闭

OCP 原则是最核心的、最基本的,其他的六个原则都是为这个原则服务的。OCP 开闭原则的核心是:只要在扩展系统功能时,不修改原来的代码,那就是符合 OCP 原则的。反之,如果在扩展系统功能的时候,修改了之前的代码,那这个设计就是失败的,违背了 OCP 原则,因为当系统功能扩展的时候,如果修改了之前的程序,之前所有的程序都需要进行重新测试,是相当麻烦的。

DIP 原则

DIP 原则是依赖倒置原则。在之前普通的 Web 项目中,Controller 层依赖 Service 层,Service 层依赖 Dao 层,这是一个上依赖下的,凡是上依赖下的,便是违反 DIP 原则,只要下一改动,上就受到牵连。依赖倒置原则实际上是倡导面向接口编程,面向抽象编程,不要面向具体编程。DIP 原则目的是降低程序的耦合度,提高扩展力。

Web 项目结构的缺陷

在之前的 JavaWeb 中,我们使用 MVC 设计模式,项目结构分为三层:Controller 层、Service 层、Dao 层。我们发现当我们更改 Dao 层的业务逻辑的时候(比如说我们需要更换一个性能更高的数据库),这个时候,不仅会牵动 Dao 层原本的实现类代码,还会牵动 Controller 层、Service 层的代码。很明显,之前的结构设计,违背了 OCP 原则和 DIP 原则

我们拿 Service 层举例,我们一开始写的 Service 层代码是这样的:

public class UserServiceImpl implements UserService {

    //UserDao接口new实现类,但是这句代码是把对象写死了
    private final UserDao userDao = new UserDaoImpl();

    @Override
    public void deleteUser() {
        System.out.println("服务层调用处理业务逻辑代码...");
        userDao.deleteById();
    }
}

我们之前提到,我们尽可能需要降低软件之间的耦合度,一种理想的状态便是我们面向接口编程或者是面向抽象编程。我们可以将上述第四行代码改为:private final UserDao userDao;,虽然暂时这种设计会报出空指针异常的问题,但是这种设计是我们的理想设计,因为这能够有效降低代码的耦合度。

控制反转 IoC 思想

上述提到,我们原本的 JavaWeb 项目程序违反了 OCP 和 DIP 两个原则,要解决这个问题,我们可以采用控制反转思想。

控制反转:IoC(Inversion of Control),“反转”的是两件事:

  1. 我们不再在程序中采用硬编码的方式来 new 对象了。(new 对象的权力交出去了)
  2. 我们不再在程序中采用硬编码的方式来维护对象之间的关系了。(对象之间的维护权我们也交出去了)

控制反转是一种编程思想,或者说是一种新型的设计模式。由于出现的时候比较新,没有被纳入 GOF23 种设计模式范围内。

而本章记录的 Spring 框架,实现了 控制反转 IoC 这种思想。可以帮助我们 new 对象,并且帮助我们维护对象和对象之间的关系。又或者说,Spring 是一个实现了 IoC 思想的容器。控制反转实现的方式有多种,其中比较重要的是依赖注入(Dependency Injection,DI,控制反转是一种思想,依赖注入是这种思想的实现)。

“依赖”指的是 A 对象和 B 对象的关系;而“注入”是一种手段,通过这种手段,可以让 A 对象和 B 对象产生关系。“依赖注入”便是对象与对象间的关系靠注入的手段来维护,而注入包括了 set 注入和构造注入。

依赖注入又包括常见的两种方式:一种是 set 注入,另一种是构造方法注入。两种注入方式简要理解代码如下:

public class UserServiceImpl implements UserService {

    //抽象接口
    private final UserDao userDao;
    
    //set注入
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    
    //构造方式注入
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao
    }

}

Spring 的 8 大模块

  1. Spring Core:控制反转(IoC)。这是 Spring 框架最基础的部分,它提供了依赖注入特征来实现容器对 Bean 的管理。核心容器的主要组件是 BeanFactory,BeanFactory 是工厂模式的一个实现,是任何 Spring 应用的核心。它使用 IoC 将应用配置和依赖从实际的应用代码中分离出来。
  2. Spring AOP:面向切面编程。
  3. Spring Web MVC:也称作 SpringMVC 框架。是 Spring 自己提供的一套 MVC 框架。
  4. Spring Webflux:是 Spring 提供的响应式 web 框架。
  5. Spring Web:支持集成常见的 Web 框架,例如:struts、webwork 等。
  6. Spring DAO:提供了单独的支持 JDBC 操作的 API。
  7. Spring ORM:支持集成常见的 ORM 框架,例如:MyBatis、Hibernate 等。
  8. Spring Context:Spring 上下文,提供了其他额外的模板技术扩展,例如:国际化消息、事件传播、验证的支持、企业服务、Velocity 和 FreeMarker 集成的支持等。

Spring 框架的特点

  1. 轻量:大小与开销两方面而言 Spring 都是轻量的。完整的 Spring 框架可以在一个大小只有1MB多的 JAR 文件里发布。并且 Spring 所需的处理开销也是微不足道的。Spring 是非侵入式的:Spring 应用中的对象不依赖于 Spring 的特定类。(侵入式设计:我开发了一个框架,这个框架有一个类,类中有一个方法,该方法上有一个参数 HttpServlet,而这个参数是需要 Tomcat 服务器的支持的,这个便是侵入式设计。侵入式设计不方便我们做单元测试。)
  2. 控制反转:Spring 通过一种称作控制反转(loC)的技术促进了松耦合。当应用了 loC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。你可以认为 loC 与 JNDI 相反—不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。
  3. 面向切面:Spring 提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。
  4. 容器:Spring 包含并管理应用对象的配置和生命周期,在这个意义上它是一种容器,你可以配置你的每个 bean (每一个被 Spring 管理的对象都叫做 bean)如何被创建—基于一个可配置原型(prototype),你的 bean 可以创建一个单独的实例或者每次需要时都生成一个新的实例——以及它们是如何相互关联的。然而,Spring 不应该被混同于传统的重量级的 EJB 容器,它们经常是庞大与笨重的,难以使用。
  5. 框架:Spring 可以将简单的组件配置、组合成为复杂的应用。在 Spring 中,应用对象被声明式地组合,典型地是在一个 XML 文件里。Spring 也提供了很多基础功能(事务管理、持久化框架集成等等),将应用逻辑的开发留给了你。

所有 Spring 的这些特征使你能够编写更干净、更可管理、并且更易于测试的代码。它们也为Spring中的各种模块提供了基础支持。

Spring 入门

  1. 配置pom.xml文件:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>cn.hnu.spring</groupId>
        <artifactId>spring6-002-first</artifactId>
        <version>1.0-SNAPSHOT</version>
        <!-- spring项目可以不是web项目 -->
        <packaging>jar</packaging>
    
        <dependencies>
            <!-- 引入spring-context依赖 -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>6.0.6</version>
            </dependency>
    
            <!-- 引入junit依赖 -->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.13.2</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
    
        <properties>
            <maven.compiler.source>20</maven.compiler.source>
            <maven.compiler.target>20</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    
    </project>
  2. java文件夹中创建 javabean 类。(这里创建 User 类为例)

  3. resources中创建 Spring 配置文件,文件名任取:

    <?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">
    
        <!-- idea为我们提供了spring配置文件的模板 -->
        <!-- 配置bean,这样spring才能帮助我们管理这个bean对象 -->
        <!--
            bean 标签中的两个重要属性
                id 是bean的唯一标识,不能重复
                class 必须填写类的全路径,全限定类名
        -->
        <bean id="userBean" class="cn.hnu.spring6.bean.User"/>
    </beans>
  4. 在 test 包下创建单元测试类,测试代码:

    package cn.hnu.spring6.test;
    
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class FirstSpringTest {
        @Test
        public void testFirstSpringCode() {
            //1. 获取spring容器对象
            /*
                ApplicationContext是应用上下文,实际上就是spring容器
                ApplicationContext是一个接口,其下有很多实现类
                ClassPathXmlApplicationContext是上述实现类之一,专门加载spring配置文件的对象
                执行下述代码相当于启动了spring容器,解析spring.xml为念,并实例化所有bean对象,放到容器中
            */
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    
            //2. 根据beanId从spring容器中获取对象
            Object userBean = applicationContext.getBean("userBean");
            System.out.println(userBean);
    
        }
    }

实现细节

  • spring.xml配置文件中,bean 的 id 是唯一标识,不可重复。
  • Spring 框架在默认情况下是通过反射机制调用类的无参构造方法来实例化对象。
  • Spring 框架把对象存入到一个 Map 集合中,key 是 bean 的 id,value 是 bean 创建出来的对象。
  • Spring 的配置文件命名没有强制要求,并且可以存在多个。我们在利用ClassPathXmlApplicationContext读取配置文件的时候也可以填入多个配置文件(底层有一个方法重载,参数是配置文件的可变参数)。
  • Spring 的配置文件中支持配置 JDK 自带的类,例如:<bean id="nowTime" class="java.util.Date"/>
  • 如果 bean 的 id 不存在,会报错。
  • 可以通过第二个参数指定返回类型:User userBean = applicationContext.getBean("userBean", User.class);
  • 当 Spring 的配置文件在别的盘符中时,应该使用FileSystemXmlApplicationContext来读取配置文件。
  • ApplicationContext接口的超级父接口是BeanFactory,翻译为“Bean工厂”,就是一个能够生产 Bean 对象的工厂。BeanFactory是 IoC 容器的顶级接口,Spring 的 IoC 容器底层实际上使用的是工厂模式(xml 解析 + 工厂模式 + 反射机制)。
  • 不是在调用getBean方法时创建对象,而是在解析配置文件的时候创建对象

Spring6 启用 Log4j2 日志框架

  1. 引入 Log4j2 依赖:

    <!-- log4j依赖 -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.20.0</version>
    </dependency>
    
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j2-impl</artifactId>
        <version>2.19.0</version>
    </dependency>
  2. 在类的根路径下提供log4j2.xml配置文件,文件名固定:

    <?xml version="1.0" encoding="UTF-8" ?>
    
    <configuration>
    
        <loggers>
            <!--
                level指定日志级别,从高到低的优先级:
                    ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF
            -->
            <root level="DEBUG">
                <appender-ref ref="spring6log"/>
            </root>
    
        </loggers>
    
        <appenders>
            <!-- 输出日志信息到控制台 -->
            <console name="spring6log" target="SYSTEM_OUT">
                <!-- 控制日志输出的格式 -->
                <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/>
            </console>
        </appenders>
    
    </configuration>
  3. 使用日志记录:

    @Test
    public void testLog4j2() {
        //自己使用log4j2记录日志信息
    
        //1. 创建日志记录器对象
        /*
        * 只要是FirstSpringTest类中的代码执行记录日志的话,就输出相关的日志信息
        * */
        private static final Logger logger = LoggerFactory.getLogger(FirstSpringTest.class);
    
        //2. 记录日志,根据不同级别来输出日志
        logger.info("我是一条消息");
        logger.debug("我是一条调试信息");
        logger.error("我是一条错误信息");
    }

Spring 对 IoC 的实现

set 注入

需要注入的类得提供set方法:

package cn.hnu.spring6.service;
import cn.hnu.spring6.dao.UserDao;

public class UserService {

    private UserDao userDao;

    //必须提供一个set方法以使用set注入
    //spring框架会调用set方法来给userDao赋值
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void saveUser() {
        userDao.insert();
    }
}

配置文件使用property来进行 set 注入:

<?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="userDaoBean" class="cn.hnu.spring6.dao.UserDao" />

    <!-- 使用set注入 -->
    <bean id="userServiceBean" class="cn.hnu.spring6.service.UserService">
        <!--
            spring调用对应的set方法需要配置property标签
            标签中name属性的值是set方法的方法名去掉set并把剩下字符串的首字母变小写
            ref翻译为引用references,值是需要注入的bean的id值
        -->
        <property name="userDao" ref="userDaoBean" />
    </bean>

</beans>

构造注入

核心原理:通过调用构造方法来给属性赋值。构造注入和 set 注入的区别就在于注入的时机是不一样的

被注入的类需要提供构造方法:

package cn.hnu.spring6.service;

import cn.hnu.spring6.dao.UserDao;
import cn.hnu.spring6.dao.VipDao;

public class CustomerService {

    private UserDao userDao;
    private VipDao vipDao;

    public CustomerService(UserDao userDao,
                           VipDao vipDao) {
        this.userDao = userDao;
        this.vipDao = vipDao;
    }

    public void save() {
        userDao.insert();
        vipDao.insert();
    }

}

配置文件使用constructor-arg来进行构造注入:

<?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="userDaoBean" class="cn.hnu.spring6.dao.UserDao" />
    <bean id="vipDaoBean" class="cn.hnu.spring6.dao.VipDao" />
    <bean id="csBean" class="cn.hnu.spring6.service.CustomerService" >

        <!-- 构造注入 -->
        <!-- 指定构造方法的第1个参数,index=0 -->
        <constructor-arg index="0" ref="userDaoBean"/>
        <!-- 指定构造方法的第2个参数,index=1 -->
        <constructor-arg index="1" ref="vipDaoBean"/>

        <!--
            也可以这样写,用name属性来指定参数
            <constructor-arg name="userDao" ref="userDaoBean"/>
            <constructor-arg name="vipDao" ref="vipDaoBean"/>

            还可以这么写,直接指定ref,不过这种写法可读性较差
            <constructor-arg ref="vipDaoBean"/>
            <constructor-arg ref="userDaoBean"/>

        -->

    </bean>
</beans>

set 注入专题

在实际开发中,set 注入使用的频率比较高,所以我们有必要来仔细研究一下 set 注入的细节。

注入外部 Bean

使用ref来引入外部的 Bean:

<?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 -->
    <bean id="orderDaoBean" class="cn.hnu.spring6.dao.OrderDao"/>
    <bean id="orderServiceBean" class="cn.hnu.spring6.service.OrderService">
        <!-- set注入外部Bean -->
        <property name="orderDao" ref="orderDaoBean"/>
    </bean>

    <bean id="orderServiceBean2" class="cn.hnu.spring6.service.OrderService">
        <property name="orderDao">
            <!-- 在property标签中使用bean标签就是内部set注入 -->
            <bean class="cn.hnu.spring6.dao.OrderDao"/>
        </property>
    </bean>
    
</beans>

注入简单类型

简单类型:八种基本类型及包装类、枚举、字符串、数字、日期、时间时区、URI、URL、语言、类。

注意:但是开发中一般我们不会把 Date 类型当作简单类型来处理,因为其格式对于中国人来讲过于逆天,太难记了(Tue Mar 05 20:20:12 CST 2024),所以实际开发中我们更偏向于使用ref来给 Date 类型赋值。

//Spring判断简单类型的源码
public static boolean isSimpleValueType(Class<?> type) {
    return Void.class != type && Void.TYPE != type && 
        (ClassUtils.isPrimitiveOrWrapper(type) || 
         Enum.class.isAssignableFrom(type) || 
         CharSequence.class.isAssignableFrom(type) || 
         Number.class.isAssignableFrom(type) || 
         Date.class.isAssignableFrom(type) || 
         Temporal.class.isAssignableFrom(type) || 
         URI.class == type || 
         URL.class == type || 
         Locale.class == type || 
         Class.class == type);
}

现有如下User类:

package cn.hnu.spring6.bean;

public class User {
    private String username;
    private String password;
    private int age;
	//Setter...
}

接下来我们利用配置文件对它进行 set 注入:

<?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="userBean" class="cn.hnu.spring6.bean.User">
        <!-- 给简单类型赋值就不能使用ref了,需要使用value-->
        <property name="username" value="张三"/>
        <property name="password" value="123"/>
        <property name="age" value="20"/>
    </bean>

</beans>

级联属性赋值(了解)

假设有一个Student类:

package cn.hnu.spring6.bean;

public class Student {
    private String name;
    private Clazz clazz;
	//setter...
}

我们给学生类进行 set 注入的时候一般这么注入:

<?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">
    <!-- 先给clazzBean赋值 -->
    <bean id="clazzBean" class="cn.hnu.spring6.bean.Clazz">
        <property name="name" value="高三八班"/>
    </bean>
    
    <!-- 再给studentBean赋值 -->
    <bean id="studentBean" class="cn.hnu.spring6.bean.Student">
        <property name="name" value="张三"/>
        <property name="clazz" ref="clazzBean"/>
    </bean>
</beans>

可以使用级联注入,不过前提是需要对外提供 get 方法

<?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="studentBean" class="cn.hnu.spring6.bean.Student">
        <property name="name" value="张三"/>
        <property name="clazz" ref="clazzBean"/>
        <!-- 级联属性赋值 -->
        <property name="clazz.name" value="高三八班"/>
    </bean>

    <bean id="clazzBean" class="cn.hnu.spring6.bean.Clazz"/>
</beans>

注入数组

假设有一个Person类,带有基本数据类型的数组和对象数组:

package cn.hnu.spring6.bean;
import java.util.Arrays;

public class Person {
    private String[] hobbies;
    private Girl[] girls;
	//setter...
}

注入数组:

<?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="girlBean1" class="cn.hnu.spring6.bean.Girl">
        <property name="name" value="小花"/>
    </bean>

    <bean id="girlBean2" class="cn.hnu.spring6.bean.Girl">
        <property name="name" value="小亮"/>
    </bean>

    <bean id="girlBean3" class="cn.hnu.spring6.bean.Girl">
        <property name="name" value="小明"/>
    </bean>


    <bean id="personBean" class="cn.hnu.spring6.bean.Person">
        <!-- 给String数组赋值 -->
        <property name="hobbies">
            <array>
                <value></value>
                <value></value>
                <value>rap</value>
                <value>篮球</value>
            </array>
        </property>

        <!-- 给对象数组赋值 -->
        <property name="girls">
            <array>
                <ref bean="girlBean1"/>
                <ref bean="girlBean2"/>
                <ref bean="girlBean3"/>
            </array>
        </property>
    </bean>

</beans>

注入 List 和 Set 集合

PersonLS类如下:

package cn.hnu.spring6.bean;
import java.util.List;
import java.util.Set;

public class PersonLS {
    private List<String> names;
    private Set<String> address;
    //setter...
}

配置文件如下:

<?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="personLSBean" class="cn.hnu.spring6.bean.PersonLS">
        <property name="names">
            <list>
                <value>张三</value>
                <value>李四</value>
                <value>王五</value>
                <value>王五</value>
            </list>
        </property>

        <property name="address">
            <set>
                <value>北京</value>
                <value>上海</value>
                <value>广州</value>
                <value>深圳</value>
            </set>
        </property>
    </bean>
</beans>

注入 Map 和 Properties

PersonLS类:

package cn.hnu.spring6.bean;

import java.util.Map;
import java.util.Properties;

public class PersonLS {
    private Map<Integer, String> phones;
    private Properties properties;
	//setter...
}

配置文件:

<?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="personLSBean" class="cn.hnu.spring6.bean.PersonLS">
        <property name="phones">
            <map>
                <entry key="1" value="110"/>
                <entry key="2" value="119"/>
                <entry key="3" value="120"/>
                <!--
                    如果key和value不是简单类型就用下述配置
                    <entry key-ref="" value-ref=""/>
                -->
            </map>
        </property>

        <property name="properties">
            <props>
                <!-- properties的key和value只能是String类型 -->
                <prop key="driver">com.mysql.cj.jdbc.Driver</prop>
                <prop key="url">jdbc:mysql://localhost:3306/spring6</prop>
                <prop key="password">123456</prop>
            </props>
        </property>
    </bean>
</beans>

注入 null 和空字符串

Cat类:

package cn.hnu.spring6.bean;

public class Cat {
    private String name;
    private String nickname;
	//setter...
}

配置文件:

<?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="catBean" class="cn.hnu.spring6.bean.Cat">
        <!-- 不给string注入值,默认注入null -->
        <!--

        -->
        <!-- 手动注入null -->
        <property name="name">
            <null/>
        </property>

        <!-- 注入空字符串 -->
        <property name="nickname" value=""/>
        <!--
            第二种注入空字符串的形式
            <property name="address">
                <value/>
            </property>
        -->

        <property name="age" value="5"/>
    </bean>
</beans>

注入特殊符号

XML 中有五个特殊符号,分别是:< > ' " &

特殊符号对照表:

特殊字符 转义字符
> &gt;
< &lt;
&apos;
&quot;
& &amp;

MathBean类:

package cn.hnu.spring6.bean;

public class MathBean {
    private String result;
	//setter...
}

配置文件:

<?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="mathBean" class="cn.hnu.spring6.bean.MathBean">
        <!-- 第一种方案,使用实体符代替特殊符号 -->
        <!--
			<property name="result" value="2 &lt; 3"/>
		-->

        <!-- 第二种方案,使用<![CDATA[]]> -->
        <property name="result">
            <!-- 只能使用value标签 -->
            <value><![CDATA[2 < 3]]></value>
        </property>
    </bean>
</beans>

p 命名空间注入

目的:简化配置。使用 p 命名空间注入的前提条件包括两个:

  1. 在 XML 头部信息中添加 p 命名空间的配置信息:xmlns:p="http://www.springframework.org/schema/p"
  2. p 命名空间注入是基于 set 注入的,所以需要对象的属性提供set方法。

Dog类:

package cn.hnu.spring6.bean;

import java.util.Date;

public class Dog {
    private String name;
    private int age;
    private Date birth;
	//setter...
}

配置文件:

<?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:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--
        在第四行添加p命名配置空间
    -->
    <bean id="dateBean" class="java.util.Date"/>    <!-- 默认获取当前时间 -->

    <!-- 使用p命名空间 -->
    <bean id="dogBean" class="cn.hnu.spring6.bean.Dog" p:name="小花" p:age="3" p:birth-ref="dateBean"/>


</beans>

c 命名空间注入

c 命名空间是简化构造方法注入的,使用 c 命名空间有两个前提:

  1. 需要在 XML 头部添加信息:xmlns:c="http://www.springframework.org/schema/c"
  2. 需要提供构造方法。

People类:

package cn.hnu.spring6.bean;

public class People {
    private String name;
    private int age;
    private boolean gender;
    
    public People(String name, int age, boolean gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
}

配置文件:

<?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:c="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--
        在第四行添加配置信息
    -->

    <bean id="peopleBean" class="cn.hnu.spring6.bean.People" c:age="18" c:name="小明" c:gender=""/>
</beans>

util 命名空间

使用 util 命名空间可以让配置复用。使用时注意配置头部信息:

<?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:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">


    <!-- util命名空间 -->
    <util:properties id="prop">
        <prop key="driver">com.mysql.cj.jdbc.Driver</prop>
        <prop key="url">jdbc:mysql://localhost:3306/spring6</prop>
        <prop key="password">123456</prop>
    </util:properties>
    
    <!-- 配置复用 -->
    <bean id="ds1" class="cn.hnu.spring6.jdbc.MyDataSource1">
        <property name="properties" ref="prop"/>
    </bean>

    <bean id="ds2" class="cn.hnu.spring6.jdbc.MyDataSource2">
        <property name="properties" ref="prop"/>
    </bean>
</beans>

基于 XML 的自动装配

Spring 还可以完成自动化注入,自动化注入又称自动装配。它可以根据名字进行装配,也可以根据类型进行装配。

根据名称自动装配

根据名称自动专配也是基于 set 注入的:

package cn.hnu.spring6.service;
import cn.hnu.spring6.dao.UserDao;

public class UserService {

    private UserDao userDao;
    //必须提供一个set方法以使用set注入
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

配置文件:

<?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是set方法去掉set的首字母小写 -->
    <bean id="orderDao" class="cn.hnu.spring6.dao.OrderDao"/>

    <!--
        <bean id="orderServiceBean" class="cn.hnu.spring6.service.OrderService">
            <property name="orderDao" ref="orderDaoBean"/>
        </bean>
    -->

    <!-- 根据名字自动装配 -->
    <bean id="orderServiceBean" class="cn.hnu.spring6.service.OrderService" autowire="byName"/>

</beans>

根据类型自动装配

根据类型自动专配也是基于 set 注入的,但要注意装配的时候,在有效的配置文件当中,某种类型的实例只能有一个:

package cn.hnu.spring6.service;

import cn.hnu.spring6.dao.UserDao;
import cn.hnu.spring6.dao.VipDao;

public class CustomerService {

    private UserDao userDao;
    private VipDao vipDao;
	//setter...
}

配置文件:

<?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 class="cn.hnu.spring6.dao.VipDao"/>
    <bean class="cn.hnu.spring6.dao.UserDao"/>
    <bean id="customerServiceBean" class="cn.hnu.spring6.service.CustomerService" autowire="byType"/>

</beans>

Spring 引入外部属性配置文件

注意需要先修改头部文件信息:

<?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文件
        location默认从类的根路径下开始加载资源
     -->
    <context:property-placeholder location="jdbc.properties"/>

    <!--
		利用下述代码可以引入外部配置文件
    	<import resource="common.xml">
    -->

    <!-- 配置数据源,使用${}配置信息 -->
    <bean id="dataSourceBean" class="cn.hnu.spring6.jdbc.MyDataSource">
        <property name="password" value="${password}"/>
        <property name="username" value="${username}"/>
        <!-- spring会优先加载windows系统下的环境变量,可能会导致username变为其他值 -->
        <property name="driver" value="${driverClass}"/>
        <property name="url" value="${url}"/>
    </bean>
</beans>

解决username输出为其他值的方案:

# 在变量名前加上jdbc.
jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring6
jdbc.username=root
jdbc.password=123456

Bean 的作用域

单例和多例

Spring 默认情况下 Bean 是单例的,在 Spring 上下文初始化的时候实例化,每一次调用getBean方法的时候都会返回那个单例的对象。

public class SpringBeanScopeTest {
    @Test
    public void testBeanScope() {

        //执行这句话的时候创建对象
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");

        //获取的是单例对象
        SpringBean sb = applicationContext.getBean("sb", SpringBean.class);
    }
}

要想让 Spring 框架以多例的方式创建对象,我们需要修改配置文件中bean标签的属性:

<?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">

    <!-- 多例:prototype   单例:singleton -->
    <bean id="sb" class="cn.hnu.spring6.bean.SpringBean" scope="prototype"/>
</beans>
public class SpringBeanScopeTest {
    @Test
    public void testBeanScope() {

        //多例模式下,这句代码便不会执行构造函数
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
        System.out.println("-----");
        
        
        //多例模式下,每一次getBean的时候才执行构造函数并创建一个对象
        SpringBean sb = applicationContext.getBean("sb", SpringBean.class);

        SpringBean sb2 = applicationContext.getBean("sb", SpringBean.class);

        SpringBean sb3 = applicationContext.getBean("sb", SpringBean.class);

    }
}

scope 的其他选项

scope 还有其他的选项:

  1. singleton:默认的,单例。
  2. prototype:原型。每调用一次getBean方法则获取一个新的 Bean 对象。或每次注入的时候都是新对象。
  3. request:一个请求对应一个 Bean。仅限于在 WEB 应用中使用
  4. session:一个会话对应一个 Bean。仅限于在 WEB 应用中使用
  5. global session:portlet 应用中专用的。如果在 Servlet 的 WEB 应用中使用 global session 的话,和 session一个效果。(portlet 和 servlet都是规范。servlet 运行在 servlet 容器中,例如 Tomcat。portlet 运行在 portlet 容器中。)
  6. application:一个应用对应一个 Bean。仅限于在 WEB 应用中使用
  7. websocket:一个 websocket 生命周期对应一个 Bean。仅限于在 WEB 应用中使用
  8. 自定义 scope:很少使用。

自定义 scope(了解)

先来看示例代码:

@Test
public void testThreadScope() {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");

    //此时的bean是单例的
    SpringBean sb = applicationContext.getBean("sb", SpringBean.class);
    System.out.println(sb);

    //启动新线程
    new Thread(new Runnable() {
        @Override
        public void run() {
            //bean是单例的
            SpringBean sb1 = applicationContext.getBean("sb", SpringBean.class);
            System.out.println(sb1);
        }
    }).start();
}

要想自定义线程 scope,需要在配置文件进行注册:

<!-- 使用自定义范围配置器 -->
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
    <property name="scopes">
        <map>
            <!-- 给scope起名字 -->
            <entry key="myThread">
                <!-- 使用自定义线程范围的spring类 -->
                <bean class="org.springframework.context.support.SimpleThreadScope"/>
            </entry>
        </map>
    </property>
</bean>

注册后使用 scope:

<bean id="sb" class="cn.hnu.spring6.bean.SpringBean" scope="myThread"/>

完成后在执行上述的 java 代码,便会发现我们成功地让每一个线程都生成一个新的 bean 了。

GoF 之工厂模式

设计模式:一种可以被重复利用的解决方案。

GoF(Gang of Four):四人组。《 Design Patterns:Elements of Reusable Object-Oriented Software 》(即《设计模式》一书),1995年由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著。这几位作者常被称为“四人组(Gang of Four)”。

《设计模式》一书描述了 23 种设计模式,不过除了 GoF 23 种设计模式之外,还有其它的设计模式,比如:JavaEE 的设计模式(DAO 模式、MVC 模式等)。

工厂模式属于创建型设计模式,Spring 中大量使用了工厂模式。

工厂模式有三种形态:

  1. 简单工厂模式(Simple Factory):不属于 23 种设计模式之一。简单工厂模式又叫做:静态工厂方法模式。简单工厂模式是工厂方法模式的一种特殊实现。
  2. 工厂方法模式(Factory Method):是 23 种设计模式之一。
  3. 抽象工厂模式(Abstract Factory):是 23 种设计模式之一。

简单工厂模式

简单工厂中有三种角色:

  1. 抽象产品。
  2. 具体产品。
  3. 工厂类。

客户端不需要关心生产过程的细节,只负责使用即可。生产和消费分离。但缺点便是可扩展性弱,类与类之间耦合度较高。

工厂方法模式

工厂方法中有四种角色:

  1. 抽象产品。
  2. 具体产品。
  3. 抽象工厂。
  4. 具体工厂。

该模式符合 OCP 原则,但是增加一个产品会比较麻烦,会使得系统的复杂度上升。

抽象工厂模式

抽象工厂模式相对于工厂方法模式来说,就是工厂方法模式是针对一个产品系列的,而抽象工厂模式是针对多个产品系列的,即工厂方法模式是一个产品系列一个工厂类,而抽象工厂模式是多个产品系列一个工厂类

抽象工厂模式特点:抽象工厂模式是所有形态的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂模式是指当有多个抽象角色时,使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体的情况下,创建多个产品族中的产品对象。它有多个抽象产品类,每个抽象产品类可以派生出多个具体产品类,一个抽象工厂类,可以派生出多个具体工厂类,每个具体工厂类可以创建多个具体产品类的实例。每一个模式都是针对一定问题的解决方案,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式针对的是多个产品等级结果。

抽象中有四种角色:

  1. 抽象产品。
  2. 具体产品。
  3. 抽象工厂。
  4. 具体工厂。

优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。缺点:产品族扩展非常困难,要增加一个系列的某一产品,既要在 Abstract Factory 里加代码,又要在具体的里面加代码。

Bean 的实例化方式

Spring 为 Bean 的创建提供了多种实例化方式,使得 Bean 的创建更加灵活,通常包括 4 种方式:

  1. 构造方法实例化。
  2. 简单工厂实例化。
  3. factory-bean实例化。
  4. FactoryBean接口实例化。

构造方法实例化

在配置文件中直接配置类全路径,Spring 会自动调用该类的无参构造方法来实例化 Bean。

<?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="sb" class="cn.hnu.spring6.bean.SpringBean"/>
    
</beans>

简单工厂实例化

我们需要提供一个产品类:

package cn.hnu.spring6.bean;
//产品类
public class Star {
    public Star() {
        System.out.println("Star无参构造");
    }
}

再提供一个工厂类,用来生产产品:

package cn.hnu.spring6.bean;
//简单工厂类模式中的工厂类角色
public class StarFactory {
    //工厂类中需要提供一个静态方法
    public static Star get() {
        //Star对象实际上还是开发者自己new的
        return new Star();
    }
}

最后在配置文件中,我们利用factory-method指定工厂类的生产方法:

<?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">

    <!--
        通过简单工厂模式实例化
        需要告诉spring调用哪个类的哪个方法获取bean
        factory-method指定的是工厂类中的静态方法
     -->
    <bean id="starBean" class="cn.hnu.spring6.bean.StarFactory" factory-method="get"/>

</beans>

factory-bean 实例化

实际上就是通过工厂方法模式进行实例化。在本实例化方法中,同样也需要有产品类和工厂类:

//产品类
public class Gun {
    public Gun() {
        System.out.println("Gun无参构造");
    }
}

//工厂类
public class GunFactory {

    public GunFactory() {
        System.out.println("GunFactory无参构造");
    }

    //工厂方法模式中的具体工厂角色中的方法是实例方法,不是静态方法
    public Gun get() {
        //实际上对象还是开发者自己new的
        return new Gun();
    }
}

配置文件中需要指明哪个类是工厂,哪个方法是生产产品的方法:

<?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">

    <!--
        通过factory-bean实例化
        工厂方法中工厂的方法并不是静态的
        故我们需要既创建工厂,又要让工厂创建对象
        factory-bean告诉spring哪个对象是工厂
        factory-method告诉spring工厂中哪个方法是生产bean的方法
    -->
    <bean id="gunFactory" class="cn.hnu.spring6.bean.GunFactory"/>
    <bean id="gunBean" factory-bean="gunFactory" factory-method="get"/>

</beans>

FactoryBean 接口实例化

以上的第三种方式中,factory-bean 是我们自定义的,factory-method 也是我们自己定义的。在 Spring 中,当我们编写的类直接实现 FactoryBean 接口之后,factory-bean 不需要指定了,factory-method 也不需要指定了。

同样,我们也需要产品类和工厂类:

//产品类
public class Person {
    public Person() {
        System.out.println("Person无参构造");
    }
}

//工厂类,实现FactoryBean接口
public class PersonFactoryBean implements FactoryBean<Person> {
    //获取bean的方法重写
    @Override
    public Person getObject() throws Exception {
        //最终对象的创建还是开发者自己new的
        return new Person();
    }

    @Override
    public Class<?> getObjectType() {
        return null;
    }

    //这个方法默认实现返回true,表示单例,如果想多例,直接修改为return false即可
    @Override
    public boolean isSingleton() {
        return true;
    }
}

因为我们的工厂类实现了 FactoryBean 接口,所以我们在配置文件中配置信息的时候就比较简单了:

<?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">

    <!--
        通过FactoryBean接口实现
        实际上就是factory-bean方法的简化
        无需我们手动指定factory-bean和factory-method
    -->
    <!-- 通过PersonFactoryBean返回Person -->
    <bean id="personBean" class="cn.hnu.spring6.bean.PersonFactoryBean"/>

</beans>

BeanFactory 和 FactoryBean 的区别

BeanFactory 是 Spring IoC 容器的顶级对象,在 Spring 的 IoC 容器中,负责创建 Bean 对象,本质是一个工厂。

而 FactoryBean 是一个接口,实现了该接口的 Bean 对象是一个能够辅助 Spring 实例化 Bean 对象。

注入自定义 Date

前面我们探讨过,Date 属于 Spring 类中的简单类型,通过value标签赋值,但是直接通过该方式赋值比较困难,原因是其对 Date 的格式要求比较严格且不符合我们的生活习惯。

我们可以结合上述的工厂模式来创建 Date 对象:

public class Student {
    //Date在Spring中属于简单类型,但是注入格式不方便
    private Date birth;

    public void setBirth(Date birth) {
        this.birth = birth;
    }

    @Override
    public String toString() {
        return "Student={" +
                "birth=" + birth +
                '}';
    }
}

//日期工厂,用来根据传入的字符串生产日期
public class DateFactory implements FactoryBean<Date> {

    private String strDate;

    public DateFactory(String strDate) {
        this.strDate = strDate;
    }

    @Override
    public Date getObject() throws Exception {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        return sdf.parse(this.strDate);
    }

    @Override
    public Class<?> getObjectType() {
        return null;
    }
}

配置文件:

<?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="nowDate" class="java.util.Date"/>
     -->
    <!-- 先利用日期工厂创建日期 -->
    <bean id="date" class="cn.hnu.spring6.bean.DateFactory">
        <constructor-arg index="0" value="2004-08-09"/>
    </bean>

    <!-- 再利用set注入创建学生对象 -->
    <bean id="student" class="cn.hnu.spring6.bean.Student">
        <property name="birth" ref="date"/>
    </bean>

</beans>

Bean 的生命周期

Bean 的五步生命周期

Bean 的生命周期可以划分为 5 步:

  1. 实例化 Bean。
  2. Bean 属性赋值。
  3. 初始化 Bean(调用init方法)。
  4. 使用 Bean。
  5. 销毁 Bean(调用destroy方法)。

生命周期的管理可以参考AbstractAutowireCapableBeanFactory类的doCreateBean方法。

现有User类:

public class User {

    private String name;

    public User() {
        System.out.println("1.构造方法执行");
    }

    public void setName(String name) {
        System.out.println("2.属性赋值");
        this.name = name;
    }

    //初始化方法需要自己写自己配,方法名随意
    public void initBean() {
        System.out.println("3.初始化bean");
    }

    public void destroyBean() {
        System.out.println("5.销毁bean");
    }
}

配置文件中需要指定init方法和destroy方法:

<?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">

    <!-- 需要指定init方法和destroy方法 -->
    <bean id="user" class="cn.hnu.spring6.bean.User" init-method="initBean" destroy-method="destroyBean">
        <property name="name" value="张三"/>
    </bean>
</beans>

销毁 Bean 的方法是关闭 Spring 容器:

public class BeanLifeCycleTest {
    @Test
    public void testBeanLifeCycleFive() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        User user = applicationContext.getBean("user", User.class);
        System.out.println("4.使用bean");
        //销毁bean,需要关闭spring容器才会销毁
        //父转子,调用close方法
        ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;
        context.close();
    }
}

Bean 的七步生命周期

在上述第三步初始化 Bean 之前和之后可以插入代码。要实现在初始化前后的操作,我们需要加入“Bean 后处理器”,需要实现BeanPostProcessor类,并重写beforeafter方法。但是需要注意,此时的“Bean”后处理器会作用到配置文件中的所有 Bean。此时 Bean 的七步生命周期分为:

  1. 实例化 Bean。
  2. Bean 属性赋值。
  3. 执行 Bean 后处理器的before方法。
  4. 初始化 Bean(调用init方法)。
  5. 执行 Bean 后处理器的after方法。
  6. 使用 Bean。
  7. 销毁 Bean(调用destroy方法)。

Bean 后处理器:

package cn.hnu.spring6.bean;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

public class LogBeanPostProcessor implements BeanPostProcessor {
    /**
     * before方法
     * @param bean 刚创建的bean对象
     * @param beanName bean的名字
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("执行bean后处理器的before方法");
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("执行bean后处理器的after方法");
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}

配置文件:

<?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">

    <!-- 需要指定init方法和destroy方法 -->
    <bean id="user" class="cn.hnu.spring6.bean.User" init-method="initBean" destroy-method="destroyBean">
        <property name="name" value="张三"/>
    </bean>

    <!-- 配置bean后处理器,bean后处理器将作用于所有的bean -->
    <bean class="cn.hnu.spring6.bean.LogBeanPostProcessor"/>
    
</beans>

Bean 的十步生命周期

如果根据源码跟踪,可以划分更细的颗粒度的步骤,共 10 步:

  1. 实例化 Bean。
  2. Bean 属性赋值。
  3. 检查 Bean 是否实现了Aware的相关接口,并设置相关依赖。
  4. 执行 Bean 后处理器的before方法。
  5. 检查 Bean 是否实现了InitializingBean接口,并调用接口方法。
  6. 初始化 Bean(调用init方法)。
  7. 执行 Bean 后处理器的after方法。
  8. 使用 Bean。
  9. 检查 Bean 是否实现了Disposable接口,并调用接口方法。
  10. 销毁 Bean(调用destroy方法)。

实现了Aware相关接口:

  1. 实现BeanNameAware,Spring 会将 Bean 的名字传递给 Bean。
  2. 实现BeanClassLoaderAware,Spring 会将 Bean 的名字传递给 Bean。
  3. 实现BeanFactroyAware,Spring 会将 Bean 工厂对象传递给 Bean。
package cn.hnu.spring6.bean;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;

public class User implements BeanNameAware,
        BeanClassLoaderAware, BeanFactoryAware,
        InitializingBean, DisposableBean {

    private String name;

    public User() {
        System.out.println("1.构造方法执行");
    }

    public void setName(String name) {
        System.out.println("2.属性赋值");
        this.name = name;
    }

    //初始化方法需要自己写自己配,方法名随意
    public void initBean() {
        System.out.println("3.初始化bean");
    }

    public void destroyBean() {
        System.out.println("5.销毁bean");
    }

    //Aware接口
    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        System.out.println("使用类加载器");
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("使用beanFactory");
    }

    @Override
    public void setBeanName(String name) {
        System.out.println("使用bean的名字");
    }

    //InitializingBean接口
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("afterPropertiesSet");
    }

    //DisposableBean接口,这个destroy方法会先被调用
    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean调用destroy方法");
    }
}

不同作用域下的不同管理方式

注意:Spring 容器只对单例(Singleton)的 Bean 进行完整的生命周期管理。如果是多例(Prototype),Spring 容器则只负责将该 Bean 初始化完毕,等客户端程序一旦获取到该 Bean 之后,Spring 容器就不再管理该对象的生命周期了(使用 Bean 这一步后就不管了,也就是说只负责前八步)。

自己 new 的对象交给 Spring 管理

有些时候我们自己创建的 Java 对象需要交给 Spring 容器管理,这个时候可以利用DefaultListableBeanFactory来完成。

public class BeanLifeCycleTest {
    @Test
    public void testRegisterBean() {
        //自己创建的对象,目前不在spring的管辖范围内
        Student student = new Student();
        System.out.println(student);

        //接下来我们把student交给spring管理(注册)
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        factory.registerSingleton("studentBean", student);

        //从spring容器中获取
        Student studentBean = factory.getBean("studentBean", Student.class);
        System.out.println(student);
    }
}

Bean 的循环依赖

A 中有 B,B 中有 A,A 与 B 就是循环依赖关系。

singleton 下的 set 注入产生的循环依赖

现有丈夫类和妻子类,二者循环依赖:

package cn.hnu.spring6.bean;

//丈夫类
public class Husband {
    private String name;
    private Wife wife;
	//setter...
}

//妻子类
public class Wife {
    private String name;
    private Husband husband;
	//setter...
}

在 singleton + set 注入条件下,因为所有对象都是单例的,只有一个,并且 Spring 不等 bean 属性是否全部赋值,直接曝光该 bean,所以直接进行配置是没有问题的:

<?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">

    <!-- singleton + set注入的循环依赖 -->
    <!--
        singleton中每个对象都只有一个
        在这种模式下,spring对bean的管理主要有两个阶段
            第一阶段:spring实例化bean的时候,只要其中任意一个bean实例化完成后,不等属性赋值,直接“曝光”
            第二阶段:bean曝光之后,再进行属性的赋值
     -->
    <bean id="husband" class="cn.hnu.spring6.bean.Husband">
        <property name="name" value="张三"/>
        <property name="wife" ref="wife"/>
    </bean>

    <bean id="wife" class="cn.hnu.spring6.bean.Wife">
        <property name="name" value="李四"/>
        <property name="husband" ref="husband"/>
    </bean>
</beans>

多例模式下的 set 注入产生的循环依赖

当一个 bean 需要创建的时候,会去创建其依赖类,而依赖类在创建的时候,又会去创建它自己的依赖类,如此往复,报出BeanCurrentlyInCreationException。解决方案是其中一个bean改为单例模式。

<?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">

    <!-- prototype + set注入的循环依赖 -->
    <!--
        BeanCurrentlyInCreationException,在bean对象创建过程中出现异常
        解决方案是其中一个bean改为单例模式
     -->
    <bean id="husband" class="cn.hnu.spring6.bean.Husband" scope="prototype">
        <property name="name" value="张三"/>
        <property name="wife" ref="wife"/>
    </bean>

    <bean id="wife" class="cn.hnu.spring6.bean.Wife" scope="prototype">
        <property name="name" value="李四"/>
        <property name="husband" ref="husband"/>
    </bean>
</beans>

构造模式下注入

构造模式下注入依旧没办法解决循环依赖的问题,原因也是 bean 对象创建的时候不会提前曝光,导致重复创建 bean 对象。

<?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="husband" class="cn.hnu.spring6.bean2.Husband">
        <constructor-arg index="0" value="张三"/>
        <constructor-arg index="1" ref="wife"/>
    </bean>

    <bean id="wife" class="cn.hnu.spring6.bean2.Wife">
        <constructor-arg index="0" value="李四"/>
        <constructor-arg index="1" ref="husband"/>
    </bean>
</beans>

循环依赖分析

Spring 为什么可以解决 set + singleton 模式下循环依赖?根本的原因在于:这种方式可以做到将“实例化 Bean”和“给 Bean属性赋值”这两个动作分开去完成。实例化 Bean 的时候:调用无参数构造方法来完成。此时可以先不给属性赋值,可以提前将该 Bean 对象“曝光”给外界。给 Bean 属性赋值的时候:调用 setter 方法来完成。两个步骤是完全可以分离开去完成的,并且这两步不要求在同一个时间点上完成。

也就是说,Bean 都是单例的,我们可以先把所有的单例 Bean 实例化出来,放到一个集合当中(我们可以称之为缓存),所有的单例 Bean 全部实例化完成之后,以后我们再慢慢的调用 setter 方法给属性赋值。这样就解决了循环依赖的问题。

Bean 的创建依赖AbstractAutowireCapableBeanFactory这个类,在这个类里面,bean 的创建和赋值代码便是分离开的。这个类在创建对象的时候,调用了DefaultSingletonBeanRegistry这个类的addSingletonFactory方法,这其中涉及到三级缓存(Map 集合中的 key 存储的是 bean 的 id):

  1. Map<String, Object> singletonObjects:一级缓存,存储的是完整的单例 bean 对象,也就是说这个缓存中的 bean 对象的属性都赋值了。
  2. Map<String, Object> earlySingletonObjects:二级缓存,存储的是早期 bean 单例对象,这个缓存中的单例 bean 对象的属性没有赋值,只是一个早期的实例对象。
  3. Map<String, ObjectFactory<?>> singletonFactories:三级缓存:存储的是制造早期单例 bean 对象的工厂对象。每一个单例 bean 对象都会对应一个单例工厂对象。所谓的“曝光”就是往这个三级缓存中增加工厂对象,使得可以将这个 bean 对象直接创建出来。

回顾反射机制

我们知道,调用一个方法,需要四个要素:

  1. 调用哪个对象。
  2. 调用哪个方法。
  3. 调用方法的时候传什么参数。
  4. 方法执行后返回什么结果。

即使是使用反射机制调用方法,也同样需要这几个要素。

反射方法的调用

利用反射机制调用方法示例:

package cn.hnu.spring6.reflect;

public class SomeService {
    public void doSome() {
        System.out.println("public void doSome()执行");
    }

    public String doSome(String s) {
        System.out.println("public String doSome(String s)执行");
        return s;
    }

    public String doSome(String s, int i) {
        System.out.println("public String doSome(String s, int i)执行");
        return s + i;
    }
}
package cn.hnu.reflect;

import java.lang.reflect.Method;

public class Test2 {
    public static void main(String[] args) throws Exception {
        //获取字节码文件
        Class<?> clazz = Class.forName("cn.hnu.spring6.reflect.SomeService");
        
        //获取方法
        Method doSome3 = clazz.getDeclaredMethod("doSome", String.class, int.class);
        
        //调用方法
        //四要素:哪个对象,哪个方法,传什么参数,返回什么结果
        Object obj = clazz.getDeclaredConstructor().newInstance();
        Object result = doSome3.invoke(obj, "hello world", 1024);
        System.out.println(result);
    }
}

SpringDI 核心实现

假设现在已知以下信息:

  • 有这样一个类,类名叫cn.hnu.spring6.reflect.User,且符合 javabean 规范。
  • 我们还知道这个类当中有一个属性,属性名叫 age。
  • 并且我们还知道 age 属性的类型是 int 类型。
  • 我们接下来使用反射机制调用相关方法给 age 赋值。
package cn.hnu.reflect;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Test3 {
    public static void main(String[] args) throws Exception{
        //已知条件
        String className = "cn.hnu.spring6.reflect.User";
        String propertyName = "age";

        //获取字节码文件
        Class<?> clazz = Class.forName(className);
        //获取方法名
        String methodName = "set" + propertyName.toUpperCase().charAt(0)
                + propertyName.substring(1);
        //根据属性名获取对应字段
        Field ageField = clazz.getDeclaredField(propertyName);
        //获取字段类型
        Class<?> ageFieldType = ageField.getType();
        //获取方法
        Method setAgeMethod = clazz.getDeclaredMethod(methodName, ageFieldType);
        //获取对象
        Object obj = clazz.getDeclaredConstructor().newInstance();
        //调用方法
        setAgeMethod.invoke(obj, 18);

        System.out.println(obj);
    }
}

手写 Spring 框架

Spring IoC 实现原理:工厂模式 + 解析 XML + 反射机制。

准备工作

利用 maven 准备依赖:

<dependencies>
    <!-- 引入dom4j组件,解析xml -->
    <dependency>
        <groupId>org.dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>2.1.3</version>
    </dependency>
    
    <dependency>
        <groupId>jaxen</groupId>
        <artifactId>jaxen</artifactId>
        <version>1.2.0</version>
    </dependency>

    <!-- 引入junit测试依赖 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

站在框架使用者的角度准备类和配置文件:

<?xml version="1.0" encoding="UTF-8"?>

<beans>
	<!-- User类,有name和age两个属性,对外提供set方法 -->
    <bean id="user" class="cn.hnu.myspring.bean.User">
        <property name="name" value="zhangsan"/>
        <property name="age" value="30"/>
    </bean>

    <!-- UserDao类,包含一个insert方法,代表向数据库保存用户信息 -->
    <bean id="userDao" class="cn.hnu.myspring.bean.UserDao"/>

    <!-- UserService,有userDao一个属性,并包含一个save方法,代表service层调用dao层方法 -->
    <bean id="userService" class="cn.hnu.myspring.bean.UserService">
        <property name="userDao" ref="userDao"/>
    </bean>

</beans>

核心接口实现

依照 Spring 框架,我们也编写一个接口ApplicationContext和其实现类ClassPathXmlApplicationContext

package org.myspringframework.core;

//myspring框架应用上下文接口
public interface ApplicationContext {
    /**
     * 根据bean的名称获取对应的bean对象
     * @param beanName bean名称,也是xml文件中bean的id
     * @return 返回对应的单例bean对象
     */
    Object getBean(String beanName);
}
package org.myspringframework.core;

import java.util.HashMap;
import java.util.Map;

public class ClassPathXmlApplicationContext implements ApplicationContext {

    private Map<String, Object> singletonObjects = new HashMap<>();

    /**
     * 解析myspring配置文件,初始化所有的bean对象,
     * 并将bean存放到singletonObjects这个Map集合中
     * @param configLocation myspring配置文件的路径
     */
    public ClassPathXmlApplicationContext(String configLocation) {
        
    }

    @Override
    public Object getBean(String beanName) {
        return null;
    }
}

实例化 Bean

public class ClassPathXmlApplicationContext implements ApplicationContext {

    private Map<String, Object> singletonObjects = new HashMap<>();

    /**
     * 解析myspring配置文件,初始化所有的bean对象,
     * 并将bean存放到singletonObjects这个Map集合中
     * @param configLocation myspring配置文件的路径
     */
    public ClassPathXmlApplicationContext(String configLocation) {
        try {
            //创建解析xml的核心对象
            SAXReader reader = new SAXReader();

            //获取一个输入流,指向类路径下的资源
            InputStream in = ClassLoader.getSystemClassLoader()
                    .getResourceAsStream(configLocation);

            //获取xml文档对象
            Document document = reader.read(in);

            /*
            * 获取所有的bean标签,需要写两个斜杠
            * 这里是dom4j的xpath语法
            * 单斜杠表示直接选择子节点,意味着我们只会选择根节点下的bean子节点
            * 双斜杠表示选择所有的子节点,意味着我们会递归地选择根节点下的所有bean子节点
            * */
            List<Node> nodes = document.selectNodes("//bean");

            //遍历bean标签
            nodes.forEach(node -> {
                try {
                    //向下转型的目的是使用Element接口里更加丰富的方法
                    Element beanElt = (Element) node;

                    //获取id属性
                    String id = beanElt.attributeValue("id");
                    //获取类名
                    String className = beanElt.attributeValue("class");

                    //通过反射机制创建对象,将其放入Map集合中提前曝光
                    Class<?> clazz = Class.forName(className);
                    //调用构造方法实例化bean
                    Object obj = clazz.getDeclaredConstructor().newInstance();
                    //将bean曝光
                    singletonObjects.put(id, obj);

                } catch (Exception e) {
                    e.printStackTrace();
                }
            });

            //可以输出map对象观察一下bean的实例化情况
            //singletonObjects.keySet().forEach(key -> {
            //    System.out.print("key = " + key);
            //   Object obj = singletonObjects.get(key);
            //   System.out.println(" value = " + obj);
            //});

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

给 Bean 属性赋值

public class ClassPathXmlApplicationContext implements ApplicationContext {

    private Map<String, Object> singletonObjects = new HashMap<>();

    public ClassPathXmlApplicationContext(String configLocation) {
        try {
            SAXReader reader = new SAXReader();

            InputStream in = ClassLoader.getSystemClassLoader()
                    .getResourceAsStream(configLocation);

            Document document = reader.read(in);

            List<Node> nodes = document.selectNodes("//bean");

            nodes.forEach(node -> {
                try {
                    Element beanElt = (Element) node;

                    String id = beanElt.attributeValue("id");
                    String className = beanElt.attributeValue("class");

                    Class<?> clazz = Class.forName(className);
                    Object obj = clazz.getDeclaredConstructor().newInstance();
                    singletonObjects.put(id, obj);

                } catch (Exception e) {
                    e.printStackTrace();
                }
            });

            //再次遍历bean标签,主要是给对象赋值
            nodes.forEach(node -> {
                try {
                    Element beanElt = (Element) node;

                    //获取id和类名
                    String id = beanElt.attributeValue("id");
                    String className = beanElt.attributeValue("class");
                    Class<?> clazz = Class.forName(className);

                    //获取该bean标签下所有的property标签
                    List<Element> properties = beanElt.elements("property");
                    properties.forEach(property -> {
                        try {
                            //获取属性名
                            String propertyName = property.attributeValue("name");
                            //set方法名
                            String setMethodName = "set" + propertyName.toUpperCase().charAt(0)
                                    + propertyName.substring(1);

                            //获取属性类型
                            Field declaredField = clazz.getDeclaredField(propertyName);

                            //获取set方法
                            Method setMethod = clazz.getDeclaredMethod(setMethodName,
                                                                       declaredField.getType());

                            //获取属性值
                            String value = property.attributeValue("value");
                            String ref = property.attributeValue("ref");
                            if(value != null) { //简单类型
                                /*
                                * myspring框架支持的简单类型有:
                                * byte short int long float double boolean char
                                * Byte Short Integer Long Float Double Boolean Character String
                                * */

                                //获取属性类型
                                String fieldSimpleName = declaredField.getType().getSimpleName();
                                Object actualValue = switch (fieldSimpleName) {
                                    case "byte", "Byte" -> Byte.parseByte(value);
                                    case "short", "Short" -> Short.parseShort(value);
                                    case "int", "Integer" -> Integer.parseInt(value);
                                    case "long", "Long" -> Long.parseLong(value);
                                    case "float", "Float" -> Float.parseFloat(value);
                                    case "double", "Double" -> Double.parseDouble(value);
                                    case "boolean", "Boolean" -> Boolean.parseBoolean(value);
                                    case "char", "Character" -> value.charAt(0);
                                    case "String" -> value;
                                    default -> throw new IllegalStateException("Unexpected value: " + fieldSimpleName);
                                };
                                setMethod.invoke(singletonObjects.get(id), actualValue);
                            }
                            if(ref != null) {   //非简单类型
                                setMethod.invoke(singletonObjects.get(id), 
                                                 singletonObjects.get(ref));
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    });
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public Object getBean(String beanName) {
        return singletonObjects.get(beanName);
    }
}

Spring IoC 注解式开发

注解的存在主要是为了简化 XML 的配置,Spring6 倡导全注解开发

注解的回顾

我们来回顾一下:

  1. 注解怎么定义,注解中的属性怎么定义?
  2. 注解怎么使用?
  3. 通过反射机制怎么读取注解?

自定义注解及使用:

//自定义注解
//如果属性名是value,value=可以省略
//如果属性值是一个数组,且数组只有一个元素,{}可以省略


//Target:标注注解的注解,叫做元注解,Target注解用来修饰Component注解可以出现的位置
//以下表示Component注解可以出现在类上和属性上
@Target({ElementType.TYPE, ElementType.FIELD})
//Retention:元注解,用来标注Component注解最终保留在class文件中,并且可以被反射机制读取
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
    //定义注解的属性   String是属性类型,value是属性名
    String value();
    String[] names();
}

//注解的使用
@Component(value="userBean", names={})
public class User {
    private String name;
}

反射机制读取注解:

package cn.hnu.spring6.client;

import cn.hnu.spring6.annotation.Component;

import java.util.Arrays;

public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        //通过反射获取类中的注解
        Class<?> clazz = Class.forName("cn.hnu.spring6.bean.User");
        //判断类上面有没有注解
        if (clazz.isAnnotationPresent(Component.class)) {
            //如果有,再获取类上的注解
            Component annotation = clazz.getAnnotation(Component.class);
            //访问注解属性
            System.out.println(annotation.value());
            System.out.println(Arrays.toString(annotation.names()));
        }
    }
}

组件扫描原理

需求:给出一个包名,包下类的个数随机,现需要获取所有携带某个特定注解的类,实例化后存入 Map 集合中。

package cn.hnu.spring6.client;

import cn.hnu.spring6.annotation.Component;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class ComponentScan {
    public static void main(String[] args) throws Exception {
        //最终的MAP集合
        Map<String, Object> map = new HashMap<>();
        //已知包名
        String packageName = "cn.hnu.spring6.bean";
        String packagePath = packageName.replaceAll("\\.", "/");
        //利用类加载器获取路径
        URL url = ClassLoader.getSystemClassLoader().getResource(packagePath);

        //获取绝对路径
        String fileName = url.getPath();
        //绝对路径使用UTF-8解码修正
        fileName = URLDecoder.decode(fileName, StandardCharsets.UTF_8);

        File file = new File(fileName);
        File[] files = file.listFiles();
        if (files != null) {
            Arrays.stream(files).forEach(f -> {
                try {
                    //通过反射机制解析注解

                    //获取类名 f.getName() --> User.class(字节码文件)
                    String className = packageName + "."
                            + f.getName().split("\\.")[0];
                    Class<?> clazz = Class.forName(className);

                    //通过类名获取注解
                    if (clazz.isAnnotationPresent(Component.class)) {
                        Component annotation = clazz.getAnnotation(Component.class);
                        //获取value值
                        String value = annotation.value();
                        //创建对象
                        Object obj = clazz.getDeclaredConstructor().newInstance();
                        //收录到Map集合中
                        map.put(value, obj);
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }

        //打印最终的map集合
        map.keySet().forEach(key -> {
            System.out.print("key = " + key);
            System.out.println(" value = " + map.get(key));
        });
    }
}

声明 Bean 的注解

负责声明 Bean 的注解常见的有以下几个:@Component(组件)、@Controller(控制器)、@Service(业务)、@Respository(DAO)。其中,后面三个注解是第一个注解的别名,这么设计是为了增加程序的可读性。

@Target({ElementType.TYPE})	//该注解只能出现在类上
@Retention(RetentionPolicy.RUNTIME)	//可以被反射读取
@Documented
@Indexed
public @interface Component {
    String value() default "";
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
    @AliasFor(	//别名,意思是Controller是Component的别名
        annotation = Component.class
    )
    String value() default "";
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

Spring 注解的使用

使用上述注解分四步:

  1. 添加 aop 依赖。(spring-context 已经包含)
  2. 在配置文件中添加 context 命名空间。
  3. 在配置文件中指定扫描的包。
  4. 在 Bean 类上使用注解。
<?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="cn.hnu.spring6.bean"/>

</beans>
@Component("userBean")	//如果不起名,则会以类名(第一个字母小写)为id
public class User {
	//使用Component注解
}

解决多个包扫描问题

<?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="cn.hnu.spring6.bean,cn.hnu.spring6.dao"/>
    
    <!-- 也可以直接指定父包,但是会降低一部分效率 -->
    <context:component-scan base-package="cn.hnu.spring6"/>
    
</beans>

选择性实例化 Bean

假设在某个包下有很多 Bean,有的 Bean 上标注了 Component,有的标注了 Controller,有的标注了 Service,有的标注了 Repository,现在由于某种特殊业务的需要,只允许其中所有的 Controller 参与 Bean 管理,其他的都不实例化。这应该怎么办呢?

<?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">

    <!--
        第一种方案:use-default-filters="false"
        表示这个包下所有带有声明bean的注解全部失效
        然后使用context:include-filter,明确具体哪个注解生效
     -->
    <context:component-scan base-package="cn.hnu.spring6.bean2" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>

    <!--
        第二种方案:use-default-filters="true"(默认值就是true)
        表示这个包下的所有带有声明bean的注解全部生效
        然后使用context:exclude-filter,明确具体哪个注解失效
    -->
    <context:component-scan base-package="cn.hnu.spring6.bean2">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

</beans>

@Value 注解

使用@Value注解,可以注在属性、setter 方法、构造方法上,给 Bean 属性赋值。

@Component
public class MyDataSource{
    @Value("com.mysql.cj.jdbc.driver")
    private String driver;
    @Value("jdbc:mysql://localhost:3306/spring6")
    private String url;
    @Value("root")
    private String username;
    @Value("123456")
    private String password;
}
@Component
public class MyDataSource{
    private String username;
    private String password;
    
    @Value("root")
    public void setUsername(String username) {
        this.username = username;
    }
    @Value("123456")
    public void setPassword(String password) {
        this.password = password;
    }
}
@Component
public class MyDataSource{
    private String username;
    private String password;

    public MyDataSource(@Value("root") String username,
                        @Value("123") String password) {
        this.username = username;
        this.password = password;
    }
}

@Autowired 和 @Qualifier 注解

@Autowired可以用来注入非简单类型默认根据类型装配

//可以出现在构造方法上,方法上,参数上,属性上,别的注解上
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    boolean required() default true;
}

直接使用@Autowired的缺点是:因为是自动类型装配,所以实现类只能有一个,如果有多个实现类,进行自动装配的时候,Spring 是不知道该装配哪一个的。

@Repository
//实现类只能有一个
public class OrderDaoImpl implements OrderDao {
    @Override
    public void insert() {
        System.out.println("mysql正在保存用户信息");
    }
}

@Service
public class OrderService {

    //Autowired注解使用的时候不需要指定任何属性
    //这个类型根据类型自动装配
    @Autowired
    private OrderDao orderDao;

    public void generate() {
        orderDao.insert();
    }
}

要想解决上述问题,需要@Autowired@Qualifier联合使用,进行按名称装配。

@Repository
public class OrderDaoImpl implements OrderDao {
    @Override
    public void insert() {
        System.out.println("mysql正在保存用户信息");
    }
}

@Repository
public class OrderDaoImpl2 implements OrderDao {
    @Override
    public void insert() {
        System.out.println("redis正在保存用户信息");
    }
}

@Service
public class OrderService {
    //联合使用,按名称装配
    @Autowired
    @Qualifier("orderDaoImpl2")
    private OrderDao orderDao;

    public void generate() {
        orderDao.insert();
    }
}

@Resource 注解

@Resource注解是 JDK 扩展包中的,也就是说属于 JDK 的一部分。所以该注解是标准注解,更加具有通用性

@Resource注解默认根据名称装配 byName,未指定 name 时,使用属性名作为 name。通过 name 找不到的话会自动启动通过类型 byType 装配。

@Resource注解用在属性上、setter 方法上

要注意:如果用 Spring6,要知道 Spring6 不再支持 JavaEE,它支持的是 JakartaEE9。(Oracle 把 JavaEE 贡献给Apache了,Apache 把 JavaEE 的名字改成 JakartaEE 了,大家之前所接触的所有的 javax.*包名统一修改为jakarta.*包名了)

 <!-- Spring6使用的@Resource注解依赖 -->
<dependency>
    <groupId>jakarta.annotation</groupId>
    <artifactId>jakarta.annotation-api</artifactId>
    <version>2.1.1</version>
</dependency>

<!-- Spring5使用的@Resource注解依赖 -->
<dependency>
	<groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
</dependency>
@Service
public class StudentService {

    @Resource(name = "studentDaoImplForMysql")
    private StudentDao studentDao;

    //也可以出现在set方法上
    //@Resource(name = "studentDaoImplForMysql")
    //public void setStudentDao(StudentDao studentDao) {
    //    this.studentDao = studentDao;
    //}
    
    public void deleteStudent() {
        studentDao.deleteById();
    }
}

全注解式开发

我们到目前为止,就算使用了上述讲到的注解,依旧还是需要使用配置文件(进行包扫描),全注解开发目的是为了让我们根本不需要使用配置文件进行开发,我们可以开发一个类来代替配置文件。

//编写一个类代替Spring框架的配置文件
@Configuration
@ComponentScan({"edu.hnu.dao", "edu.hnu.service"})
public class Spring6Config {
}

使用的时候需要用AnnotationConfigApplicationContext这个类来获取配置信息:

@Test
public void testNoXML() {
    AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(Spring6Config.class);
    StudentService studentService = annotationConfigApplicationContext.getBean("studentService", StudentService.class);
    studentService.deleteStudent();
}

JdbcTemplate(了解)

JdbcTemplate 是 Spring 提供的一个 JDBC 模板类,是对 JDBC 的封装,简化 JDBC 代码。当然,你也可以不用,可以让 Spring 集成其他的 ORM 框架,例如:MyBatis、Hibernate 等。接下来,我们就来使用 JdbcTemplate 完成简单的增删改查操作。

使用之前需要加上对于依赖:

<!-- spring-jdbc驱动 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>6.0.6</version>
</dependency>

<!-- mysql依赖 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>

配置文件:

<?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="ds" class="cn.hnu.spring6.bean.MyDataSource">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/test"/>
        <property name="username" value="root"/>
        <property name="password" value="MySQL:040809"/>
    </bean>

    <!-- 配置jdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 使用自己的数据源,只要实现了DataSource的都叫数据源 -->
        <property name="dataSource" ref="ds"/>
    </bean>
</beans>

使用 JdbcTemplate :

public class SpringJDBCTest {
    @Test
    public void testJDBC() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        System.out.println(jdbcTemplate);
    }
}

在配置文件中配置:

@Configuration
@ComponentScan({"cn.hnu.bank"})
public class Spring6Config {
    
   @Bean(name = "dataSource")
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/test");
        dataSource.setUsername("root");
        dataSource.setPassword("MySQL:040809");
        return dataSource;
    }

    @Bean(name = "jdbcTemplate")
    //Spring框架会默认根据注解配置dataSource
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
}

基础操作

public class SpringJDBCTest {
    @Test
    public void testJDBC() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        //新增
        String sql = "insert into spring6_user(real_name, age) values(?, ?)";
        jdbcTemplate.update(sql, "张三", 18);
        
        //修改
        String sql = "update spring6_user set age = ? where real_name = ?";
        jdbcTemplate.update(sql, 32, "张三");
        
        //删除
        String sql = "delete from spring6_user where real_name = ?";
        jdbcTemplate.update(sql, "赵六");
        
        //查询一个对象
        String sql = "select * from spring6_user where id = ?";
        //BeanPropertyRowMapper是用来做数据和对象行字段映射的(real_name --> realName)
        User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), 1);
        System.out.println(user);
        
        //查询多个对象
        String sql = "select * from spring6_user";
        List<User> users = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
        System.out.println(users);
        
        //查询一个值
        String sql = "select count(*) from spring6_user";
        Integer total = jdbcTemplate.queryForObject(sql, int.class);
        System.out.println(total);
    }
}

批量操作

public class SpringJDBCTest {
    @Test
    public void testJDBC() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        //批量添加
        String sql = "insert into spring6_user values(?, ?, ?)";
        //准备三个对象
        Object[] obj1 = {null, "小明", 18};
        Object[] obj2 = {null, "小红", 19};
        Object[] obj3 = {null, "小王", 20};
        //添加到List集合
        List<Object[]> list = new ArrayList<>();
        Collections.addAll(list, obj1, obj2, obj3);
        //执行sql语句
        int[] counts = jdbcTemplate.batchUpdate(sql, list);
        System.out.println(Arrays.toString(counts));//[1, 1, 1]
        
        //批量修改
        String sql = "update spring6_user set real_name = ? where id = ?";
        //准备三个对象
        Object[] obj1 = {"大明", 5};
        Object[] obj2 = {"大红", 6};
        Object[] obj3 = {"老王", 7};
        //添加到List集合
        List<Object[]> list = new ArrayList<>();
        Collections.addAll(list, obj1, obj2, obj3);
        //执行sql语句
        jdbcTemplate.batchUpdate(sql, list);
        
        //批量删除
        String sql = "delete from spring6_user where id = ?";
        //准备数据
        Object[] obj1 = {5};
        Object[] obj2 = {6};
        Object[] obj3 = {7};
        //添加到List集合
        List<Object[]> list = new ArrayList<>();
        Collections.addAll(list, obj1, obj2, obj3);
        //执行sql
        jdbcTemplate.batchUpdate(sql, list);
    }
}

回调函数

public class SpringJDBCTest {
    @Test
    public void testJDBC() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        //回调函数(如果你想写jdbc代码,你可以使用callback回调函数)
        String sql = "select * from spring6_user where id = ?";
        //注册回调函数
        User user = jdbcTemplate.execute(sql, new PreparedStatementCallback<User>() {
            @Override
            public User doInPreparedStatement(PreparedStatement ps)
                    throws SQLException, DataAccessException {
                User user = null;
                ps.setInt(1, 2);
                ResultSet resultSet = ps.executeQuery();
                if(resultSet.next()) {
                    int id = resultSet.getInt("id");
                    String realName = resultSet.getString("real_name");
                    int age = resultSet.getInt("age");
                    user = new User(id, realName, age);
                }
                return user;
            }
        });
        System.out.println(user);
    }
}

使用 Druid 连接池

配置 Druid 依赖:

 <!-- druid连接池 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.20</version>
</dependency>

引入连接池:

<!-- 引入druid连接池 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/test"/>
    <property name="username" value="root"/>
    <property name="password" value="MySQL:040809"/>
</bean>

GoF 之代理模式

代理模式属于结构型模式,代理模式的作用:

  1. 保护某个对象,通过代理对象去完成某个行为。
  2. 当一个对象欠缺功能,需要给某个对象的功能进行功能增强的时候,可以考虑找一个代理进行增强。
  3. A 对象和 B 对象无法直接交互时,也可以通过代理模式来解决。

代理模式中有三大角色:

  1. 目标对象。(演员)
  2. 代理对象。(替身演员)
  3. 目标对象和代理对象的公共接口。(演员和替身演员需要具有相同的行为动作,因为不想让观众看出来是替身。即当服务端使用代理模式时,客户端是没办法察觉出是自己在使用代理对象的。)

代理模式中,目标对象也可以是代理对象的一个属性,这样子两个对象便是关联关系,耦合度没有继承关系那么高。

//目标对象和代理对象的公共接口
public interface 表演 {
    public void 武打();
}

//目标对象
public class 演员 implents 表演{
    public void 武打() {}
}

//代理对象
public class 替身演员 implents 表演 {
    public void 武打() {}
}

静态代理

假设现在有几个业务操作相关的类:

//公共接口
public interface OrderService {
    void generate();
    void modify();
    void detail();
}

//目标对象
public class OrderServiceImpl implements OrderService {
    @Override
    public void generate() {	//目标方法
        System.out.println("订单已生成");
    }
    @Override
    public void modify() {		//目标方法
        System.out.println("订单已修改");
    }
    @Override
    public void detail() {		//目标方法
        System.out.println("请看订单详情");
    }
}

现在我们要来统计OderServiceImpl这个类各个方法的耗时情况, 如果直接去修改源代码进行时间统计,违背了 OCP 原则,并且,代码也无法复用。

我们来思考这样一种解决方案:我们编写一个子类继承OrderServiceImpl,并对每个业务方法进行重写。虽然可以保证在不修改原来的代码的情况下实现我们的需求。但是,相同代码依旧没有办法得到复用,且耦合度过高(采用了继承关系,继承关系是一种耦合度非常高的关系,不建议使用)。

这个需求的最佳解决方案是使用代理模式

//代理对象,实现和目标对象一样的接口
public class OrderServiceProxy implements OrderService {

    //目标对象作为代理对象的一个属性(关联关系,耦合度低于继承)
    //这里写一个公共接口,因为耦合度低
    private OrderService target;

    //创建代理对象的时候传递一个目标对象
    public OrderServiceProxy(OrderService orderService) {//目标对象
        this.target = orderService;
    }

    @Override
    public void generate() {    //代理方法
        //代理方法对目标方法的增强
        long begin = System.currentTimeMillis();
        target.generate();  //代理方法执行目标方法
        long end = System.currentTimeMillis();
        System.out.println("耗时" + (end - begin) + "ms");
    }
	//...
}
public class Client {
    public static void main(String[] args) {
        //创建目标对象
        OrderService orderService = new OrderServiceImpl();
        //创建代理对象
        OrderService orderServiceProxy = new OrderServiceProxy(orderService);
        //调用代理对象的代理方法
        orderServiceProxy.generate();
    }
}

静态代理模式优点:

  1. 解决了 OCP 问题。
  2. 采用代理模式(关联关系),降低了代码的耦合度。

缺点:类爆炸。假设系统中有成千个接口,那么每个接口都需要代理类,那么类会急剧膨胀,不好维护。

动态代理

动态代理还是代理模式,只不过添加了字节码生成技术,可以在内存中为我们动态生成一个 class 字节码,这个字节码就是代理类。在程序运行阶段,在内存中动态生成代理类,被称为动态代理,目的是为了减少代理类的数量。解决代码复用的问题。

在内存当中动态生成类的技术常见的包括:

  1. JDK 动态代理技术:只能代理接口。
  2. CGLIB 动态代理技术:CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高性能,高质量的 Code 生成类库,它可以在运行期扩展 Java 类与实现 Java 接口。它既可以代理接口,又可以代理类,底层是通过继承的方式实现的。性能比 JDK 动态代理要好。(底层有一个小而快的字节码处理框架 ASM。)
  3. Javassist 动态代理技术:Javassist 是一个开源的分析、编辑和创建 Java 字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba(干叶 滋)所创建的。它已加入了开放源代码 JBoss 应用服务器项目,通过使用 Javassist 对字节码操作为 JBoss 实现动态”AOP”框架。

接下来,我们专门开几个章节来讲讲动态代理。

JDK 动态代理

参数问题

使用代理对象的代码:

package cn.hnu.proxy.client;

import cn.hnu.proxy.service.Impl.OrderServiceImpl;
import cn.hnu.proxy.service.OrderService;
import cn.hnu.proxy.service.TimerInvocationHandler;

import java.lang.reflect.Proxy;

public class Client {
    public static void main(String[] args) {
        //创建目标对象
        OrderService target = new OrderServiceImpl();
        /*
         * //创建代理对象
         * newProxyInstance翻译为新建代理对象
         * newProxyInstance方法本质上做了两件事:
         *   1.在内存中动态生成了一个代理类.class
         *   2.用这个.class new了一个对象
         *
         * 三个参数:类加载器,代理类要实现的接口,调用处理器
         *      1.类加载器:将内存中的.class加载到java虚拟机中,且必须得和目标对象使用同一个类加载器
         *      2.代理类要和目标对象实现同一个或同一些接口
         *      3.调用处理器主要是让我们往当中传入要增强的程序
         *
         * 代理对象和目标对象实现的接口一样,所以可以直接向下转型
         * */
        OrderService proxyObj = (OrderService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new TimerInvocationHandler(target));
        //调用代理对象的代理方法
        proxyObj.generate();
        proxyObj.detail();
        proxyObj.modify();
    }
}

调用处理器:

package cn.hnu.proxy.service;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

//调用处理器
public class TimerInvocationHandler implements InvocationHandler {

    private Object target;  //目标对象,注意是Object类型

    public TimerInvocationHandler(Object target) {
        this.target = target;
    }

    //这个接口的目的便是让我们有地方写增强代码,到时候直接调用invoke函数
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        //参数:代理对象的引用(使用较少),目标对象上要执行的目标方法,目标方法上的参数

        //增强代码1
        Long begin = System.currentTimeMillis();

        //调用目标对象上的目标方法
        Object returnValue = method.invoke(target, args);

        //增强代码2
        Long end = System.currentTimeMillis();

        System.out.println("耗时" + (end - begin) + "ms");

        //把目标对象的目标方法返回值返回出去
        return returnValue;
    }
}

JDK 动态代理工具类封装

package cn.hnu.proxy.util;

import cn.hnu.proxy.service.TimerInvocationHandler;

import java.lang.reflect.Proxy;

public class ProxyUtil {
    public static Object newProxyInstance(Object target) {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new TimerInvocationHandler(target));
    }
}

CGLIB 动态代理

CGLIB 既可以代理接口,又可以代理类,底层采用继承的方式实现。所以被代理的目标类不能使用 final 修饰。

使用前添加依赖:

<dependencies>
    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.3.0</version>
    </dependency>
</dependencies>

客户端:

package cn.hnu.proxy.client;

import cn.hnu.proxy.service.TimerMethodInterceptor;
import cn.hnu.proxy.service.UserService;
import net.sf.cglib.proxy.Enhancer;

public class Client {
    public static void main(String[] args) {
        //创建字节码增强器对象
        //这个对象是CGLIB库当中的核心对象,就是以靠它来生成代理类的
        Enhancer enhancer = new Enhancer();

        //告诉CGLIB父类是谁
        enhancer.setSuperclass(UserService.class);

        //设置回调(等同JDK调用处理器)
        //在CGLIB中不是InvocationHandler接口了,而是方法拦截器MethodInterceptor接口
        enhancer.setCallback(new TimerMethodInterceptor());

        //创建代理对象
        /*
        * 1.在内存中会生成子类,实际上就是代理类的字节码
        * 2.创建代理对象
        * */
        UserService userService =(UserService) enhancer.create();
        System.out.println(userService.login("admin", "123"));
        userService.logout();
    }
}

方法拦截器实现:

package cn.hnu.proxy.service;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class TimerMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object target, Method method,
                            Object[] objects,
                            MethodProxy methodProxy)
            throws Throwable {

        //增强代码1
        long begin = System.currentTimeMillis();

        //调用目标对象的目标方法(代理对象是继承了目标对象)
        Object resultValue = methodProxy.invokeSuper(target, objects);

        //增强代码2
        long end = System.currentTimeMillis();

        System.out.println("耗时" + (end - begin) + "ms");

        return resultValue;
    }
}

不过,因为 CGLIB 本身的问题,运行时可能因为 JDK 版本过高而报错。解决方法是在运行配置中,拉取右侧“修改选项”菜单,选择“添加虚拟机选项”,在“虚拟机选项”中添加--add-opens java.base/java.lang=ALL-UNNAMED,在“程序实参”中添加--add-opens java.base/sun.net.util=ALL-UNNAMED

面向切面编程 AOP

loC 使软件组件松耦合。AOP 让你能够捕捉系统中经常使用的功能,把它转化成组件。(动态代理实际上就是一种 AOP 的代码实现)

AOP(Aspect Oriented Programming):面向切面编程,面向方面编程。(AOP是一种编程技术)

切面:程序中和业务逻辑没有关系的通用代码。

AOP 是对 OOP 的补充延伸。AOP 底层使用的就是动态代理来实现的。Spring 的 AOP 使用的动态代理是:JDK 动态代理 + CGLIB 动态代理技术。Spring 在这两种动态代理中灵活切换,如果是代理接口,会默认使用 JDK 动态代理,如果要代理某个类,这个类没有实现接口,就会切换使用 CGLIB。当然,你也可以强制通过一些配置让 Spring 只使用 CGLIB。

AOP 介绍

一般一个系统当中都会有一些系统服务,例如:日志、事务管理、安全等。这些系统服务被称为:交叉业务

这些交叉业务几乎是通用的,不管你是做银行账户转账,还是删除用户数据。日志、事务管理、安全,这些都是需要做的。

如果在每一个业务处理过程当中,都掺杂这些交叉业务代码进去的话,存在两方面问题:

  1. 交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复用。并且修改这些交叉业务代码的话,需要修改多处。
  2. 程序员无法专注核心业务代码的编写,在编写核心业务代码的同时还需要处理这些交叉业务。

AOP 优点:

  1. 代码复用性增强。
  2. 代码易维护。
  3. 使开发者更关注业务逻辑。

关于 AOP 的七大术语

public class UserSerivce {
    public void fun1() {}
    public void fun2() {}
    public void fun3() {}
    public void fun4() {}
    public void fun5() {}
    public void service() {	//核心业务方法
        fun1();
        fun2();
        fun3();
        fun5();
    }
}
  1. 连接点Joinpoint:在程序的整个执行流程中,可以织入切面的位置。方法的执行前后,异常抛出之后等位置。(上述代码中,service方法大括号中的地方便是连接点)

  2. 切点Pointcut:在程序执行流程中,真正织入切面的方法,其实就是筛选出来的连接点。(一个切点对应多个连接点,一个类中的所有方法都可以是 Joinpoint,具体哪个方法要增强功能,那个方法就叫 Pointcut)

    public void service() {
        try {
            //Joinpoint
            fun1(); //Pointcut
            //Joinpoint
            fun2(); //Pointcut
            //Joinpoint
            fun3(); //Pointcut
            //Joinpoint
            fun5(); //Pointcut
            //Joinpoint
        } catch (Exception e) {
            //Joinpoint
        } finally {
            //Joinpoint
        }
    }
  3. 通知Advice:通知又叫增强,就是具体增强的代码。(事务、日志、安全代码等)。通知包括:前置通知(目标代码前)、后置通知(目标代码后)、环绕通知(目标代码前后)、异常通知(写在catch中的代码)、最终通知(写在finally中的代码)。

  4. 切面Aspect:切点 + 通知就是切面。

  5. 织入Weaving:把通知应用到目标对象上的过程。

  6. 代理对象Proxy:一个目标对象被织入通知后产生的新对象。

  7. 目标对象Target:被织入通知的对象。

切点表达式

切点表达式用来定义通知(Advice)在哪些方法上切入。

切点表达式语法格式:

execution([访问控制权限修饰符] 返回值类型 [全限定类名] 方法名(形式参数列表) [异常])
  • 访问控制权限修饰符:可选项,没写就是四个权限都包括,写 public 就表示只包括公开的方法。
  • 返回值类型:必填项,*表示返回值类型任意。
  • 全限定类名:可选项,没写代表所有的类。..表示当前包下以及子包下所有的类。省略时表示所有的类。
  • 方法名:必填项,*表示所有方法,xxx*表示以xxx开头的方法。
  • 形式参数列表:必填项,()表示没有参数的方法,(..)表示参数类型和个数任意的方法,(*)表示只有一个参数的方法,(*, xxx)表示第一个参数类型任意,第二个参数类型为xxx的方法。
  • 异常:可选项,省略时表示任意类型的异常。

示例:

//service包下所有类中以delete开始的所有方法
execution(public * cn.hnu.mall.service.*.delete*(..))
    
//mall包下所有类的所有方法
execution(* cn.hnu.mall..*(..))
    
//所有类的所有方法
execution(* *(..))

使用 Spring 的 AOP

Spring 对 AOP 的实现包括以下3种方式:

  1. Spring 框架结合 AspectJ 框架实现的 AOP,基于注解方式。
  2. Spring 框架结合 AspectJ 框架实现的 AOP,基于 XML 方式。
  3. Spring 框架自己实现的 AOP,基于 XML 配置方式。

实际开发中,都是 Spring + AspectJ 来实现AOP。所以我们重点学习第一种和第二种方式。

什么是AspectJ?(Eclipse 组织的一个支持 AOP 的框架。AspectJ 框架是独立于 Spring 框架之外的一个框架,Spring 框架用了 AspectJ)

基于注解的 AOP

准备工作

使用前需要添加依赖:

<!-- 引入spring aspects依赖 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>6.0.6</version>
</dependency>

配置文件添加头部信息:

<?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"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

</beans>

实现步骤

准备目标类和切面类,并交由 Spring 框架处理(配置文件中别忘了使用context:component-scan进行包扫描):

//目标类(没有实现接口,故动态代理是使用的CGLIB)
@Service
public class UserService {
    //目标方法
    public void login() {
        System.out.println("系统正在进行身份认证...");
    }
}

//切面类
@Component
public class LogAspect {
    //切面 = 通知 + 切点
    //通知:就是增强,就是具体要编写的增强代码
    //这里通知Advice以方法的形式出现
    public void intensifier() {
        System.out.println("我是一段增强代码");
    }
}

标注注解,形成切面:

//切面类
@Component
@Aspect //使用Aspect注解进行标注,形成切面
public class LogAspect {
    //Before注解标注的方法是一个前置通知
    @Before("execution(* cn.hnu.spring6.service.UserService.*(..))")
    public void intensifier() {
        System.out.println("我是一段增强代码");
    }
}

配置文件开启动态代理:

<?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"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 组件扫描 -->
    <context:component-scan base-package="cn.hnu.spring6.service"/>

    <!--
        开启自动代理
        spring在扫描该类的时候,查看该类上是否有@Aspect
        如果有,则依照切面类生成对应的代理对象
        proxy-target-class="true"表示必须使用CGLIB
    -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

使用 AOP:

public class SpringAOPTest {
    @Test
    public void testBefore() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.login();
    }
}

所有通知类型

  • 前置通知:@Before
  • 后置通知:@AfterReturning
  • 环绕通知:@Around。(环绕是最大的通知,在前置通知之前,在后置通知之后)
  • 异常通知:@After Throwing。(异常通知之后直接最终通知,不会执行后置通知和后环绕通知)
  • 最终通知:@After
//切面类
@Component
@Aspect //使用Aspect注解进行标注,形成切面
public class LogAspect {
    //前置通知
    @Before("execution(* cn.hnu.spring6.service.UserService.*())")
    public void beforeAdvice() {
        System.out.println("前置通知");
    }

    //后置通知
    @AfterReturning("execution(* cn.hnu.spring6.service.UserService.*())")
    public void afterAdvice() {
        System.out.println("后置通知");
    }

    //环绕通知
    @Around("execution(* cn.hnu.spring6.service.UserService.*())")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("前环绕通知");
        //利用joinPoint执行目标方法
        joinPoint.proceed();
        System.out.println("后环绕通知");
    }

    //异常通知
    @AfterThrowing("execution(* cn.hnu.spring6.service.UserService.*())")
    public void throwAdvice() {
        System.out.println("异常通知");
    }

    //最终通知
    @After("execution(* cn.hnu.spring6.service.UserService.*())")
    public void finallyAdvice() {
        System.out.println("最终通知");
    }
}

切面顺序

利用@Order注解对切面进行排序,数字越小,优先级越高:

//切面类
@Component
@Aspect //使用Aspect注解进行标注,形成切面
@Order(0)//使用Order注解对切面进行排序
public class LogAspect {
	//...
}

@Component
@Aspect
@Order(1)
public class SecurityAspect {
	//...
}

通用切点

利用@Pointcut注解定义通用的切点表达式:

//切面类
@Component
@Aspect //使用Aspect注解进行标注,形成切面
@Order(2)
public class LogAspect {
    //定义通用的切点表达式
    @Pointcut("execution(* cn.hnu.spring6.service.UserService.*())")
    public void generalPointCut() {}

    //前置通知
    @Before("generalPointCut()")
    public void beforeAdvice() {
        System.out.println("前置通知");
    }

    //...
}

//跨类使用通用切点
@Component
@Aspect
@Order(3)
public class SecurityAspect {
    @Before("cn.hnu.spring6.service.LogAspect.generalPointCut()")
    public void beforeAdvice() {
        System.out.println("安全前置通知");
    }
}

连接点

除了环绕通知,其他通知的方法参数列表中都可以写上JoinPoint joinPoint,表示连接点。通过调用getSignature,可以获取目标方法的签名。

//切面类
@Component
@Aspect //使用Aspect注解进行标注,形成切面
@Order(2)
public class LogAspect {
    //定义通用的切点表达式
    @Pointcut("execution(* cn.hnu.spring6.service.UserService.*())")
    public void generalPointCut() {}
    
    //前置通知
    @Before("generalPointCut()")
    public void beforeAdvice(JoinPoint joinPoint) {
        System.out.println("前置通知");
        //joinPoint在Spring调用这个方法的时候直接传递过来
        //getSignature:得到目标方法的签名
        Signature signature = joinPoint.getSignature();
        //通过方法的签名可以获取到方法的具体信息
        //获取方法名
        String methodName = signature.getName();
        //获取修饰符
        int modifiers = signature.getModifiers();
        //获取类名
        String declaringTypeName = signature.getDeclaringTypeName();
    }
}

全注解开发

和 IoC 全注解开发一样,我们也需要编写一个配置类:

@Configuration
//组件扫描
@ComponentScan({"cn.hnu.spring6.service"})
//启用aspectj的自动代理,proxyTargetClass = true表示使用动态代理
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Spring6Config {
}

在主程序中,我们使用AnnotationConfigApplicationContext来创建配置信息:

public class SpringAOPTest {
 	@Test
    public void testNoXML() {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Config.class);
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.login();
    }
}

AOP 的实际案例

事务处理

事务:一组事件的集合,同时成功或者同时失败。(事务的详细介绍可以查看《MySQL》一文)

public class method() {
    try {
        //开启事务
        startTransaction();
        //执行核心逻辑...
        //提交事务
        commitTransaction();
    } catch (Exception e) {
        //回滚事务
        rollbackTransaction();
    }
}

目标对象、代理对象以及配置文件代码:

//目标类
@Service
public class AccountService {
    //目标方法
    public void transfer() {
        System.out.println("银行账户正在完成转账操作");
    }
    //目标方法
    public void withdraw() {
        System.out.println("银行账户正在取款,请稍后...");
    }
}

//事务切面类
@Component
@Aspect
public class TransactionAspect {
    @Around("execution(* cn.hnu.spring6.service..*(..))")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) {
        //执行目标方法
        try {
            System.out.println("开启事务");
            joinPoint.proceed();
            System.out.println("提交事务");
        } catch (Throwable e) {
            System.out.println("回滚事务");
            e.printStackTrace();
        }
    }
}

//配置类
@Configuration
@ComponentScan({"cn.hnu.spring6.service"})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Config {
}

安全日志

只要做了增删改,都要把这个人的信息记录下来。

//目标类
@Service
public class UserService {
    public void saveUser() {
        System.out.println("新增用户信息");
    }
    public void deleteUser() {
        System.out.println("删除用户信息");
    }
    public void modifyUser() {
        System.out.println("修改用户信息");
    }
    public void getUser() {
        System.out.println("获取用户信息");
    }
}

//切面类
@Component
@Aspect
public class SecurityLogAspect {
    //定义通用切点
    @Pointcut("execution(* cn.hnu.spring6.biz..save*(..))")
    public void savePointCut() {}

    @Pointcut("execution(* cn.hnu.spring6.biz..delete*(..))")
    public void deletePointCut() {}

    @Pointcut("execution(* cn.hnu.spring6.biz..modify*(..))")
    public void modifyPointCut() {}

    //用||连接多个切点
    @Before("savePointCut() || deletePointCut() || modifyPointCut()")
    public void beforeAdvice(JoinPoint joinPoint) {
        //获取系统时间
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String date = sdf.format(new Date());
        //获取方法签名
        Signature signature = joinPoint.getSignature();
        //获取类名
        String declaringTypeName = signature.getDeclaringTypeName();
        //获取方法名
        String name = signature.getName();
        System.out.println("now time:" + date + " 操作了" + declaringTypeName + "." + name);
    }
}

Spring 对事务的支持

上述 AOP 实现的切面织入事务称为编程式事务,除此之外,Spring 框架还提供了声明式事务。

Spring 对事务的管理底层实现方式是基于 AOP 的,采用 AOP 的方式进行封装。所以 Spring 专门开发了一套 API,核心接口:PlatformTransactionManager,这个是 Spring 事务管理器核心接口,在 Spring6 中它有两个实现:

  1. DataSourceTransactionManager:支持 JdbcTemplate、MyBatis、Hibernate 等事务管理。
  2. JtaTransactionManager:支持分布式事务管理。

如果在 Spring 中使用 JdbcTemplate,就要使用DataSourceTransactionManager来管理事务。(Spring 内置写了,可以直接使用)

基于配置方式

<?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"
       xmlns:tx="http://www.springframework.org/schema/tx"
       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
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 组件扫描 -->
    <context:component-scan base-package="cn.hnu.bank"/>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/test"/>
        <property name="username" value="root"/>
        <property name="password" value="MySQL:040809"/>
    </bean>

    <!-- 配置JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置事务管理器 -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 事务注解驱动器开启 -->
    <tx:annotation-driven transaction-manager="txManager"/>
</beans>

全注解开发

配置事务管理器:

@Configuration
@ComponentScan({"cn.hnu.bank"})
@EnableTransactionManagement(proxyTargetClass = true)	//开启注解驱动器
public class Spring6Config {

   @Bean(name = "dataSource")
    public DataSource dataSource() {
        //配置数据源
        return dataSource;
    }

    @Bean(name = "txManager")
    public DataSourceTransactionManager dataSourceTransactionManager(DataSource dataSource) {
        DataSourceTransactionManager txManager = new DataSourceTransactionManager();
        txManager.setDataSource(dataSource);
        return txManager;
    }

}

在需要使用事务的地方配置@Transactional注解,该注解可以配置在方法上,也可以配置在类上。如果该注解配置在类上,就说明该类的所有方法均使用事务。

事务之传播行为

在 service 类中有 a 方法和 b 方法,a 方法上有事务,b 方法上也有事务,当 a 方法执行过程中调用了 b 方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。

一共有七种传播行为:

  1. REQUIRED:使用了该传播行为的方法 a,在方法 b 调用方法 a 时,如果方法 b 有事务,方法 a 就使用方法 b 的事务,否则方法 a 就自己创建一个事务(默认)。
  2. SUPPORTS:原先方法有事务,就使用原先方法的,没有就不管了。
  3. MANDATORY:原先方法有事务,就是用原先方法的,没有就抛异常。
  4. REQUIRES_NEW:不管原先方法有没有事务,直接挂起原先的事务,自己开启一个新事务。新事务和原先事务没有嵌套关系。
  5. NOT_SUPPORTED:不支持事务,只要事务存在,则挂起程序,执行当前程序。
  6. NEVER:不支持事务,发现事务则直接抛异常。
  7. NESTED:有事务的话,就在这个事务里嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和 REQUIRED 一样。

事务的三大读问题

  • 脏读:读取到没有提交到数据库的数据(读到了缓存当中的数据),叫做脏读。

    Time | T1          | T2
    -----|-------------|------------------
    T0   | BEGIN       |
         | READ A = 10 |
    T1   |             | BEGIN
         |             | UPDATE A = 20
    T2   | READ A = 20 | 
    T3   |             | ROLLBACK (A=10)
    T4   | (A的值不再准确)
  • 不可重复读:在同一个事务当中,第一次和第二次读取的数据不一样

    Time | T1          | T2
    -----|-------------|------------------
    T0   | BEGIN       |
         | READ A = 10 |
    T1   |             | BEGIN
         |             | UPDATE A = 20
    T2   |             | COMMIT
    T3   | READ A = 20 |
  • 幻读:读到的数据出现幻象。只要有多个事务并发执行,一定会导致幻读情况。

    Time | T1                     | T2
    -----|------------------------|------------------
    T0   | BEGIN                  |
         | SELECT * FROM ... WHERE|
    T1   | Returns 5 rows         |
         |                        | BEGIN
         |                        | INSERT INTO ...
    T2   |                        | COMMIT
    T3   | (实际上是 6 rows,但是T1依旧认为是 5 rows)

事务的隔离级别

  • 读未提交(READ_UNCOMMITTED):这种隔离级别,存在脏读问题。
  • 读提交(READ_COMMITTED):解决了脏读问题,其他事务提交之后才能读到,但存在不可重复读问题。
  • 可重复读(REPEATABLE_READ):解决了不可重复读,可以达到可重复读效果,只要当前事务不结束,读取到的数据一直都是一样的。
  • 序列化(SERIALIZABLE):解决了幻读问题,事务排队执行,不支持并发。
隔离级别 脏读 不可重复读 幻读
Read uncommited
Read commited ×
Repeatable Read × ×
Serializable × × ×

事务的超时问题

代码实现:

@Transactional(timeout = 10)

以上代码表示设置事务的超时时间为 10 秒。(默认值为 -1,表示没有时间限制)

表示超过 10 秒如果该事务中所有的 DML 语句还没有执行完毕的话,最终结果会选择回滚。

事务超时时间指的是:在当前事务中,最后一条 DML 语句执行之前的时间。如果最后一条 DML 语句后面有很多业务逻辑,这些业务代码执行的时间将不计入超时时间中。

只读事务启动优化策略

代码如下:

@Transactional(readOnly = true)

将当前事务设置为只读事务,在该事务执行过程中只允许 select 语句执行,delete、insert、update 均不可执行。

该特性的作用是:启动 Spring 的优化策略,提高 select 的查询效率。

如果该事务中没有增删改操作,建议设置为只读事务。

异常时事务的回滚处理

@Transactional(rollbackFor = RuntimeException.class)

表示只有发生RuntimeException异常或该异常的子类异常才回滚。

@Transactional(noRollbackFor = NullPointerException.class)

表示发生NullPointerException异常或该异常的子类不回滚,其他异常则回滚。

声明式事务之 XML 实现方式

配置步骤:

  1. 配置事务管理器。
  2. 配置通知。
  3. 配置切面。
<?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"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 组件扫描 -->
    <context:component-scan base-package="cn.hnu.bank"/>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/test"/>
        <property name="username" value="root"/>
        <property name="password" value="MySQL:040809"/>
    </bean>

    <!-- 配置JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置事务管理器 -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置通知,也就是配置具体的增强代码 -->
    <tx:advice id="txActive" transaction-manager="txManager">
        <!-- 配置通知的相关属性,之前所讲的所有的事务属性都可以在以下标签中配置 -->
        <tx:attributes>
            <tx:method name="transfer" propagation="REQUIRED" rollback-for="java.lang.Throwable" />
        </tx:attributes>
    </tx:advice>

    <!-- 配置切面 -->
    <aop:config>
        <!-- 切点 -->
        <aop:pointcut id="txPointCut" expression="execution(* cn.hnu.bank.service..*(..))"/>
        <!-- 切面 -> 通知 + 切点 -->
        <aop:advisor advice-ref="txActive" pointcut-ref="txPointCut"/>
    </aop:config>

</beans>

Spring6 整合 JUnit

Spring6 对 JUnit4 的支持

<!-- spring对junit4支持的依赖 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>6.0.6</version>
</dependency>

在单元测试类中使用注解简化代码:

@RunWith(SpringJUnit4ClassRunner.class)	//使用Spring的Junit4支持
@ContextConfiguration(classes = {SpringConfig.class})	//添加配置类
public class SpringJunit4Test {
    @Autowired	//使用注解对user进行注入
    private User user;

    @Test
    public void testUser() {
        //直接使用user
        System.out.println(user.getName());
    }
}

Spring6 对 Junit5 的支持

<!-- junit5的依赖 -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>
@ExtendWith(SpringExtension.class)//使用Spring的Junit5支持
@ContextConfiguration(classes = {SpringConfig.class})
public class SpringJunit5Test {
    @Autowired
    private User user;
    
    @Test
    public void testUser() {}
}

Spring6 集成 MyBatis

所需依赖(核心是 mybatis-spring,这个是 MyBatis 提供的与 Spring 相互集成的依赖):

<dependencies>
    <!-- 引入spring框架 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>6.0.6</version>
    </dependency>

    <!-- spring-jdbc驱动 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>6.0.6</version>
    </dependency>

    <!-- mysql链接驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>

    <!-- mybatis依赖 -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.13</version>
    </dependency>

    <!-- mybatis-spring集成依赖 -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>3.0.3</version>
    </dependency>

    <!-- Spring6使用的@Resource注解依赖 -->
    <dependency>
        <groupId>jakarta.annotation</groupId>
        <artifactId>jakarta.annotation-api</artifactId>
        <version>2.1.1</version>
    </dependency>

    <!-- 引入junit测试依赖 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>

    <!-- druid连接池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.20</version>
    </dependency>
    
    <!-- 引入logback依赖 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.4.11</version>
    </dependency>
</dependencies>

mybatis-config 配置文件只需要配置自动驼峰命名和懒加载即可。

spring6.xml配置文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 组件扫描 -->
    <context:component-scan base-package="cn.hnu.bank"/>

    <!-- 引入外部属性文件 -->
    <context:property-placeholder location="jdbc.properties"/>

    <!-- 数据源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!-- 配置SqlSessionFactoryBean -->
    <bean class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- 注入数据源 -->
        <property name="dataSource" ref="dataSource"/>
        <!-- 指定mybatis核心配置文件 -->
        <property name="configLocation" value="mybatis-config.xml"/>
        <!-- 指定别名 -->
        <property name="typeAliasesPackage" value="cn.hnu.bank.pojo"/>
    </bean>

    <!-- 配置mapper扫描配置器,主要扫描mapper接口生成代理类 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- 指定扫描哪个包 -->
        <property name="basePackage" value="cn.hnu.bank.mapper"/>
    </bean>

    <!-- 事务管理器 -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 数据源 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 启用事务注解 -->
     <tx:annotation-driven transaction-manager="txManager"/>

</beans>

引入子配置文件

在 Spring 中,一个配置文件是可以引入其他的子配置文件的。一般情况下,dao 层的配置文件放在一个文件中,service 的配置文件放在一个文件中,分文件管理,这样会使得文件的数据更好维护。

使用import引入外部文件:

<import resource="fileName.xml"/>

Spring 中的八大模式

  1. 简单工厂模式:BeanFactorygetBean()方法(BeanFactory是一个工厂,用来生产 Bean),通过唯一标识来获取 Bean 对象,是典型的简单工厂模式(静态工厂模式)。
  2. 工厂方法模式:FactoryBean是典型的工厂方法模式(FactoryBean是一个 Bean,协助我们来生产 Bean),在配置文件中通过factory-method属性来指定工厂方法,该方法是一个实例方法。
  3. 单例模式:Spring 用的是双重判断加锁的单例模式。
  4. 代理模式:Spring 的 AOP 就是代理模式。
  5. 装饰器模式:JavaSE 的 IO 流就体现了装饰器模式。Spring 中配置 DataSource 的时候,这些 dataSource 可能是各种不同类型的,比如不同的数据库:Oracle、SQL Server、MySQL等,也可能是不同的数据源:比如 apache 提供的org.apache.commons.dbcp.BasicDataSource、Spring 提供的org.springframework.jndi.JndiObjectFactoryBean等。这时,能否在尽可能少修改原有类代码下的情况下,做到动态切换不同的数据源?此时就可以用到装饰者模式。Spring 根据每次请求的不同,将 dataSource 属性设置成不同的数据源,以到达切换数据源的目的。Spring 中类名中带有:Decorator 和 Wrapper 单词的类,都是装饰器模式。
  6. 观察者模式:定义对象间的一对多的关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。Spring 中观察者模式一般用在 listener 的实现。Spring 中的事件编程模型就是观察者模式的实现。在 Spring 中定义了一个 ApplicationListener 接口,用来监听 Application 的事件,Application 其实就是 ApplicationContextApplicationContext 内置了几个事件,其中比较容易理解的是:ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent、ContextClosedEvent
  7. 策略模式:策略模式是行为性模式,调用不同的方法,适应行为的变化,强调父类的调用子类的特性。一个接口下可以有多个实现类,这就已经体现了策略模式。策略模式也可以看成是面向接口编程,最顶层就一个 Service,其底下到底是用哪个实现类我们不需要关心,底层可以灵活调用和实现。
  8. 模板方法模式:Spring 中的 JdbcTemplate 就是一个模板类。它就是一个模板方法设计模式的体现。在模板类的模板方法execute中编写核心算法,具体的实现步骤在子类中完成(模板类定义算法骨架,具体实现放子类中去完成)。

文章作者: 热心市民灰灰
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 热心市民灰灰 !
  目录