Spring5 学习笔记

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

个人代码: GitHub

  1. Spring 概述

1.1 Spring 简介

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

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

Spring5 学习笔记

1.2 优点

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

  1. 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. 添加依赖
  3. spring-context
  4. junit
  5. 定义接口和实现类
  6. 接口: SomeService
    • 方法: doSome(): void
  7. 实现类: SomeServiceImpl
  8. 创建 Spring 配置文件(.xml),声明需要创建的对象
  9. 通过 <bean></bean>标签声明对象,一个标签对应一个对象。
  10. 使用容器中的对象
  11. 创建 ApplicationContext 对象
  12. 通过 getBean() 获取容器中的对象

2.3.2 依赖文件


    4.0.0

    com.bpf
    M01-ioc-demo
    1.0-SNAPSHOT

            org.springframework
            spring-context
            5.3.12

            junit
            junit
            4.13
            test

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 配置文件


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。标准的配置文件格式如下:


Spring5 学习笔记

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


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属性指定创建的对象类型。


2.5.3 注解方式

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

Spring 提供了四个注解

注解 说明 @Component

表示普通的java对象 @Repository

常用于创建DAO层的对象,持久层对象,表示可以访问数据库 @Service

常用于创建Service层的对象,业务层对象,表示拥有事务功能 @Controller

常用于创建Controller层的对象,控制器对象,表示可以接收和处理请求。

配置文件开启注解扫描:


2.6 Spring IOC ☞ 属性注入

Source Code

2.6.1 XML方式

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

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

特点

  • 注入的属性 必须存在对应的 setter 方法
  • 如果属性在对象中不存在,但存在 setter 方法,依然不会报错。
  • Spring 容器 只负责调用 setter 方法,与方法的具体实现无关。

    ...

    ...

    ...

    ...

            xxx

            xxx

            xxx

            xxx

(2)构造注入

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

特点

  • 不需要属性的 setter 方法
  • 需要有相对应的含参构造器

    ...

(3)引用类型自动注入

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

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

特点

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

    ...

(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)指定。

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;
}
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(" [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();

        /** 执行结果
         *  [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 及之前是自带此注解的,更高的版本需要手动导入依赖。

    javax.annotation
    javax.annotation-api
    1.3.2

2.7 Spring IOC 总结

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

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

补充

> 完全注解开发

> Spring Bean 的生命周期

  1. 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 基金会。
  • 官网:https://www.eclipse.org/aspectj/

3.5 AspectJ 框架

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


    org.springframework
    spring-aspects
    5.3.12

3.5.1 注解方式

Source Code

(1)Advice 通知注解

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

注解 通知类型 执行时间 @Before

前置通知 业务方法前执行 @AfterReturning

后置通知 业务方法后执行 @Around

环绕通知 业务方法前和后都执行 @AfterThrowing

异常通知 业务方法过程中出现异常时执行 @After

最终通知 业务方法后执行

(2)Pointcut 切入点表达式

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

execution(访问权限? 返回值类型 全限定类名?方法名(参数列表) 异常类型?)
  • ? 代表可选。
  • 最简形式: execution(&#x8FD4;&#x56DE;&#x503C;&#x7C7B;&#x578B; &#x65B9;&#x6CD5;&#x540D;(&#x53C2;&#x6570;&#x5217;&#x8868;))
  • 四个部分之间通过空格分开,并且都可以使用通配符👇。

通配符 含义 *

代表任意字符 ..

用在方法参数中,表示任意参数列表

用在包名中,表示当前包及其子包路径 +

用在类名后,表示当前类及其子类

用在接口后,表示当前接口及其实现类

(3)@Before 前置通知

  • 注解
  • 前置通知在 目标方法执行之前起作用。
  • 属性
  • value: 切入点表达式
  • 方法定义
  • public void &#x65B9;&#x6CD5;&#x540D;(&#x53C2;&#x6570;)
  • 第一个参数只能是 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 &#x65B9;&#x6CD5;&#x540D;(&#x53C2;&#x6570;)
  • 第一个参数只能是 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 &#x65B9;&#x6CD5;&#x540D;(&#x53C2;&#x6570;)
  • 返回值类型必须有,推荐是 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 &#x65B9;&#x6CD5;&#x540D;(&#x53C2;&#x6570;)
  • 第一个参数只能是 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 &#x65B9;&#x6CD5;&#x540D;(&#x53C2;&#x6570;)
  • 第一个参数只能是 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 &#x65B9;&#x6CD5;&#x540D;()
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 技术就最适合不过了。

  1. Spring 集成 MyBatis

Source Code

4.1 集成步骤

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

:当 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 编码


    4.0.0

    com.bpf
    M05-spring-mabtis
    1.0-SNAPSHOT

            org.springframework
            spring-context
            5.3.12

            org.springframework
            spring-jdbc
            5.3.12

            org.springframework
            spring-tx
            5.3.12

            org.mybatis
            mybatis
            3.5.7

            org.mybatis
            mybatis-spring
            2.0.6

            mysql
            mysql-connector-java
            8.0.26

            com.alibaba
            druid
            1.1.21

            junit
            junit
            4.13
            test

                src/main/java

                    **/*.xml

                false

package com.bpf.dao;

import com.bpf.bean.Student;

import java.util.List;

public interface StudentDao {

    int insertStudent(Student student);

    List selectStudents();
}

        insert into student(name, age) values(#{name}, #{age})

        select id,name,age from student

package com.bpf.service;

import com.bpf.bean.Student;

import java.util.List;

public interface StudentService {

    int addStudent(Student student);

    List 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 queryStudent() {
        return studentDao.selectStudents();
    }
}
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}
         */
    }
}
  1. 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 配置文件


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 牛刀小试

