Spring5 学习笔记

学习地址: B站-动力节点

个人代码: GitHub

1. Spring 概述

1.1 Spring 简介

  Spring Framework 是一个使用Java开发的、轻量级的、开源框架,它的主要作用是为了解耦合。Spring 的核心技术是 IOC(控制反转)AOP(面向切面编程)

  Spring 框架提高了很多功能,包括IOC容器、AOP、数据访问、事务、测试功能、定时任务、缓存等等。

001_Spring简介_功能模块

1.2 优点

轻量、解耦、面向切面编程、方便与其他框架集成、方便测试、减低开发难度。

2. IOC 控制反转

2.1 IOC 是什么

  IOC (Inversion of Control, 控制反转) 是一种理论,指导开发人员如何使用对象、管理对象,将对象的生命周期交给容器来管理。通过容器管理对象,开发人员只需要拿到对象,执行对象的方法即可。

  • 控制:管理对象的创建、属性赋值、生命周期的管理。
  • 正转:让开发人员掌控对象的创建、属性赋值,即整个生命周期的管理。
  • 反转:把开发人员管理对象的权限转移给容器来实现,让容器完成管理。

2.2 IOC 的技术实现

  DI (Dependency Injection, 依赖注入) 是 IOC 的一种技术实现,开发人员通过对象的名称获取已初始化的对象,而对象的创建、属性赋值、对象间的调用等都由容器内部实现。

2.3 IOC-创建对象 牛刀小试

Source Code

2.3.1 测试步骤

  1. 创建 maven-quickstart 项目,并调整项目结构(字符编码、JDK版本等)
  2. 添加依赖
    • spring-context
    • junit
  3. 定义接口和实现类
    • 接口: SomeService
      • 方法: doSome(): void
    • 实现类: SomeServiceImpl
  4. 创建 Spring 配置文件(.xml),声明需要创建的对象
    • 通过<bean>标签声明对象,一个标签对应一个对象。
  5. 使用容器中的对象
    • 创建 ApplicationContext 对象
    • 通过 getBean() 获取容器中的对象

2.3.2 依赖文件

<?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>com.bpf</groupId>
    <artifactId>M01-ioc-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.12</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

2.3.3 接口与实现类

package com.bpf.service;

public interface SomeService {

    void doSome();
}
package com.bpf.service.impl;

import com.bpf.service.SomeService;

public class SomeServiceImpl implements SomeService {

    public SomeServiceImpl() {
        System.out.println("[SomeServiceImpl] 无参构造方法");
    }

    @Override
    public void doSome() {
        System.out.println("[SomeServiceImpl] someService()...");
    }
}

2.3.4 配置文件

<?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      自定义对象的名称,保持唯一。
        class   自定义对象的全限定类名,不能是接口。

        >>> Spring 根据 id 和 class 创建对象,并将对象放入一个 map 对象中。
    -->
    <bean id="someService" class="com.bpf.service.impl.SomeServiceImpl" />
    <bean id="someService1" class="com.bpf.service.impl.SomeServiceImpl" />

    <bean id="mydate" class="java.util.Date" />
</beans>

2.3.5 测试创建对象

测试创建对象: CreateBeanTest.java
package com.bpf.service;

import com.bpf.service.impl.SomeServiceImpl;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.util.Arrays;
import java.util.Date;

public class CreateBeanTest {

    /**
     * 传统方式: new 获取对象
     */
    @Test
    public void testCreateBeanClassical() {
        SomeService someService = new SomeServiceImpl();
        someService.doSome();
    }

    /**
     * 使用 Spring 容器方式获取对象
     */
    @Test
    public void testCreateBean() {
        // 创建容器对象
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");

        // 通过 getBean() 获取 bean 对象
        SomeService someService = (SomeService) ctx.getBean("someService");
        // 调用对象方法
        someService.doSome();
    }

    /**
     * Spring 创建对象,调用的是类的哪个构造器呢?
     *  默认调用的是类的无参构造器!
     */
    @Test
    public void testCreateStyle() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
        SomeService someService = (SomeService) ctx.getBean("someService");
        someService.doSome();
        // 在无参构造器上添加输出语句,如果把无参构造器改成有参构造器,执行测试方法时会报错:无法找到默认的构造方法。

        /** 执行结果
         * [SomeServiceImpl] 无参构造方法
         * [SomeServiceImpl] someService()...
         */
    }

    /**
     * Spring 创建对象,是什么时候创建的呢?
     *   Spring在创建容器对象 ApplicationContext时,会读取配置文件,并创建文件中声明的所有java对象。
     *
     * 优点:获取对象速度快。
     * 缺点:占用内存。
     */
    @Test
    public void testCreateTime() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");

        /** 执行结果
         * [SomeServiceImpl] 无参构造方法
         * [SomeServiceImpl] 无参构造方法
         */
    }

    /**
     * 获取Spring容器中的对象信息
     */
    @Test
    public void testGetCtxInfo() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");

        // 容器中对象的数量
        int count = ctx.getBeanDefinitionCount();
        // 容器中对象的名称
        String[] names = ctx.getBeanDefinitionNames();

        System.out.println("容器中对象的数量:" + count);
        System.out.println("容器中对象的名称:" + Arrays.toString(names));

        /** 执行结果
         * [SomeServiceImpl] 无参构造方法
         * [SomeServiceImpl] 无参构造方法
         * 容器中对象的数量:2
         * 容器中对象的名称:[someService, someService1]
         */
    }

    /**
     * 创建非自定义对象
     */
    @Test
    public void testOtherBean() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
        Date date = (Date) ctx.getBean("mydate");
        System.out.println("date = " + date);

        /** 执行结果
         * [SomeServiceImpl] 无参构造方法
         * [SomeServiceImpl] 无参构造方法
         * date = Wed Dec 22 19:35:37 CST 2021
         */
    }
}

2.4 Spring 的配置文件

  Spring 配置文件通常命名为ApplicationContext.xml。标准的配置文件格式如下:

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

<!-- 
    1) 根标签是 beans
    2) xxx.xsd 是当前XML文件的约束文件
    3) 在 beans 标签内声明 bean 对象。
       一个 bean 就是一个java对象。
 -->
</beans>

002_Spring配置文件

  Spring 支持多配置文件方式,Spring 管理多配置文件常用的是包含关系。即在主配置文件中使用import标签包含其他配置文件,在其他配置文件中定义声明各自的信息。

<!-- 主配置文件 -->

<!-- 路径中可以使用通配符 * 同时引入多个文件 -->
<import resource="classpath:其他配置文件路径" />

2.5 Spring IOC ☞ 创建对象

2.5.1 Spring 容器创建对象的特点

Spring 框架使用 DI 实现 IOC 思想,底层通过反射机制创建对象、初始化对象。

  1. 容器对象是ApplicationContext,它是一个接口。常用的实现类是ClassPathXmlApplicationContext,并且通过getBean()方法获取已初始化的对象。
  2. Spring 创建对象默认调用类的无参构造器
  3. Spring 在创建容器对象后,会读取配置文件,并创建文件中声明的所有java对象,然后都放在map对象(ConcurrentMap)中。

2.5.2 XML方式

  Spring 通过在配置文件中使用bean标签声明对象,使用id属性指定创建的对象名称,使用class属性指定创建的对象类型。

<!-- 配置文件中声明一个 bean 标签代表一个 java对象 -->
<bean id="对象名称" class="对象类型" />

2.5.3 注解方式

  使用注解代替配置文件中的bean标签,在Java类上使用注解,通过value属性指定创建的对象名称(相对于标签的id属性)。同时还需要在配置文件中开启注解扫描并指定扫描的包路径。

Spring 提供了四个注解

注解 说明
@Component 表示普通的java对象
@Repository 常用于创建DAO层的对象,持久层对象,表示可以访问数据库
@Service 常用于创建Service层的对象,业务层对象,表示拥有事务功能
@Controller 常用于创建Controller层的对象,控制器对象,表示可以接收和处理请求。

