Spring6全篇学习笔记下

93 阅读19分钟

上篇太长导致写的时候卡的不行,这是下篇

上篇地址Spring6全篇学习笔记 - 掘金 (juejin.cn)

AOP

AOP场景模拟

我们尝试实现一个简单的计算器类

package com.atguigu.aop.calculator;

public interface calculator {
    int add(int i, int j);

    int sub(int i, int j);

    int mul(int i, int j);

    int div(int i, int j);
}

好的,我们现在希望他可以在输出结果的同时做一些简单的打印,让结果看起来更加明确,下面是它的实现类

package com.atguigu.aop.calculator;

public class CalculatorImpl implements calculator{
    @Override
    public int add(int i, int j) {

        int result = i + j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int sub(int i, int j) {

        int result = i - j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int mul(int i, int j) {

        int result = i * j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int div(int i, int j) {

        int result = i / j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

}

好的现在我们有新的需求了,我们希望给他加上日志功能。就像下面这样

@Override
    public int add(int i, int j) {
    
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
    
        int result = i + j;
    
        System.out.println("方法内部 result = " + result);
    
        System.out.println("[日志] add 方法结束了,结果是:" + result);
    
        return result;
    }

好的,我们完成了自己的设想。

此时我们发现一个问题,我们把不属于计算的其他逻辑一股脑写在了我们的原生方法内部。而这些新的功能本身与基本功能本身没有任何联系。 我们可不可以使用一种更好的方式, 将基本功能和附加功能解耦出来, ,此时我们用到新的代理模式

代理模式

二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。

proxy.png

  1. 静态代理
public class CalculatorImplProxy implements calculator{
    private CalculatorImpl calculator;

    public CalculatorImplProxy(CalculatorImpl calculator) {
        this.calculator = calculator;
    }

    @Override
    public int add(int i, int j) {
        // 附加功能由代理类中的代理方法来实现
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);

        int addResult = calculator.add(i, j);

        System.out.println("[日志] add 方法结束了,结果是:" + addResult);

        return addResult;
    }

根据上面的代码,我们如果以后需要带有日志功能的计算器,我们只需要调用该对象即可。该对象在执行方法时,会对原方法原封不动的执行,但同时加上自己的日志功能。


但它同时也暴露了问题,就是如果我们需要对别的类进行加强,比如给一个计算随机数的类用于用户的抽奖的类增加日志功能,是不是也需要写新的代理类。这样看来,他也并不是那么完美。我们提出新的设想

我们需要一个新的类,专门给日志功能用。只需要提供一个任意类,他就可以将日志功能加上去。

动态代理

先看代码

package com.atguigu.Proxy;


import com.atguigu.aop.calculator.CalculatorImpl;
import com.atguigu.aop.calculator.calculator;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class myProxy {
    private Object metaObj;

    public myProxy(Object metaObj) {
        this.metaObj = metaObj;
    }

    public Object getProxy() {
        return Proxy.newProxyInstance(metaObj.getClass().getClassLoader(), metaObj.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("日志哈哈哈哈~~~");
                Object invoke = method.invoke(metaObj, args);
                System.out.println("日志嘿嘿嘿~~~");
                return invoke;
            }
        });
    }
    
}

讲解一下代码做了什么事:

传进来一个任意对象,返回一个新的对象,这个新的对象实现了原对象的全部接口(但不再是由原对象的类创建的),对方法进行了改造,除执行原方法外,打印了日志哈哈哈~~~和日志嘿嘿嘿~~~

Proxy.newProxyInstance()是什么

java的一个代理对象类下的一个静态方法,需要传入如下三个参数。

  1. 类的classloader(); 它包含了类的一切信息
  2. 一个class对象数组 需要放入类的所有接口
  3. 一个继承了InvocationHandler类的对象 下面详细讲

在说InvocationHandler之前,先讲讲Proxy.newProxyInstance()做了什么吧。 他根据提供的classLoader和类的接口数组创建了一个新的类,新类的所有方法都不再执行,根据得到的信息创建InvocationHandler对象,并 执行其中的invoke()方法

InvocationHandler是什么?

一个接口,继承该接口需实现invoke()方法。

好了,我们上面知道了,新对象会执行我们InvocationHandler的invoke()方法,而不是它自己的方法。 这似乎与我们的初衷(执行原方法的同时加上我们的日志内容)背道而驰。

invoke()方法才是重点,这个接口也是为它而生的,让我们来看看invoke()方法。

  1. 参数 Object proxy 传入一个对象
  2. 参数 Method method 传入一个方法
  3. 参数 Object[] args 传入参数

它们准确的列出了一个方法执行时所需要的一切内容,哪个对象?哪个方法?传入哪些参数? 幸运的是我们不用自己填这些参数。 Proxy.newProxyInstance()在执行方法时,会把这些所需要的参数填满,并执行它,而不是自己的方法来看看上面的代码中我让他做了什么吧

System.out.println("日志哈哈哈哈~~~");
Object invoke = method.invoke(metaObj, args);
System.out.println("日志嘿嘿嘿~~~"); 
return invoke;

最后一个东西需要讲一下。method.invoke(metaObj, args); 我在这里传入了我们最初的那个需要被代理的对象是的,我们这里执行的方法是原代理对象的方法

总结

好的,我们完成了初衷,在执行原方法的同时加上新的日志功能

唯一要说的是代理方法Proxy.newProxyInstance()方法所提供的给我们的新对象已经不再是由我们最初提供的所在的类创建的对象了,虽然新的类拥有原对象类的所有接口,但已经是新的了。好的是它依旧可以被原对象所在类实现的接口接收。就像下面的代码:可以使用calculator接收而不能使用CalculatorImpl接收

@Test
public void t11(){
    myProxy myProxy = new myProxy(new CalculatorImpl());
    calculator proxy =  (calculator) myProxy.getProxy();
   
}

AOP概念

AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP的抽象概念(最蠢的地方)

  1. 横切关注点

意思就是你把一堆带有日志功能的类并排放在一起,把实现日志的代码部分横着连成一条线,组成这条线的点就叫横切关注点,就是功能代码本身

  1. 通知 就是功能本身,比如,日志,安全,事务
    1. 前置通知,方法执行前
    2. 返回通知,result返回之后
    3. 异常通知,报异常时直接通知
    4. 后置通知,方法结束时通知(报异常后也执行)
    5. 环绕通知,相当于invoke()方法

AOP.png

  1. 切面 通知类(比如上面的代码)

AOP2.png

  1. 目标 被代理的对象
  2. 连接点 可以被插入代码的地方
  3. 切入点 真的被插入代码的地方

AOP动态代理

AOP的动态代理分为jdk动态代理cglib动态代理。jdk动态代理上面已经讲的很明白了。cglib动态代理本质上就是通过继承原对象的类实现动态代理,用于应对需代理的对象没有实现接口的情况

AOP实操

AspectJ

AspectJ是一个年代久远的框架,因为其经典的动态代理方式,被Spring吸收使用。我们将使用AspectJ的注解来完成Spring的动态代理。


我们依旧采用对上一章中的计算器类来作为目标,进行面向切面编程

  1. 引入依赖
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>6.0.8</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>6.0.2</version>
</dependency>
  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"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:component-scan base-package="com.atguigu.AspectJ"/>
    <aop:aspectj-autoproxy/>
</beans>
  1. 将计算器类交给Spring管理
@Repository
public class CalculatorImpl implements calculator {
  1. 创建一个通知类,添加前置通知。
package com.atguigu.AspectJ.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class logger {

    @Before(value = "execution(public int com.atguigu.AspectJ.*.add(..))")
    public void BeforeHandler(){
        System.out.println("前置通知~~~");
    }
}

代码讲解

  1. @Aspect 声明当前类是一个通知类
  2. @Before 声明该方法作为前置通知

关于@Aspect注解中的表达式

AspectJ.png


关于各种通知的注解

   1. 前置通知,@Before
   2. 返回通知,@AfterReturning
   3. 异常通知,@AfterThrowing
   4. 后置通知,@After
   5. 环绕通知,@Around

关于各种通知的实操

package com.atguigu.AspectJ.aspect;

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

import java.lang.annotation.Retention;
import java.util.Arrays;

@Aspect
@Component
public class logger {

    @Before(value = "execution(public int com.atguigu.AspectJ.*.add(..))")
    public void BeforeHandler(JoinPoint joinPoint){
        System.out.println("前置通知~~~我可以获得切入点的方法与参数"+joinPoint.getSignature().getName()
                +"参数信息"+ Arrays.toString(joinPoint.getArgs()));
    }

    @AfterReturning(value = "execution(public int com.atguigu.AspectJ.*.add(..))",returning = "result")
    public void afterReturning(Object result){
        System.out.println("返回通知~~~我可以获得切入点的结果"+result);
    }
    @AfterThrowing(value = "execution(public int com.atguigu.AspectJ.*.add(..))",throwing = "ex")
    public void afterReturning(Throwable ex){
        System.out.println("异常通知~~~我在异常时执行,可以获得切入点的异常信息"+ex.getMessage())
    }

}

重点讲解一下参数。 在方法执行之前的方法可以获得参数与方法名,获取方法为填入参数Joinpoint。而在之后则可以获得返回值(异常通知只能获得异常信息),他们的获取方式略有不同,需要在切入点表达式中声明,而且要求声明时给定的名称与参数名必须保持一致


环绕通知的写法

@Around(value = "execution(public int com.atguigu.AspectJ.*.add(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) {
    Object result = null;
    
    try {
        System.out.println("环绕通知的前置,先于一切前置");
        result = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
        System.out.println("环绕通知的后置,后于一切后置通知");
    } catch (Throwable e) {
        System.out.println("环绕通知的异常");
        e.printStackTrace();
    } finally {
        System.out.println("环绕通知的finally,在一切的最后");
        return result;
    }
}

环绕通知写法非常特殊,它可以获得正在执行的进程相当于我们手写的代理类中的invoke()方法。它非常强大,可以一套完成所有通知

环绕通知的运行结果也出人意料,它先于所有其他通知,又后于所有通知

环绕通知的前置,先于一切前置
前置通知~~~我可以获得切入点的方法与参数add参数信息[1, 2]
方法内部 result = 3
返回通知~~~我可以获得切入点的结果3
环绕通知的后置,后于一切后置通知
环绕通知的finally,在一切的最后

一个小bug引发的思考

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
CalculatorImpl bean = context.getBean(CalculatorImpl.class);

这个代码本身是没有问题的,但是如果有切入点存在,它就会报错因为一旦使用了AOP的动态代理,因为内部是jdk方式实现的,IOC中就不再有以CalculatorImpl.class构建的对象了,而是代理对象,代码就会报错。只能使用它的接口calculator来接收

而我尝试让CalculatorImpl不再实现接口calculator,因为cglib方式实现动态代理,代码就可以运行了。

public class CalculatorImpl  {

将切入点表达式定义为变量

定义一个切入点表达式,并引用它。如果是在不同的类中,则需要填写全路径。

@Pointcut("execution(public int com.atguigu.AspectJ.*.add(..))")
public void pointCut01(){
    
}

@Around(value = "pointCut01()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) {

多个通知对应增强单个切入点的情况

pointCutOrder.png

那么很显然,我们的环绕通知的优先级要大于其他通知,数字也就小。

基于XML实现AOP

需要注意的点:

  1. 类的名称作为id时需要小写,xml的所有id都要小写
  2. 依然遵守变量名与表达式中指定名称一致的规则。
<aop:config>
    <!--配置切面类-->
    <aop:aspect ref="loggerAspect">
        <aop:pointcut id="pointCut" 
                   expression="execution(* com.atguigu.aop.xml.CalculatorImpl.*(..))"/>
        <aop:before method="beforeMethod" pointcut-ref="pointCut"></aop:before>
        <aop:after method="afterMethod" pointcut-ref="pointCut"></aop:after>
        <aop:after-returning method="afterReturningMethod" returning="result" pointcut-ref="pointCut"></aop:after-returning>
        <aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointCut"></aop:after-throwing>
        <aop:around method="aroundMethod" pointcut-ref="pointCut"></aop:around>
    </aop:aspect>
</aop:config>

Spring整合Junit5,Junit4完成快速测试

引入依赖

  1. Junit5
  2. Spring测试支持依赖
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>${junit.api.version}</version>
</dependency>

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

引入注解直接@AutoWired注入直接调用,无需getBean();

@SpringJUnitConfig(locations = "classpath:bean.xml")
public class t1 {

    @Autowired
    calculator calculator;
    @Test
    public void t12(){
       calculator.add(1,2);
    }
}

Junit4

  1. 把Junit版本换成4
<dependency>
 <groupId>junit</groupId>
 <artifactId>junit</artifactId>
 <version>4.12</version>
</dependency>
  1. 注解略有不同
@RunWith(SpringJUnit4ClassRunner.class) 
@ContextConfiguration("classpath:beans.xml")

3.必须使用@Test,且为import org.junit.Test;包下的注解

JdbcTemplate

Spring提供的数据库操作,对传统jdbc进行了封装。

1 基本配置

  1. 导入依赖
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>6.0.8</version>
<dependency>
</dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
  1. 我们使用Druid连接池技术配置数据源
jdbc.username=root
jdbc.password=密码
jdbc.url=jdbc:mysql://数据库地址/spring6?characterEncoding=utf8&useSSL=false
jdbc.driver=com.mysql.cj.jdbc.Driver

xml中依旧是老一套,context命名空间引入配置文件,配置Druid数据源。 一个小细节,com.alibaba.druid.pool.DruidDataSource中有两个name属性,一个是name,另一个是username,我们使用username,否则会导致连接创建失败

<bean id="druid" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
    <property name="driverClassName" value="${jdbc.driver}"/>
    <property name="url" value="${jdbc.url}"/>
</bean>
  1. 并把它整合进jdbcTemplate中,jdbcTemplate允许我们使用自己的连接池技术,在配置数据源时一起加进去。
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="druid"/>
</bean>

2 增删改查

2.1增删改

package com.atguigu.jdbc;


import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SpringJUnitConfig(locations = "classpath:jdbc-template.xml")
public class t1 {
    @Autowired
    JdbcTemplate jdbcTemplate;
    @Test
    public void t2(){
        String sql = "insert into student values (?,?,?)";
        int updated = jdbcTemplate.update(sql, 1003, "tom", 24);
        System.out.println(updated!=0?"插入成功":"插入失败");


        String updateSql = "update student set `name`=?,age=? where id=?";
        int updated2 = jdbcTemplate.update(updateSql,  "mary", 20,1003);
        System.out.println(updated2!=0?"修改成功":"修改失败");

        String deleteSql = "delete from student where id=?";
        int deleted = jdbcTemplate.update(deleteSql,1003);
        System.out.println(deleted!=0?"删除成功":"删除失败");
    }
}

2.2 查询操作

  1. 返回单个对象。
@Test
public void t3(){
    String sql = "select id,`name`,age from student where id=?";
    studentMapper student = jdbcTemplate.queryForObject(sql, (result, rowNum) -> {
        studentMapper studentMapper = new studentMapper();
        studentMapper.setName(result.getString("name"));
        studentMapper.setAge(result.getInt("age"));
        studentMapper.setId(result.getInt("id"));
        return studentMapper;
    }, 1002);
    System.out.println(student);
}

要求传个查询结果处理器,我们用lambda创建个匿名内部类,自己导入数据到mapper。也可以使用他提供的处理器

new BeanPropertyRowMapper(Mapper类),无需手动设置。

@Test
public void t3(){
    String sql = "select id,`name`,age from student where id=?";
    studentMapper student = jdbcTemplate.queryForObject(sql,new BeanPropertyRowMapper<>(studentMapper.class), 1002);
    System.out.println(student);
}
  1. 返回多个对象(list合集接收)
  2. 特别要注意,用的是query()而不是queryForList()
@Test
public void t4(){
    String sql = "select id,`name`,age from student";
    List<studentMapper> query = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(studentMapper.class));
    query.forEach(System.out::println);
}
  1. 返回单条数据
@Test
public void t5(){
    String sql = "select count(*) from student";
    Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
    System.out.println(count);
}

@Transactional 事务操作

提前准备的一些配置

我们希望通过买书这件事来演示事务操作。

  1. 用户admin去买书,首先admin把书给销售员,
  2. 销售员根据id查找书本的名字,获得书本价格,
  3. 扫码将库存-1,
  4. 然后admin付钱,钱包减去书本价格。

当然,第一步是先建表

  1. 创建书库存表
CREATE TABLE t_book(
	book_id INT(11) NOT NULL AUTO_INCREMENT UNIQUE KEY COMMENT '主键',
	book_name VARCHAR(32) DEFAULT NULL COMMENT '书名',
	book_price INT(11) UNSIGNED DEFAULT NULL  COMMENT '价格',
	stock INT(10) UNSIGNED DEFAULT	NULL COMMENT '库存'
	)ENGINE=INNODB DEFAULT CHARSET=utf8 AUTO_INCREMENT=3
  1. 放入两本书
INSERT  INTO `t_book`(`book_id`,`book_name`,`book_price`,`stock`) VALUES (1,'斗破苍穹',80,100),(2,'斗罗大陆',50,100);
  1. 创建用户表
CREATE TABLE `t_user` (
  `user_id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` VARCHAR(20) DEFAULT NULL COMMENT '用户名',
  `c` INT(10) UNSIGNED DEFAULT NULL COMMENT '余额(无符号)',
  PRIMARY KEY (`user_id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
  1. 添加用户admin 并设置500块
INSERT  INTO `t_user`(`user_id`,`username`,`balance`) VALUES (1,'admin',500);

把上面现实中的操作变成代码,4个方法

  1. admin给出书本id和自己的id
  2. 根据书本id查找价格
  3. 根据书本id减去库存
  4. 根据查出的书本价格与给出的用户id修改用户余额

代码实现如下

Controller层

@Controller
public class buyBookController {
    @Autowired
    private bookService bookService;

    public void buyBook(Integer userId,Integer bookId){
        bookService.Buybook(userId,bookId);
    }
}

Service层

@Service
public class bookServiceImpl implements bookService{
    @Autowired
    private bookDao bookDao;
    @Override
    public void Buybook(Integer userId, Integer bookId) {
        Integer price = bookDao.getPriceById(bookId);
        bookDao.updateStock(bookId);
        bookDao.updateBalance(userId,price);
    }
}

DAO层

@Repository
public class bookDaoImpl implements bookDao {
    Logger logger = LoggerFactory.getLogger(bookDaoImpl.class);
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Override
    public Integer getPriceById(Integer bookId) {
        String getPriceSql = "select book_price from t_book where book_id=?";
        Integer bookPrice = jdbcTemplate.queryForObject(getPriceSql, Integer.class, bookId);
        return bookPrice;
    }

    @Override
    public void updateStock(Integer bookId) {
        String updateStock = "update t_book set stock=stock-1 where book_id=?";
        int updated = jdbcTemplate.update(updateStock, bookId);
        logger.info(updated>0?"库存修改成功!":"库存修改失败!");
    }

    @Override
    public void updateBalance(Integer userId, Integer price) {
        String updateBalance = "update t_user set balance=balance-? where user_id=?";
        int updatedBalance = jdbcTemplate.update(updateBalance, price, userId);
        logger.info(updatedBalance>0?"余额修改成功!":"余额修改失败!");
    }
}

Spring事务

好的,根据上面的代码,我们可以实现我们设想的操作,但是要知道,在网络中,一切具有不确定性,并不如现实那么可靠。我们假设:admin此时余额已经不足,在代码层面无非就是报错,但是放在现实中,admin获得了一张无限透支的银行卡,这会造成很大的问题。

我们需要使用@Transactional注解来保证事务出错时会回滚!


  1. @Transactional注解依赖于一个名为tx的命名空间,我们开启它
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">
  1. 事务必须指定基于哪个数据源。将它放入org.springframework.jdbc.datasource.DataSourceTransactionManager类dataSource属性中(id可以随便起)
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="druid"/>
</bean>
  1. 还需要开启注解驱动
<tx:annotation-driven  transaction-manager="transactionManager"/>

@Transactional实操

好的,我们现在只需要将@Transactional加在指定的类或者方法上,就可以实现失败回滚了。

  1. 如果加在方法上,则表示该方法支持事务
  2. 如果加在类上,则表示该类的所有方法都被放入同一事务。
@Transactional
@Service
public class bookServiceImpl implements bookService{

@Transactional中的参数

我们从重要的到不重要的依次讲解


1. 本章重点: 事务的传递 propagation

该参数用于解决一个事务中调用了另一个事务的情况

  1. Propagation.REQUIRED

他表示始终使用单个事务,其中任何的环节出错,就会造成回滚

  1. Propagation.NEW_REQUIRED

他表示每个事务都是独立的,单个事务的失败只会造成当前事务管辖范围内的回滚


我们通过买多本书这件事来举例,在这种事情里,其实我们并不希望如果admin的钱不够买多本书,就导致一本都不买而在扣钱方面,我们则希望如果钱不够,则回滚,不造成库存损失与超额扣费

2 本章次要 timeout (单位 秒)

表示事务在多少秒内没有完成则回滚

@Transactional(timeout = 3)
@Service
public class bookServiceImpl implements bookService{
    @Autowired
    private bookDao bookDao;
    @Override
    public void Buybook(Integer userId, Integer bookId) {
        try {
        //线程睡眠5秒,手动超时
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

2 本章次要 noRollbackFor 指定某些异常不会滚

@Transactional(noRollbackFor = ArithmeticException.class)
@Service
public class bookServiceImpl implements bookService{
    @Autowired
    private bookDao bookDao;
    @Override
    public void Buybook(Integer userId, Integer bookId) {
    //手写除0异常
        int err =  1/0;

2 本章次要 isolation 隔离级别

数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

隔离级别一共有四种:

读未提交:READ UNCOMMITTED

允许Transaction01读取Transaction02未提交的修改。

读已提交:READ COMMITTED

要求Transaction01只能读取Transaction02已提交的修改。

可重复读:REPEATABLE READ

确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。

串行化:SERIALIZABLE

确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。

isolation01.png

isolation02.png

选项如下:

@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化

事务全注解开发

package com.atguigu.transaction.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableTransactionManagement
@ComponentScan(basePackages = "com.atguigu.transaction")
@Configuration
@Component
public class mySpringConfig {

    @Bean
    public DruidDataSource druidDataSource(){
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("密码");
        druidDataSource.setUrl("jdbc:mysql://数据库地址/spring6?characterEncoding=utf8&useSSL=false");
        druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return druidDataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DruidDataSource druidDataSource){
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(druidDataSource);
        return jdbcTemplate;
    }

    @Bean()
    public DataSourceTransactionManager dataSourceTransactionManager(DruidDataSource druidDataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(druidDataSource);
        return dataSourceTransactionManager;
    }
}

让我们看看这些代码都做了什么,先看注解 1.放入Spring 2. 声明是配置类 3. 开启注解扫描 4. 开启事务注解

@Bean 是什么?做了什么?

@Bean下面的方法返回了一个对象,放入Spring,唯一索引是方法的返回类型的小写。而在最后一个@Bean中,我手动声明了唯一索引的名字。


测试代码:

@Test
public void t11() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(mySpringConfig.class);
    buyBookController buyBookController = context.getBean(buyBookController.class);
    buyBookController.buyBook(1,1);
}

Spring事务全注解开发

  1. 无需再开启注解驱动
<tx:advice id="transactionInterceptor" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="get*" read-only="true"/>
    </tx:attributes>
</tx:advice>
  1. advice 标签,指定我们写好的事务管理器
  2. advice 设置规则
  3. tx:method name="get*" read-only="true" 表示对所有get开头的方法生效。 只能执行读操作。

那我们如何把他放到我们指定的地方呢?

首先我们需要AOP命名空间,导入后代码如下, 创建一个配置,指定切入点,然后将事务处理器引入,并对切入点表达式指定的类或方法进行管理

<aop:config>
    <aop:pointcut id="bookService" expression="execution(* com.atguigu.transaction.Service.*.*(..))"/>
    <aop:advisor advice-ref="transactionInterceptor" pointcut-ref="bookService"/>
</aop:config>

Resource 资源处理类

Resource是Spring提供的一个接口,内部给出了很多他的实现类

目标是为了简化Java对低级资源的访问


Resource的3个常用实现类

  1. UrlResource()实现类

    1. 可以传入Http链接,获得资源信息,包括数据流。
    2. 可以传入file链接,获得资源信息,包括数据流。
    public static void main(String[] args) {
        UrlResource urlResource = null;
        try {
            urlResource = new UrlResource("file:d:/t1.java");
            int len = 0;
            InputStreamReader ipt = new     InputStreamReader(urlResource.getInputStream(), "utf-8");
            while ((len = ipt.read())!=-1){
                System.out.print((char) len);
            }
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }


  1. ClassPathResource()实现类

顾名思义,获得类路径下资源


  1. FileSystemResource()实现类

顾名思义,获得系统资源

ResourceLoader接口

该接口提供一个方法,getResource()方法,返回一个Resource对象。 在Spring内部,如果需要一个资源,会由他提供。它会根据情况来决定该资源使用ClassPathResource获取还是FileSystemResource获取。


我们常用的ApplicationContext也实现了该接口。这依旧意味着我们可以直接使用它的getResource()方法获取资源。

ApplicationContext context = new AnnotationConfigApplicationContext(mySpringConfig.class);
Resource resource = context.getResource("http://www.baidu.com");

ResourceLoaderAware接口

如果我们将这个接口实现,并把他交给Spring管理,该接口的方法会提供一个资源加载器ResourceLoader(),那么当我们创建对象,并使用该方法时,会发现,它提供给我们的ResourceLoader(资源加载器),就是Spring容器本身

重点 将Resource与代码解耦

package com.atguigu.transaction.MyResourceLoader;

import org.springframework.core.io.Resource;

import java.lang.reflect.Proxy;

public class myResourceLoader {
    private Resource resource;

    public Resource getResource() {
        return resource;
    }

    public void setResource(Resource resource) {
        this.resource = resource;
    }
}
<bean id="myResourceLoader" class="com.atguigu.transaction.MyResourceLoader.myResourceLoader">
    <property name="resource" value="classpath:a.txt"/>
</bean>

最后正常getBean即可。

重点 Spring加载多个资源文件

使用*通配符来将多个xml文件加载进Spring容器中

ApplicationContext context = new ClassPathXmlApplicationContext("classpath:jdbc*.xml");
myResourceLoader myResourceLoader = context.getBean("myResourceLoader", myResourceLoader.class);
try {
    System.out.println(myResourceLoader.getResource().getInputStream().read());
} catch (IOException e) {
    throw new RuntimeException(e);
}