导入依赖


    org.springframework
    spring-aspects
    5.3.12

Spring 配置文件


  1. 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("注册成功");
    }
}
26-Dec-2021 15:48:06.790 &#x4FE1;&#x606F; [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 &#x4FE1;&#x606F; [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 &#x4FE1;&#x606F; [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 如何使用监听器

导入依赖


    org.springframework
    spring-web
    5.3.12

在web.xml中声明监听器


    org.springframework.web.context.ContextLoaderListener

    contextConfigLocation
    classpath:applicationContext.xml

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("注册成功");
    }
}

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

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

Source Code

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

7.1 使用之前


        org.springframework
        spring-context
        5.3.12

        junit
        junit
        4.13
        test

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


        org.springframework
        spring-context
        5.3.12

        org.springframework
        spring-test
        5.3.12

        junit
        junit
        4.13
        test

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

        org.springframework
        spring-context
        5.3.12

        org.springframework
        spring-test
        5.3.12

        org.junit.jupiter
        junit-jupiter-api
        5.8.2
        test

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);
    }
}
  1. 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

导入依赖


        org.springframework
        spring-context
        5.3.12

        org.springframework
        spring-jdbc
        5.3.12

        mysql
        mysql-connector-java
        8.0.26

        com.alibaba
        druid
        1.1.21

Spring 配置文件


8.3 Spring JdbcTemplate

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


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

(2)查询一列值

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

(3)查询一个对象

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

(4)查询多个对象