配置文件开启注解扫描:

<!-- base-package 指定要扫描的包路径,Spring 会自动扫描包及其子包内表有上述注解之一的类,并创建和管理。 -->
<context:componet-scan base-package="包路径" />

<!-- 如何扫描多个包? -->
<!-- 1. 使用多个标签 -->
<context:componet-scan base-package="xx.yy.pack01" />
<context:componet-scan base-package="xx.yy.pack02" />

<!-- 2. 使用分隔符:分号(;)或逗号(,) -->
<context:componet-scan base-package="xx.yy.pack01;xx.yy.pack02" />

<!-- 3. 使用共同的父包 -->
<context:componet-scan base-package="xx.yy" />

2.6 Spring IOC ☞ 属性注入

Source Code

2.6.1 XML方式

(1)set注入(设值注入)

set注入:通过对象的 setXxx() 方法给属性赋值。

特点

  • 注入的属性必须存在对应的 setter 方法
  • 如果属性在对象中不存在,但存在 setter 方法,依然不会报错。
  • Spring 容器只负责调用 setter 方法,与方法的具体实现无关。
<!-- 简单类型注入: 基本数据类型、String类型 -->
<bean id="xxx" class="yyy">
    <property name="属性名" value="xxx" />
    ...
</bean>

<!-- 引用Java对象 -->
<bean id="xxx" class="yyy">
    <property name="属性名" ref="其他bean标签的id值" />
    ...
</bean>
<!-- 或 -->
<bean id="xxx" class="yyy">
    <property name="属性名">
        <bean class="想要注入此属性的对象"></bean>
    </property>
    ...
</bean>

<!-- 注入null值 -->
<bean id="xxx" class="yyy">
    <property name="属性名">
        <null/>
    </property>
    ...
</bean>

<!-- 集合类型 -->
<bean id="xxx" class="yyy">
    <property name="属性名">
        <!-- 数组 -->
        <array>
            <value>xxx</value>
        </array>
    </property>

    <property name="属性名">
        <!-- List -->
        <list>
            <value>xxx</value>
            <ref bean="其他bean标签的id值" />
        </list>
    </property>

    <property name="属性名">
        <!-- Set -->
        <set>
            <value>xxx</value>
        </set>
    </property>

    <property name="属性名">
        <!-- Map -->
        <map>
            <entry key="xxx" value="yyy" />
        </map>
    </property>

    <property name="属性名">
        <!-- 数组 -->
        <array>
            <value>xxx</value>
        </array>
    </property>
</bean>

(2)构造注入

构造注入:通过对象的 含参构造器 方法给属性赋值。

特点

  • 不需要属性的 setter 方法
  • 需要有相对应的含参构造器
<!-- 
    index   对应构造器的形参索引,从0开始,可以省略
    name    对应构造器的形参名
    value   对应构造器的形参值
    ref     对应其他的Java Bean
 -->
<bean id="xxx" class="yyy">
    <constructor-arg name="构造器形参名" value="xxx" />
    <constructor-arg index="构造器形参索引" value="xxx" />
    ...
</bean>

(3)引用类型自动注入

引用类型自动注入:只针对对象中的引用类型有效,可以指定根据名称或类型自动注入属性的值。

  • byName: 根据名称注入。当配置文件中bean标签的id值与对象的属性名匹配且属于同个类型时,可以进行注入。
  • byType: 根据类型注入。当配置文件中bean标签的class值与对象的属性类型同源时,可以进行注入。
    • bean标签的class值与对象的属性类型相同时。
    • bean标签的class值与对象的属性类型存在父子关系时。
    • bean标签的class值与对象的属性类型存在接口-实现类关系时。

特点

  • byName 方式通过 bean 标签的id属性,需要保证id唯一
  • byType 方式提供 bean 标签的class属性,需要保证只能存在一个同源的bean,否则会报错。
  • 引用类型自动注入本质上使用的是setter方法进行属性赋值的。
<!-- 引用类型自动注入 -->
<bean id="xxx" class="yyy" autowired="byName | byType">
    ...
</bean>

(4)小作业

主要功能:模拟用户注册操作。

  • 实体类 User,保存用户数据。
  • 定义一个 UserDao 接口,提供方法 insertUser(User),同时定义接口的实现类 MySqlUserDao,方法实现输出 "通过MySQL插入用户:用户数据"。
  • 定义一个 UserService 接口,提供方法 addUser(User),同时定义接口的实现类 UserServiceImpl,并实现方法。

要求:使用 Spring 创建和管理接口的实现类对象,并通过 Spring 获取对象完成用户注册操作。

Source Code

2.6.2 注解方式

(1)@Value

  • @Value注解只能为属性赋普通类型的值。
  • @Value注解的位置:
    • 属性声明上:无需setter方法
    • setter方法上:需要setter方法,并且会调用setter方法
  • 赋的值可以通过外部配置文件(.properties)指定。
<!-- 配置文件中引入外部配置文件 -->
<context:property-placeholder location="classpath:properties文件的路径" />
<?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 https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.bpf.anno.value" />
    <context:property-placeholder location="classpath:bean-value.properties" />
</beans>
stu.name=凯特斯
stu.age=13
package com.bpf.anno.value;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class Student {

    /**
     * @Value 注解:为属性赋值
     * 使用位置:
     *      1. 属性声明上:无需setter方法
     *      2. setter方法上:需要setter方法且会调用setter方法
     */
    @Value("${stu.name}")
    private String name;
    private Integer age;

    public void setName(String name) {
        System.out.println("name = " + name);
        this.name = name;
    }

    @Value("${stu.age}")
    public void setAge(Integer age) {
        System.out.println("age = " + age);
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
package com.bpf.xml;

import com.bpf.anno.value.Student;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestAnnoValue {

    @Test
    public void test() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("anno-value-applicationContext.xml");
        Student student = (Student) ctx.getBean("student");
        System.out.println("student = " + student);

        /** 执行结果
         * age = 13
         * student = Student{name='凯特斯', age=13}
         */
    }
}

(2)@Autowired

  • @Autowired注解可以为属性赋引用类型的值,默认方式是byType
  • @Autowired注解的位置:
    • 属性声明上:无需setter方法
    • setter方法上:需要setter方法,并且会调用setter方法
/**
 * Autowired 注解源码
 *   包含了 required 属性,默认值为true。表示当赋值的属性必须有值且赋值成功,当赋值的对象为null时,会抛出异常。
 */
public @interface Autowired {
    boolean required() default true;
}
<?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 https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.bpf.anno.autowired" />
</beans>
package com.bpf.anno.service;

public interface UserService {

    void sayHello();
}
package com.bpf.anno.autowired;

import com.bpf.anno.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class StudentServiceImpl implements UserService {

    @Override
    public void sayHello() {
        System.out.println("<com.bpf.anno.autowired> [StudentServiceImpl] sayHello()...");
    }
}
package com.bpf.anno.autowired;

import com.bpf.anno.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class Student {

    /**
     * @Autowired 注解:为属性赋值
     * 使用位置:
     *      1. 属性声明上:无需setter方法
     *      2. setter方法上:需要setter方法且会调用setter方法
     * 属性:
     *      boolean required: 表示此属性是否必须,默认值为true。表示当对应的java对象为null时会抛出异常。
     *          org.springframework.beans.factory.NoSuchBeanDefinitionException
     */
    // @Autowired(required = false)
    @Autowired
    private UserService userService;

    public void sayHello() {
        userService.sayHello();
    }
}
package com.bpf.xml;

import com.bpf.anno.autowired.Student;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestAnnoAutowired {

    @Test
    public void test() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("anno-autowired-applicationContext.xml");
        Student student = (Student) ctx.getBean("student");
        student.sayHello();

        /** 执行结果
         * <com.bpf.anno.autowired> [StudentServiceImpl] sayHello()...
         *
         * 当 StudentServiceImpl 类去掉 @Service 注解,Student 类中引用类型 userService 注解改成 @Autowired(required=false) 时:
         * 会抛出空指针异常,因为在 Student 的 sayHello() 方法中,userService未成功赋值,所以在真正使用上并不会修改 required
         */
    }
}

(3)@Qualifer

  当使用@Autowired注解进行引用类型注入时,由于默认方式为byType,当存在多个同源的bean时,会抛出异常:org.springframework.beans.factory.NoUniqueBeanDefinitionException。这时候就需要使用byName方式了。

  • @Qualifer注解结合@Autowired注解使用可以实现byName方式的引用类型自动注入。
  • 注解位置同上。
/**
 * Qualifer 注解中只有一个属性 value, 用来指定 bean 的名称即 id。
 */
public @interface Qualifier {
    String value() default "";
}

(4)@Resource

  • @Resource注解是JDK自带的注解,但 Spring 支持这样的注解使用。
  • @Resource注解只能为属性赋引用类型的值,默认方式是byName
    • 当使用byName无法匹配到任何bean时,会使用byType方式。
    • 通过指定name属性让注解只通过byName方式注入bean。
  • 在 JDK8 及之前是自带此注解的,更高的版本需要手动导入依赖。
<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
</dependency>

2.7 Spring IOC 总结

  IOC 就是用来管理对象、管理依赖关系的。通过 IOC 可以实现解决处理业务逻辑对象之间的耦合关系,即 Service 和 DAO 之间的解耦合。

  • 不适合交给Spring管理的对象:
    • 实体类
    • servlet、listener、filter 等 WEB 中的对象,因为它们是由 Tomcat 创建和管理的对象。

补充

> 完全注解开发

> Spring Bean 的生命周期

3. AOP 面向切面编程

3.1 AOP 是什么

  AOP (Aspect Orient Programming, 面向切面编程) 是一种编程思想。它可以在不改变源代码的基础上,给业务方法新增功能。

  AOP 是一种动态的思想,它是在程序运行期间,为特定的业务创建代理,通过代理来增加切面功能,而这个代理是存在于内存中的。

什么是切面

  • 给业务功能新增的功能就是切面。
  • 切面一般是非业务功能,而且一般都是可复用的。
  • 比如:日志功能、事务功能、权限检查、参数检查、信息统计等等。

AOP的作用

  • 给业务功能新增方法不需改变源代码。
  • 让开发人员专注业务逻辑,提高开发效率。
  • 实现业务功能与非业务功能解耦合。
  • 切面复用。

3.2 AOP 中的重要术语

术语 翻译 解释
Aspect 切面 给业务方法新增的功能
JoinPoint 连接点 即业务方法
Pointcut 切入点 切面的执行位置。一个或多个连接点的集合,即增加切面的所有业务方法。
Target 目标对象 业务方法的执行者
Advice 通知 切面的执行时间

  AOP 中重要的三个要素:AspectPointcutAdvice,表示在 Advice时间、在 Pointcut位置 执行 Aspect切面

3.3 AOP 的使用时机

  • 当某些方法需要增加相同功能,而源代码又不方便修改时
  • 当给业务方法增加非业务功能时

3.4 AOP 的技术实现

  常用的 AOP 实现技术是 SpringAspectJ

  • Spring:Spring 框架实现了 AOP 思想中的部分功能。但其操作比较繁琐和笨重。
  • AspectJ:独立的框架,专门负责 AOP,属于 Eclipse 基金会。

3.5 AspectJ 框架

  AspectJ 框架中可以使用 注解XML配置文件 的方式实现 AOP。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.12</version>
</dependency>

3.5.1 注解方式

Source Code

(1)Advice 通知注解

 AspectJ 框架中表示切面执行的时间是五种通知注解,分别代表不同的执行时间。

注解 通知类型 执行时间
@Before 前置通知 业务方法前执行
@AfterReturning 后置通知 业务方法后执行
@Around 环绕通知 业务方法前和后都执行
@AfterThrowing 异常通知 业务方法过程中出现异常时执行
@After 最终通知 业务方法后执行

(2)Pointcut 切入点表达式

 AspectJ 框架中表示切面执行的位置是切入点表达式,本质上可以看作是业务方法的定位标志。

execution(访问权限? 返回值类型 全限定类名?方法名(参数列表) 异常类型?)
  • ? 代表可选。
    • 最简形式:execution(返回值类型 方法名(参数列表))
  • 四个部分之间通过空格分开,并且都可以使用通配符👇。
通配符 含义
* 代表任意字符
.. 用在方法参数中,表示任意参数列表
用在包名中,表示当前包及其子包路径
+ 用在类名后,表示当前类及其子类
用在接口后,表示当前接口及其实现类

(3)@Before 前置通知

  • 注解
    • 前置通知在目标方法执行之前起作用。
  • 属性
    • value: 切入点表达式
  • 方法定义
    • public void 方法名(参数)
    • 第一个参数只能是JoinPoint
    • JoinPoint: 表示连接点,即执行的业务方法。可以获取方法的相关信息,如参数、方法名等。
package com.bpf.before.handler;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Date;

@Component
@Aspect
public class MyBeforeAspect {

    @Before("execution(public void com.bpf.before.service.impl.SomeServiceImpl.doSome(String) )")
    public void addExecTime() {
        System.out.println("[MyBeforeAspect] (前置通知) 当前执行时间:" + new Date());
    }

    @Before("execution(void do*(..))")
    public void noteExecMethod(JoinPoint point) {
        System.out.println("[MyBeforeAspect] (前置通知) 当前正在运行的方法是:");
        System.out.println("\tSign: " + point.getSignature());
        System.out.println("\tTarget: " + point.getTarget());
        System.out.println("\tKind: " + point.getKind());
        System.out.println("\tArgs: " + Arrays.toString(point.getArgs()));
    }
}

(4)@AfterReturning 后置通知

  • 注解
    • 前置通知在目标方法执行之后起作用。
  • 属性
    • value: 切入点表达式
    • returning: 声明自定义变量名,必须与形参中的变量名一致,代表目标方法的执行结果。
  • 方法定义
    • public void 方法名(参数)
    • 第一个参数只能是JoinPoint
    • JoinPoint: 表示连接点,即执行的业务方法。可以获取方法的相关信息,如参数、方法名等。
    • Object: 表示目标方法的执行结果,推荐使用Object
  • 特点
    • 当业务方法的返回值类型是 基本数据类型及其包装类 或 String 时,切面方法无法改变返回值内容。
    • 当业务方法的返回值类型是 其他引用类型的Java对象时,切面方法可以改变返回值内容。
package com.bpf.afterreturning.handler;

import com.bpf.afterreturning.bean.Person;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MyAfterReturningAspect {

    @AfterReturning(value = "execution(* *..SomeServiceImpl.do*(..) )",
            returning = "res")
    public void process(JoinPoint point, Object res) {
        System.out.println("[MyAfterReturningAspect] (后置通知) 目标方法的执行结果是:" + res);

        // 当 返回值类型为 String 时,尝试修改,但修改失败。
        if (res instanceof String) {
            res += " < AfterReturning";
            System.out.println("[MyAfterReturningAspect] (后置通知) 修改方法返回值:string = " + res);
        }

        // 当 返回值类型为 其他引用类型的java对象时,可以修改成功。
        if (res instanceof Person) {
            Person person = (Person) res;
            person.setName("ZH-" + person.getName());
            System.out.println("[MyAfterReturningAspect] (后置通知) 修改方法返回值:person = " + person);
        }
    }
}