使用函数式接口 ResultSetExtractor

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

    return jdbcTemplate.query(sql, new ResultSetExtractor>() {
        @Override
        public List extractData(ResultSet rs) throws SQLException, DataAccessException {
            List 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 selectUsers() {
    String sql = "select u_id,u_name,u_pwd from user";

    List 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 {
    @Nullable
    T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
/**
 * 查询所有用户信息
 *
 * @return
 */
@Override
public List 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 {
    @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 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的方法参数中,大部分使用到了 SqlParameterSource接口。这个接口有三大实现类:

Spring5 学习笔记

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 paramMap)
        *      需要手动组装 Map 对象数据
        */
    Map 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语句。使用之前需要先在配置文件中注入对象:


使用时可以让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 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 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 list) {

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

        Map[] maps = collect.toArray(new Map[collect.size()]);
        return Arrays.stream(jdbcInsert.executeBatch(maps)).sum();

        // 数据表列名与属性名相同才能这样用:
        // return Arrays.stream(jdbcInsert.executeBatch(SqlParameterSourceUtils.createBatch(list))).sum();
    }
}

Original: https://www.cnblogs.com/bpf-1024/p/15735827.html
Author: 步平凡
Title: Spring5 学习笔记

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/612293/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • flask 之上传本地图片

    项目配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import os class Config(object): DEBUG = True SQLALCH…

    Linux 2023年6月8日
    0107
  • 秒懂悲观、乐观锁、互斥、自旋锁、读写锁

    前言 生活中用到的锁,用途都比较简单粗暴,上锁基本是为了防止外人进来、电动车被偷等等。 但生活中也不是没有 BUG 的,比如加锁的电动车在「广西 – 窃·格瓦拉」面前,…

    Linux 2023年6月14日
    0110
  • 博客园装饰——(一)置顶菜单栏

    功能描述:当页面向下滚动到菜单栏上边沿触碰到浏览器窗口上边沿时,菜单栏会固定地显示在浏览器窗口上方(贴紧),即达到了置顶菜单栏的效果。而当页面向上滚动到原来的位置时,菜单栏又会自动…

    Linux 2023年6月14日
    0111
  • python爬虫配置随机请求头headers伪装User-Agent

    fake_useragent 库 调用方法 ua.random可以随机返回一个headers(User-Agent) from fake_useragent import User…

    Linux 2023年6月14日
    089
  • 剑指offer计划29(动态规划困难)—java

    1.1、题目1 剑指 Offer 19. 正则表达式匹配 1.2、解法 动态规划后面再研究 1.3、代码 class Solution { public boolean isMat…

    Linux 2023年6月11日
    0108
  • 安装及管理文件

    优点: 契合系统兼容性强 如果你可以看懂源代码,修改新增功能 比较自由 缺点: 如果编译出了问题,你看不懂源代码,无法解决 安装过程复杂 没有统一的管理人员 安装过程 程序包编译安…

    Linux 2023年6月6日
    0101
  • Linux 查看运行中进程的 umask

    线上某台虚机因为故障重装了系统(基线 CentOS 6.9 内核 2.6.x),重新部署了应用。这个应用会生成一个文件,到NFS挂载目录。 而这个 NFS 挂载目录是一个 FTP …

    Linux 2023年5月27日
    0153
  • Linux下如何修复陈旧的第三方微信版本electronic-wechat

    因为现在的Linux发行版软件库太新的缘故,导致陈旧的electronic-wechat的文本引擎库不能正确运行,一般表现为harfbuzz too old等错误。 即使你把har…

    Linux 2023年6月14日
    0110
  • debian与windows时间不同步的简单治疗方法

    试过几种方法, 但就这个方法好使点。hwclock -w –localtime Original: https://www.cnblogs.com/leotiger/p…

    Linux 2023年6月13日
    072
  • 【原创】Linux虚拟化KVM-Qemu分析(六)之中断虚拟化

    背景 Read the fucking source code! –By 鲁迅 A picture is worth a thousand words. –…

    Linux 2023年6月8日
    0109
  • Shell第三章《for循环》

    语法结构: for &#x53D8;&#x91CF;&#x540D; [ in &#x53D6;&#x503C;&#x5217;&a…

    Linux 2023年6月6日
    0146
  • Mac远程windows工具

    现在很多小伙伴都是使用MAC系统,但在工作中或多或少会遇到需要远程windows的情况,今天给大家安利一款软件,让你轻轻松松远程windows Microsoft Remote D…

    Linux 2023年6月7日
    0103
  • shell脚本调试方法

    set -x bash -x test.sh 作者:习惯沉淀 如果文中有误或对本文有不同的见解,欢迎在评论区留言。 如果觉得文章对你有帮助,请点击文章右下角【推荐】一下。您的鼓励是…

    Linux 2023年5月28日
    093
  • .htaccess文件解析漏洞

    前言 htaccess文件(或者”分布式配置文件”),全称是Hypertext Access(超文本入口)。提供了针对目录改变配置的方法, 即,在一个特定的…

    Linux 2023年6月13日
    079
  • 虚拟机Ubuntu22.04 chrome页面显示异常

    虚拟机上ubuntu安装chrome出现页面显示异常的解决办法 将chrome上的硬件加速关掉就能恢复正常 具体原因是啥,有没有大佬能解答以下啊 引用:https://blog.c…

    Linux 2023年6月13日
    086
  • Debian10 命令行启动

    打开 default grub 配置 $ sudo vi /etc/default/grub 修改以下3处内容 保存修改 更新grub配置 $ sudo update-grub 设…

    Linux 2023年5月27日
    0101
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球