(5)@Around 环绕通知

  • 注解
    • 前置通知在目标方法执行之前或之后起作用。
  • 属性
    • value: 切入点表达式
  • 方法定义
    • public Object 方法名(参数)
    • 返回值类型必须有,推荐是Object,表示目标方法的执行结果返回值。
    • 第一个参数只能是ProceedingJoinPoint
    • ProceedingJoinPoint: 是JoinPoint的子类,代表执行的业务方法。可以执行目标方法proceed()、获取方法的相关信息,如参数、方法名等。
  • 特点
    • 可以选择是否执行目标方法。
    • 可以修改目标方法的返回结果。
package com.bpf.around.handler;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
@Aspect
public class MyAroundAspect {

    @Around(value = "execution(* *..SomeServiceImpl.do*(..) )")
    public Object process(ProceedingJoinPoint point) throws Throwable {
        System.out.println("[MyAroundAspect] (环绕通知) 目标方法之前:记录执行时间 " + new Date());

        // 执行目标方法并拿到执行结果
        Object result = point.proceed();

        if (result instanceof String) {
            String res = (String) result;
            if (res.contains("doSome")) {
                result = res.replace("doSome", "something here");
            }
        }

        System.out.println("[MyAroundAspect] (环绕通知) 目标方法之后:执行事务功能");

        return result;
    }
}

(6)@AfterThrowing 异常通知

  • 注解
    • 前置通知在目标方法执行抛出异常后起作用。
  • 属性
    • value: 切入点表达式
    • throwing: 声明自定义变量名,必须与形参中的变量名一致,代表目标方法抛出的异常对象。
  • 方法定义
    • public void 方法名(参数)
    • 第一个参数只能是JoinPoint
    • JoinPoint: 表示连接点,即执行的业务方法。可以获取方法的相关信息,如参数、方法名等。
    • Exception: 异常类型的参数表示目标方法执行时抛出的异常。
  • 特点
    • 只有在目标方法执行抛出异常时才执行,否则不执行。
    • 此切面方法只适合当作目标方法的监控程序,不适合作为异常处理程序!
package com.bpf.afterthrowing.handler;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
@Aspect
public class MyAfterThrowingAspect {

    @AfterThrowing(value = "execution(* *..SomeServiceImpl.do*(..) )",
            throwing = "ex")
    public void process(JoinPoint point, Exception ex) {
        System.out.println("[MyAfterThrowingAspect] (异常通知) 目标方法抛出异常时执行:");
        System.out.println("\t记录执行时间: " + new Date());
        System.out.println("\t记录异常信息:" + ex.getMessage());
        System.out.println("\t记录异常类型:" + ex.getClass());
    }
}

(7)@After 最终通知

  • 注解
    • 前置通知在目标方法的最后起作用。
  • 属性
    • value: 切入点表达式
  • 方法定义
    • public void 方法名(参数)
    • 第一个参数只能是JoinPoint
    • JoinPoint: 表示连接点,即执行的业务方法。可以获取方法的相关信息,如参数、方法名等。
  • 特点
    • 在目标方法的最后执行,无论有没有抛出异常。
package com.bpf.after.handler;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
@Aspect
public class MyAfterAspect {

    @After(value = "execution(* *..SomeServiceImpl.do*(..) )")
    public void process(JoinPoint point) {
        System.out.println("[MyAfterAspect] (最终通知) 目标方法的最后执行:记录完成时间 " + new Date());
    }
}

(8)@Pointcut 切入点表达式注解

  • 注解
    • 用于定义可复用的切入点表达式。
  • 属性
    • value: 切入点表达式
  • 方法定义
    • * void 方法名()
package com.bpf.pointcut.handler;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
@Aspect
public class MyAfterAspect {

    @Pointcut("execution(* *..SomeServiceImpl.do*(..) )")
    private void doMethods() {}

    @After(value = "doMethods()")
    public void process() {
        System.out.println("[MyAfterAspect] (最终通知) 目标方法的最后执行:记录完成时间 " + new Date());
    }

    @AfterThrowing(value = "doMethods()", throwing = "ex")
    public void process(Exception ex) {
        System.out.println("[MyAfterThrowingAspect] (异常通知) 目标方法抛出异常时执行:");
        System.out.println("\t记录执行时间: " + new Date());
        System.out.println("\t记录异常信息:" + ex.getMessage());
        System.out.println("\t记录异常类型:" + ex.getClass());
    }
}

3.5.2 XML方式

【详见 5.4 AspectJ 事务控制 】

3.6 AOP 总结

 AOP 是一种动态的技术思想,目的是实现业务功能和非业务功能的解耦合。

 当目标方法需要增加功能,而不想修改或不能修改源代码时,使用 AOP 技术就最适合不过了。

4. Spring 集成 MyBatis

Source Code

4.1 集成步骤

  1. 使用 MySQL 数据库,创建学生表
  2. 创建 maven 项目
  3. 导入依赖
  4. 创建实体类 Student
  5. 创建 DAO 接口 和 Mapper文件
  6. MyBatis 配置文件
  7. 创建 Service 接口和实现类
  8. Spring 配置文件
    1. 声明数据源 DataSource, 用于连接数据库
    2. 声明 SqlSessionFactoryBean, 用于创建 SqlSessionFactory 对象
    3. 声明 MapperScannerConfigurer, 用于创建 DAO 的代理对象
    4. 声明 Service 对象,并注入 DAO
  9. 测试方法测试

:当 MyBatis 整合 Spring 时,所有事务都默认是自动提交的。

CREATE TABLE `student` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(80) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

4.2 编码

<?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>com.bpf</groupId>
    <artifactId>M05-spring-mabtis</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.12</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.3.12</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.3.12</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.7</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.6</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.21</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <!-- 包含Mapper 文件 -->
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>
</project>
package com.bpf.dao;

import com.bpf.bean.Student;

import java.util.List;

public interface StudentDao {

    int insertStudent(Student student);

    List<Student> selectStudents();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bpf.dao.StudentDao">
    <!--int insertStudent(Student student);-->
    <insert id="insertStudent">
        insert into student(name, age) values(#{name}, #{age})
    </insert>
    <!--List<Student> selectStudents();-->
    <select id="selectStudents" resultType="com.bpf.bean.Student">
        select id,name,age from student
    </select>
</mapper>
package com.bpf.service;

import com.bpf.bean.Student;

import java.util.List;

public interface StudentService {

    int addStudent(Student student);

    List<Student> queryStudent();
}
package com.bpf.service.impl;

import com.bpf.bean.Student;
import com.bpf.dao.StudentDao;
import com.bpf.service.StudentService;

import java.util.List;

public class StudentServiceImpl implements StudentService {

    private StudentDao studentDao;

    public void setStudentDao(StudentDao studentDao) {
        this.studentDao = studentDao;
    }

    @Override
    public int addStudent(Student student) {
        return studentDao.insertStudent(student);
    }

    @Override
    public List<Student> queryStudent() {
        return studentDao.selectStudents();
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!-- 设置日志 -->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

    <!-- 别名 -->
    <typeAliases>
        <package name="com.bpf.bean"/>
    </typeAliases>

    <!-- 指定Mapper文件位置
        package: 需要保证 接口 与 mapper映射文件 在同一包下,且名称相同。
     -->
    <mappers>
        <package name="com.bpf.dao"/>
    </mappers>
</configuration>
<?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 https://www.springframework.org/schema/context/spring-context.xsd">

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

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
    </bean>

    <!-- 声明 SqlSessionFactoryBean 用来创建 SqlSessionFactory 对象 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" >
        <!-- 指定数据源 -->
        <property name="dataSource" ref="dataSource" />
        <!-- 指定MyBatis配置文件 -->
        <property name="configLocation" value="classpath:mybatis.xml" />
    </bean>

    <!-- 声明 MapperScannerConfigurer 用来创建 DAO接口的动态代理对象
        循环遍历 basePackage 中所有的接口,然后使用 SqlSession.getMapper() 为每个接口创建对应的对象,并添加到容器中。

        ApplicationContext ctx = ...

        SqlSessionFactory factory = ctx.getBean("", SqlSessionFactory.class);
        SqlSession session = factory.openSession();

        for(接口: com.bpf.dao ) {
            接口 对象 = session.getMapper(接口.class);
            springMap.put(对象名,对象);

        }

        对象名:接口名首字母小写
     -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" >
        <!-- 指定 SqlSessionFactory -->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
        <!-- 指定 DAO 接口所在的包 -->
        <property name="basePackage" value="com.bpf.dao" />
    </bean>

    <!-- 声明 StudentServiceImpl -->
    <bean id="studentService" class="com.bpf.service.impl.StudentServiceImpl">
        <property name="studentDao" ref="studentDao" />
    </bean>
</beans>
package com.bpf;

import com.bpf.bean.Student;
import com.bpf.service.StudentService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class StudentServiceTest {

    // 获取 Spring 容器中的对象
    @Test
    public void testSpringInfo() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

        System.out.println("定义的对象个数:" + ctx.getBeanDefinitionCount());
        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println("\t" + name + " ==> " + ctx.getBean(name));
        }

        /** 执行结果
         * 定义的对象个数:11
         * 	org.springframework.context.support.PropertySourcesPlaceholderConfigurer#0 ==> org.springframework.context.support.PropertySourcesPlaceholderConfigurer@6221a451
         * 	dataSource ==> {
         * 	    CreateTime:"2021-12-25 19:17:26",
         * 	    ActiveCount:0,
         * 	    PoolingCount:0,
         * 	    CreateCount:0,
         * 	    DestroyCount:0,
         * 	    CloseCount:0,
         * 	    ConnectCount:0,
         * 	    Connections:[]
         *  }
         * 	sqlSessionFactory ==> org.apache.ibatis.session.defaults.DefaultSqlSessionFactory@3012646b
         * 	org.mybatis.spring.mapper.MapperScannerConfigurer#0 ==> org.mybatis.spring.mapper.MapperScannerConfigurer@4a883b15
         * 	studentService ==> com.bpf.service.impl.StudentServiceImpl@25641d39
         * 	studentDao ==> org.apache.ibatis.binding.MapperProxy@7b36aa0c
         * 	org.springframework.context.annotation.internalConfigurationAnnotationProcessor ==> org.springframework.context.annotation.ConfigurationClassPostProcessor@5824a83d
         * 	org.springframework.context.annotation.internalAutowiredAnnotationProcessor ==> org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@537f60bf
         * 	org.springframework.context.annotation.internalCommonAnnotationProcessor ==> org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@5677323c
         * 	org.springframework.context.event.internalEventListenerProcessor ==> org.springframework.context.event.EventListenerMethodProcessor@18df8434
         * 	org.springframework.context.event.internalEventListenerFactory ==> org.springframework.context.event.DefaultEventListenerFactory@65c7a252
         */
    }

    // 插入数据
    @Test
    public void testInsert() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

        StudentService service = (StudentService) ctx.getBean("studentService");

        service.addStudent(new Student("Tom", 14));
        service.addStudent(new Student("Marry", 15));
    }

    // 查询数据
    @Test
    public void testQuery() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

        StudentService service = (StudentService) ctx.getBean("studentService");

        service.queryStudent().forEach(System.out::println);

        /** 执行结果
         * Student{id=1, name='Tom', age=14}
         * Student{id=2, name='Marry', age=15}
         */
    }
}

5. Spring 事务

事务:可以理解为多个 sql 语句的组合,要么都执行成功,要么都执行失败。

开发中,一般将事务放在 public 的业务方法上。

5.1 事务管理器

5.1.1 不同的数据库访问技术

(1)JDBC 的事务处理

public void updateAccount() {
    Connection conn = ...
    conn.setAutoCommit(false);
    stat.insert(..);
    stat.update(..);
    conn.commit();
    conn.setAutoCommit(true);
}

(2)MyBatis 的事务处理

public void updateAccount() {
    SqlSession session = SqlSession.openSession(false);

    try {
        session.insert(..);
        session.update();
        session.commit();
    } catch(Exception e) {
        session.rollback();
    }
}

5.1.2 Spring 统一事务管理

  由于不同的数据库技术使用的事务管理方式也不同。当项目中使用不同数据库且来回切换时,会导致代码需要频繁修改。
  Spring 提供了统一的事务管理器,用来管理不同数据库访问技术的事务处理。开发人员就只需要面对 Spring 的事务处理一种接口进行编程,省去了不同数据库之间的差别。

5.1.3 Spring 事务管理器

 Spring 提供的统一事务管理器接口是 PlatformTransactionManager

 这个接口提供了很多实现类,如对于 JDBC或MyBatis的DataSourceTransactionManager,Hibernate的HibernateTransactionManager等等。

public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}

5.1.4 Spring 事务的工作原理

 Spring 事务使用AOP 的环绕通知来实现目标业务方法的事务增强功能,这样就无需修改源代码了。

@Around("execution(* *..*.*(..))")
public Object myAround(ProceedingJoinPoint point) {
    try {
        // 事务开始
        PlatformTransactionManager.beginTransaction();
        // 执行目标业务方法
        point.proceed();
        PlatformTransactionManager.commit();
    } catch (Exception e) {
        PlatformTransactionManager.rollback();
    }
}

5.2 事务定义接口 TransactionDefinition

TransactionDefinition事务定义接口定义了事务隔离级别事务传播行为事务超时时间三类事务属性的常量值。

5.2.1 事务隔离级别

隔离级别:控制事务之间影响的程度。

隔离级别 说明
DEFAULT 根据数据库类型选择默认的隔离级别。
 MySQL:REPEATABLE_READ
 Oracle:READ_COMMITTED
READ_UNCOMMITTED 读未提交。为解决任何并发问题。
READ_COMMITTED 读已提交。解决脏读,存在不可重复读与幻读。
REPEATABLE_READ 可重复读。解决脏读、不可重复读,存在幻读。
SERIALIZABLE 串行化。不存在并发问题。

5.2.2 事务超时时间

超时时间:表示一个业务方法最长的执行时间,以秒为单位,整数值。默认值为-1,表示无限长。

5.2.3 事务传播行为

传播行为:当业务方法被调用时,事务在方法之间的传播和使用的变化。

传播行为 说明
PROPAGATION_REQUIRED 默认的传播行为。如果已存在事务就使用当前的事务,否则创建新事务。
PROPAGATION_REQUIRES_NEW 必须创建新事务,如果已存在事务就将其挂起。
PROPAGATION_SUPPORTS 有无事务都能正常执行。
PROPAGATION_NEVER
PROPAGATION_NOT_SUPPORTED
PROPAGATION_NESTED
PROPAGATION_MANDATORY

5.3 Spring 事务控制

Source Code

5.3.1 Spring 事务控制的方式

  Spring 框架提供了@Transactional注解用于控制事务。使用这个注解可以定义事务的属性,包括隔离级别、传播行为、超时时间等等。

注解属性

属性 类型 默认值 说明
propagation enum Propagation Propagation.REQUIRED 事务的传播行为
isolation enum Isolation Isolation.DEFAULT 事务的隔离级别
readOnly boolean false 是否只读
timeout int -1 事务的超时时间,单位:秒
rollbackFor Class<? extends Throwable>[] - 事务回滚的异常类列表,取值为异常类类型
rollbackForClassName String[] - 事务回滚的异常类列表,取值为异常类名称
noRollbackFor Class<? extends Throwable>[] - 事务不回滚的异常类列表
noRollbackForClassName String[] - 事务不回滚的异常类列表
  • rollbackFor: 当业务方法抛出的异常存在于参数列表中时,事务一定回滚;否则继续判断是否为RuntimeException或其子类,若是则事务一定回滚。

使用方法

  1. 在 Spring 配置文件中声明事务管理器
  2. 在 Spring 配置文件中声明开启事务注解驱动
  3. 在 public 的 Service 方法上使用@Transactional注解。

特点

  • 优点:使用方便,效率高;适合中小型项目。
  • 缺点:需要改动源代码。

5.3.2 牛刀小试

Spring 配置文件

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

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

Service方法

@Transactional(
        propagation = Propagation.REQUIRED,
        isolation = Isolation.DEFAULT,
        timeout = 20, readOnly = false,
        rollbackFor = {NullPointerException.class, GoodNotEnoughException.class})
/**
 * rollbackFor: 表示当抛出的异常属于 NullPointerException 或 GoodNotEnoughException,事务一定回滚,
 *          否则如果是 RuntimeException,事务也一定回滚。
 */
public class BuyGoodServiceImpl implements BuyGoodService {...}

5.4 AspectJ 事务控制

Source Code

5.4.1 AspectJ 事务控制的方式

  AspectJ 框架通过全配置文件的方式进行事务控制,无需改动代码。

使用方法

  1. 导入依赖:spring-aspects
  2. 在 Spring 配置文件中声明事务管理器
  3. 在 Spring 配置文件中声明业务方法的事务属性和切入点表达式

特点

  • 缺点:理解难,配置较复杂。
  • 优点:实现代码于事务配置解耦,实现事务功能无需修改源代码;哪个快速的了解和掌控项目的全部事务;适合大型项目。

5.4.2 牛刀小试

导入依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.12</version>
</dependency>

Spring 配置文件

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

<!-- 声明业务方法的事务属性
    在此,只声明具有某种规则方法名的方法具有哪些事务属性,而没指定具体哪些方法具有事务功能
    -->
<tx:advice id="serviceAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!--
            tx:method  指定具有哪些规则方法名的方法的事务属性
                name        可以使用通配符,若只有*,表示除上述的方法之外
                propagation 传播行为
                isolation   隔离级别
                read-only   是否只读
                timeout     超时时间
                rollback-for 事务回滚时的异常类型列表,使用逗号(,)隔开
        -->
        <tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT"
                    read-only="false" timeout="20"
                    rollback-for="java.lang.NullPointerException, com.bpf.except.GoodNotEnoughException"/>

        <tx:method name="add*" propagation="REQUIRES_NEW" rollback-for="java.lang.Exception" />
        <tx:method name="modify*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
        <tx:method name="remove*" propagation="REQUIRED" rollback-for="java.lang.Exception" />

        <!-- "*" 表示除了上述方法之外使用下面的事务属性: query*, search*, find*, get* ... -->
        <tx:method name="*" propagation="SUPPORTS" read-only="true" />
    </tx:attributes>
</tx:advice>

<aop:config>
    <!-- 声明切入点表达式:表示任何包下的service下的任何方法 -->
    <aop:pointcut id="servicePoint" expression="execution(* *..service..*.*(..))"/>

    <!-- 关联切入点表达式 与 事务通知 -->
    <aop:advisor advice-ref="serviceAdvice" pointcut-ref="servicePoint" />
</aop:config>

6. Spring Web

Source Code

6.1 存在的问题

在每个 Servlet 程序中,如果每次的容器对象都是新建出来的,那一个请求就会创建一次容器,这不仅耗时,而且浪费空间。

public class AddStudentServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getParameter("name");
        String age = req.getParameter("age");
        Student student = new Student(name, Integer.valueOf(age));

        // 直接创建容器对象
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        System.out.println("ctx = " + ctx);

        // 从容器中获取 Service 对象
        StudentService studentService = (StudentService) ctx.getBean("studentService");
        studentService.addStudent(student);

        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html");
        resp.getWriter().write("<h1>注册成功</h1>");
    }
}
26-Dec-2021 15:48:06.790 信息 [http-nio-8080-exec-4] com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl.info {dataSource-2} inited
ctx = org.springframework.context.support.ClassPathXmlApplicationContext@340804ae, started on Sun Dec 26 15:48:06 CST 2021

26-Dec-2021 15:48:12.408 信息 [http-nio-8080-exec-5] com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl.info {dataSource-3} inited
ctx = org.springframework.context.support.ClassPathXmlApplicationContext@4db4beb9, started on Sun Dec 26 15:48:12 CST 2021

26-Dec-2021 15:48:15.947 信息 [http-nio-8080-exec-6] com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl.info {dataSource-4} inited
ctx = org.springframework.context.support.ClassPathXmlApplicationContext@2f50dd69, started on Sun Dec 26 15:48:15 CST 2021
...

6.2 解决方法

目前要处理的问题是:只让容器对象创建一次,并且能在多个Servlet程序之间共享。

 Spring 提供了监听器ServletContextListener,它可以创建容器对象,并且能够放入ServletContext全局共享作用域中。

 在这个接口中,定义了两个方法:分别对应初始化时的操作和销毁时的操作。

public interface ServletContextListener extends EventListener {
    public void contextInitialized(ServletContextEvent sce);

    public void contextDestroyed(ServletContextEvent sce);
}

 接口的常用实现类是ContextLoaderListener。类中的contextInitialized()方法调用的是父类ContextLoaderinitWebApplicationContext方法。

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    try {
        // 如果 容器对象为空,就创建
        if (this.context == null) {
            this.context = this.createWebApplicationContext(servletContext);
        }

        // 将创建的 容器对象 放入 ServletContext中
        // WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE: 用于保存的key
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

        return this.context;
    } catch (Error | RuntimeException e) {
        ...
    }
}

6.3 如何使用监听器

导入依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.3.12</version>
</dependency>

在web.xml中声明监听器

<!-- 配置监听器,用于创建容器对象 ApplicationContext 并放入全局共享域 ServletContext 中
    ContextLoaderListener 默认读取配置文件的地址是: /WEB-INF/applicationContext.xml
    可以通过 <context-param> 重新指定配置文件路径
    -->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- 指定 Spring 配置文件路径 -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
</context-param>

Servlet程序中获取容器对象

 从上述源码可见,监听器将容器对象保存到ServletContext的key是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,于是:

public class AddStudentServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getParameter("name");
        String age = req.getParameter("age");
        Student student = new Student(name, Integer.valueOf(age));

        // 方法一:直接创建容器对象
        // ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

        // 方法二:使用监听器,然后从 ServletContext 中获取容器对象
        WebApplicationContext ctx = null;
        // ContextLoaderListener 监听器将容器对象保存到 ServletContext 中的key
        String key = WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE;
        Object attribute = getServletContext().getAttribute(key);
        if (attribute != null) {
            ctx = (WebApplicationContext) attribute;
        }

        System.out.println("ctx = " + ctx);

        // 从容器中获取 Service 对象
        StudentService studentService = (StudentService) ctx.getBean("studentService");
        studentService.addStudent(student);

        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html");
        resp.getWriter().write("<h1>注册成功</h1>");
    }
}

 如果觉得麻烦,可以使用 Spring 提供的工具类,用来获取容器对象,其实就是上述代码的封装。

// 不同在于:第一个方法找不到时会抛异常 java.lang.IllegalStateException
WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(getServletContext());

7. Spring Test

Source Code

  Spring 提供了专门的测试模块,可以方便程序测试。

7.1 使用之前

<?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 https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.bpf.bean" />
</beans>
 <dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.12</version>
    </dependency>

    <!-- junit4 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13</version>
        <scope>test</scope>
    </dependency>
</dependencies>
package com.bpf;

import com.bpf.bean.Student;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringTest01 {

    @Test
    public void test() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

        Student student = ctx.getBean("student", Student.class);
        System.out.println("student = " + student);
    }
}

7.2 Junit 4 的 Spring Test

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.12</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.3.12</version>
    </dependency>

    <!-- junit4 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13</version>
        <scope>test</scope>
    </dependency>
</dependencies>
package com.bpf;

import com.bpf.bean.Student;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class SpringTest02 {

    @Resource
    private Student student;

    @Test
    public void test() {
        System.out.println("student = " + student);
    }
}

7.3 Junit 5 的 Spring Test

 Spring Test 提供了两种用于 Junit 5 的注解:

  • @SpringJUnitConfig: 用于普通的测试工程。
    • String[] locations: Spring 配置文件
    • Class<?>[] classes: Spring 配置类,此属性同value
  • @SpringJUnitWebConfig: 用于Web的测试工程。
    • String resourcePath: 指定web目录,默认值为: src/main/webapp
    • 注解属性只比@SpringJUnitConfig多了一个resourcePath
    • 在注解定义上比@SpringJUnitConfig多了一个@WebAppConfiguration
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.12</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.3.12</version>
    </dependency>

    <!-- junit5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.8.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>
package com.bpf;

import com.bpf.bean.Student;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import javax.annotation.Resource;

@SpringJUnitConfig(locations = "classpath:applicationContext.xml")
public class SpringTest03 {

    @Resource
    private Student student;

    @Test
    public void test() {
        System.out.println("student = " + student);
    }
}

8. Spring JDBC

  Spring JDBC 模块提供了数据访问技术,底层实现对原生 JDBC 操作进行抽象处理,方便开发使用。

操作 Spring 已完成 开发者 待完成
定义连接参数
打开连接
指定SQL语句
声明参数和提供参数值
准备和执行语句
返回结果的迭代(若存在)
具体操作每个迭代
异常处理
事务处理
管理连接

8.1 Spring JDBC API

 Spring JDBC 在数据访问实现上提供了多种API:

API 描述
org.springframework.jdbc.core
JdbcTemplate
最常用、最基础。
org.springframework.jdbc.core.namedparam
NamedParameterJdbcTemplate
JdbcTemplate的基础上,可以使用命名参数代替占位符?,具有更好的可读性
org.springframework.jdbc.core.simple
SimpleJdbcInsert
SimpleJdbcCall
利用JDBC驱动所提供的数据库元数据的一些特性来简化数据库操作。
org.springframework.jdbc.object
MappingSqlQuery
SqlUpdate
StoredProcedure
需要开发者在初始化应用数据访问层时创建可重用和线程安全的对象。

8.2 Spring JDBC 使用

Source Code

导入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.12</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.3.12</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.26</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.21</version>
    </dependency>
</dependencies>

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 https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 注解扫描 -->
    <context:component-scan base-package="com.bpf" />

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

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

8.3 Spring JdbcTemplate

 Spring 中要使用JdbcTemplate需要在配置文件中注入后才能使用:

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

8.3.1 增删改

 JdbcTemplate 中提供了多个增删改的重载方法update(..)

(1)public int update(String sql, @Nullable Object... args)

 这个是最常用的方法,只需提供sql语句和参数值。

/**
 * 根据用户ID更新用户信息
 *
 * @param user
 * @return
 */
@Override
public Integer update(User user) {
    String sql = "update user set u_name = ?, u_pwd = ? where u_id = ?";

    return jdbcTemplate.update(sql, user.getUsername(),
        user.getPassword(), user.getId());
}

(2)public int update(String sql, @Nullable PreparedStatementSetter pss)

 这个方法本质上与上一方法的相同的,只是将参数赋值的操作自己实现而已。PreparedStatementSetter是一个函数式接口,它有两个实现类:

  • ArgumentTypePreparedStatementSetter: 重写setValues()方法时,为参数赋值时需指定参数类型(java.sql.Types的静态常量)。
  • ArgumentPreparedStatementSetter: 重写setValues()方法时,为参数赋值时会获取参数类型,并做相应赋值。
@FunctionalInterface
public interface PreparedStatementSetter {
    void setValues(PreparedStatement ps) throws SQLException;
}
/**
 * 根据用户ID更新用户信息
 *
 * @param user
 * @return
 */
@Override
public Integer update(User user) {
    String sql = "update user set u_name = ?, u_pwd = ? where u_id = ?";

    // 方法一: 使用 PreparedStatementSetter 匿名实现类 
    return jdbcTemplate.update(sql, new PreparedStatementSetter() {
        @Override
        public void setValues(PreparedStatement ps) throws SQLException {
            ps.setString(1, user.getUsername());
            ps.setString(2, user.getPassword());
            ps.setInt(3, user.getId());
        }
    });

    // 方法二: 使用 ArgumentTypePreparedStatementSetter(@Nullable Object[] args, @Nullable int[] argTypes)
    return jdbcTemplate.update(sql, new ArgumentTypePreparedStatementSetter(
            new Object[] {user.getUsername(), user.getPassword(), user.getId()},
            new int[] {Types.VARCHAR, Types.VARCHAR, Types.INTEGER}));

    // 方法三: 使用 ArgumentPreparedStatementSetter(@Nullable Object[] args)
    return jdbcTemplate.update(sql, new ArgumentPreparedStatementSetter(
            new Object[] {user.getUsername(), user.getPassword(), user.getId()}));
}

(3)public int update(final PreparedStatementCreator psc, final KeyHolder generatedKeyHolder)

PreparedStatementCreator 是一个函数式接口,执行方法后会将主键值保存KeyHolder中。

KeyHolder 接口的唯一实现类是GeneratedKeyHolder

/**
 * 保存用户,返回主键
 *
 * @param user
 * @return
 */
@Override
public Integer insertAndReturnKey(User user) {
    // 使用 KeyHolder 的唯一实现类 GeneratedKeyHolder
    GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();

    // 方法第一个参数: PreparedStatementCreator 是一个函数式接口,参数是 Connection, 返回值是 PreparedStatement
    // 方法第二个参数: KeyHolder. 方法执行后会将主键值放在 keyHolder 中
    jdbcTemplate.update(conn -> {
        Statement statement = conn.createStatement();
        PreparedStatement ps = conn.prepareStatement("insert into user(u_name, u_pwd) values(?, ?)", Statement.RETURN_GENERATED_KEYS);
        ps.setString(1, user.getUsername());
        ps.setString(2, user.getPassword());

        return ps;
    }, keyHolder);

    return keyHolder.getKey().intValue();
}

8.3.2 查询

(1)查询一个值

public <T> T queryForObject(String sql, Class<T> requiredType, @Nullable Object... args)
/**
 * 统计用户总记录
 *
 * @return
 */
@Override
public Integer count() {
    return jdbcTemplate.queryForObject("select count(1) from user", Integer.class);
}

(2)查询一列值

public <T> List<T> queryForList(String sql, Class<T> elementType, @Nullable Object... args)

(3)查询一个对象

public <T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args)
/**
 * 根据ID查询用户信息
 *
 * @param id
 * @return
 */
@Override
public User selectById(Integer id) {
    String sql = "select u_id,u_name,u_pwd from user where u_id = ?";

    // public <T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args) throws DataAccessException
    // 此处也可使用 BeanPropertyRowMapper
    return jdbcTemplate.queryForObject(sql, new UserRowMapper(), id);
}

(4)查询多个对象

使用函数式接口ResultSetExtractor

@FunctionalInterface
public interface ResultSetExtractor<T> {
    @Nullable
    T extractData(ResultSet rs) throws SQLException, DataAccessException;
}
/**
 * 查询所有用户信息
 *
 * @return
 */
@Override
public List<User> selectUsers() {
    String sql = "select u_id,u_name,u_pwd from user";

    return jdbcTemplate.query(sql, new ResultSetExtractor<List<User>>() {
        @Override
        public List<User> extractData(ResultSet rs) throws SQLException, DataAccessException {
            List<User> userList = new ArrayList<>();
            while (rs.next()) {
                User user = new User();
                user.setId(rs.getInt("u_id"));
                user.setUsername(rs.getString("u_name"));
                user.setPassword(rs.getString("u_pwd"));
                userList.add(user);
            }
            return userList;
        }
    });
}

使用函数式接口RowCallbackHandler

@FunctionalInterface
public interface RowCallbackHandler {
    void processRow(ResultSet rs) throws SQLException;
}
/**
 * 查询所有用户信息
 *
 * @return
 */
@Override
public List<User> selectUsers() {
    String sql = "select u_id,u_name,u_pwd from user";

    List<User> userList = new ArrayList<>();
    jdbcTemplate.query(sql, new RowCallbackHandler() {
        @Override
        public void processRow(ResultSet rs) throws SQLException {
            User user = new User();
            user.setId(rs.getInt("u_id"));
            user.setUsername(rs.getString("u_name"));
            user.setPassword(rs.getString("u_pwd"));
            userList.add(user);
        }
    });
    return userList;
}

使用函数式接口RowMapper

@FunctionalInterface
public interface RowMapper<T> {
    @Nullable
    T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
/**
 * 查询所有用户信息
 *
 * @return
 */
@Override
public List<User> selectUsers() {
    // 方法一: 使用自定义实现类
    String sql = "select u_id,u_name,u_pwd from user";
    return jdbcTemplate.query(sql, new UserRowMapper());

    // 方法二: 使用 BeanPropertyRowMapper, 但需要列名与属性名相同
    sql = "select u_id id, u_name username, u_pwd password from user";
    return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
}

// 自定义 RowMapper 实现类
public static class UserRowMapper implements RowMapper<User> {
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = new User();
        user.setId(rs.getInt("u_id"));
        user.setUsername(rs.getString("u_name"));
        user.setPassword(rs.getString("u_pwd"));
        return user;
    }
}

8.3.3 批量增删改查

 批量操作常用的是借助函数式接口BatchPreparedStatementSetter的方法:

public int[] batchUpdate(String sql, final BatchPreparedStatementSetter pss)
public interface BatchPreparedStatementSetter {
    void setValues(PreparedStatement ps, int i) throws SQLException;

    int getBatchSize();
}
/**
 * 批量保存用户,返回影响的行数
 *
 * @param list
 * @return
 */
@Override
public Integer batchInsert(List<User> list) {
    String sql = "insert into user(u_name,u_pwd) values(?,?)";
    return Arrays.stream(jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
        @Override
        public void setValues(PreparedStatement ps, int i) throws SQLException {
            User user = list.get(i);
            ps.setString(1, user.getUsername());
            ps.setString(2, user.getPassword());
        }

        @Override
        public int getBatchSize() {
            return list.size();
        }
    })).sum();
}

8.4 Spring NamedParameterJdbcTemplate

 使用之前需要先在配置文件中注入对象:

<!-- 注入 NamedParameterJdbcTemplate -->
<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
    <constructor-arg name="dataSource" ref="dataSource" />
</bean>

  在NamedParameterJdbcTemplate的方法参数中,大部分使用到了SqlParameterSource接口。这个接口有三大实现类:

003_SqlParameterSource实现类

8.4.1 BeanPropertySqlParameterSource

/**
 * 保存用户,返回影响的行数
 *
 * @param user
 * @return
 */
@Override
public Integer insert(User user) {
    String sql = "insert into user(u_name,u_pwd) values(:name, :pwd)";

    /**
        * public int update(String sql, Map<String, ?> paramMap)
        *      需要手动组装 Map 对象数据
        */
    Map<String, Object> map = new HashMap<>();
    map.put("name", user.getUsername());
    map.put("pwd", user.getPassword());
    // return namedParameterJdbcTemplate.update(sql, map);

    /**
        * public int update(String sql, SqlParameterSource paramSource)
        *      BeanPropertySqlParameterSource 使用时需要注意命名的参数需与对象的属性名相同!
        */
    sql = "insert into user(u_name,u_pwd) values(:username, :password)";
    return namedParameterJdbcTemplate.update(sql, new BeanPropertySqlParameterSource(user));
}

/**
 * 保存用户,返回主键
 *
 * @param user
 * @return
 */
@Override
public Integer insertAndReturnKey(User user) {
    String sql = "insert into user(u_name,u_pwd) values(:username, :password)";
    GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
    /**
        * public int update(String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHolder)
        */
    namedParameterJdbcTemplate.update(sql, new BeanPropertySqlParameterSource(user), keyHolder);
    return keyHolder.getKey().intValue();
}

8.4.2 MapSqlParameterSourceEmptySqlParameterSource

/**
 * 根据用户ID删除
 *
 * @param id
 * @return
 */
@Override
public Integer delete(Integer id) {
    String sql = "delete from user where u_id=:id";
    // 只有一对 key:value 时可以直接使用 MapSqlParameterSource(String, Object)
    return namedParameterJdbcTemplate.update(sql, new MapSqlParameterSource("id", id));
}

/**
 * 统计用户总记录
 *
 * @return
 */
@Override
public Integer count() {
    String sql = "select count(1) from user";
    // EmptySqlParameterSource 对应空参数
    return namedParameterJdbcTemplate.queryForObject(sql, new EmptySqlParameterSource(), Integer.class);
}

8.5 Spring SimpleJdbcInsert

SimpleJdbcInsert可以简化数据插入的操作,不需要写sql语句。使用之前需要先在配置文件中注入对象:

<!-- 注入 SimpleJdbcInsert -->
<bean id="simpleJdbcInsert" class="org.springframework.jdbc.core.simple.SimpleJdbcInsert">
    <constructor-arg name="dataSource" ref="dataSource" />
</bean>

 使用时可以让DAO实现类实现InitializingBean接口,在重写的方法afterPropertiesSet()中指定操作的表、数据列等属性。

package com.bpf.dao.simple;

import com.bpf.bean.User;
import com.bpf.dao.UserDao;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Repository("userDaoImplSimpleInsert")
public class UserDaoImpl implements UserDao, InitializingBean {

    @Resource
    private SimpleJdbcInsert jdbcInsert;

    /**
     * Bean 的初始化方法,进行 SimpleJdbcInsert 的通用配置
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        // 指定操作哪个表,若此处不指定,需在每处使用时都指定
        jdbcInsert.withTableName("user");

        // 配置代操作的列名
        // jdbcInsert.usingColumns("u_id", "u_name", "u_pwd");
    }

    /**
     * 保存用户,返回影响的行数
     *
     * @param user
     * @return
     */
    @Override
    public Integer insert(User user) {
        Map<String, Object> map = new HashMap<>();
        map.put("u_name", user.getUsername());
        map.put("u_pwd", user.getPassword());
        return jdbcInsert.execute(map);

        // 使用 BeanPropertySqlParameterSource 需要数据表列名与属性名相同!
        // return jdbcInsert.execute(new BeanPropertySqlParameterSource(user));
    }

    /**
     * 保存用户,返回主键
     *
     * @param user
     * @return
     */
    @Override
    public Integer insertAndReturnKey(User user) {
        Map<String, Object> map = new HashMap<>();
        map.put("u_name", user.getUsername());
        map.put("u_pwd", user.getPassword());
        return jdbcInsert.usingGeneratedKeyColumns("u_id").executeAndReturnKey(map).intValue();
    }

    /**
     * 批量保存用户,返回影响的行数
     *
     * @param list
     * @return
     */
    @Override
    public Integer batchInsert(List<User> list) {

        List<Map<String, Object>> collect = list.stream().map(user -> {
            Map<String, Object> map = new HashMap<>();
            map.put("u_name", user.getUsername());
            map.put("u_pwd", user.getPassword());
            return map;
        }).collect(Collectors.toList());

        Map<String, Object>[] maps = collect.toArray(new Map[collect.size()]);
        return Arrays.stream(jdbcInsert.executeBatch(maps)).sum();

        // 数据表列名与属性名相同才能这样用:
        // return Arrays.stream(jdbcInsert.executeBatch(SqlParameterSourceUtils.createBatch(list))).sum();
    }
}
posted @ 2021-12-27 12:44  步平凡  阅读(192)  评论(0编辑  收藏  举报