2022尚硅谷SSM框架跟学(五)Spring基础二

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

2022尚硅谷SSM框架跟学 五Spring基础二

3.AOP

3.1场景模拟

新建Module

Name:spring-proxy
GroupId:com.atguigu.spring

设置打包方式

    <packaging>jar</packaging>

配置pom.xml加入junit依赖

 <dependencies>
        <!-- junit测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

3.1.1声明接口

声明计算器接口Calculator包含加减乘除的抽象方法
创建接口com.atguigu.spring.proxy.Calculator
Calculator.java

package com.atguigu.spring.proxy;

/**
 * @InterfaceName: Calculator
 * @Description:
 * @Author: wty
 * @Date: 2023/1/11
 */

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

3.1.2创建实现类

实现类
创建接口的实现类com.atguigu.spring.proxy.CalculatorImpl

package com.atguigu.spring.proxy;

/**
 * @ClassName: CalculatorImpl
 * @Description:
 * @Author: wty
 * @Date: 2023/1/11
 */

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;
    }
}

3.1.3创建带日志功能的实现类

实现类
修改类CalculatorImpl.java添加日志记录

package com.atguigu.spring.proxy;

/**
 * @ClassName: CalculatorImpl
 * @Description:
 * @Author: wty
 * @Date: 2023/1/11
 */

public class CalculatorImpl implements Calculator {
    @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;
    }

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

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

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

3.1.4提出问题

(1)现有代码缺陷

针对带日志功能的实现类我们发现有如下缺陷

  • 对核心业务功能有干扰导致程序员在开发核心业务功能时分散了精力
  • 附加功能分散在各个业务功能方法中不利于统一维护
(2)解决思路

解决这两个问题核心就是解耦。我们需要把附加功能从业务功能代码中抽取出来。

(3)困难

解决问题的困难要抽取的代码在方法内部靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。

3.2代理模式

3.2.1概念

(1)介绍

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

(2)生活中的代理

  • 广告商找大明星拍广告需要经过经纪人
  • 合作伙伴找大老板谈合作要约见面时间需要经过秘书
  • 房产中介是买卖双方的代理

(3)相关术语

  • 代理将非核心逻辑剥离出来以后封装这些非核心逻辑的类、对象、方法。
  • 目标被代理“套用”了非核心逻辑代码的类、对象、方法。

3.2.2静态代理

创建静态代理类CalculatorStaticProxy.java

package com.atguigu.spring.proxy;

/**
 * @ClassName: CalculatorStaticProxy
 * @Description:
 * @Author: wty
 * @Date: 2023/1/11
 */

public class CalculatorStaticProxy implements Calculator {
    private Calculator target;

    public CalculatorStaticProxy() {
    }

    public CalculatorStaticProxy(Calculator calculator) {
        this.target = calculator;
    }

    public Calculator getCalculator() {
        return target;
    }

    public void setCalculator(Calculator calculator) {
        this.target = calculator;
    }

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

    }

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

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

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

修改CalculatorImpl.java

package com.atguigu.spring.proxy;

/**
 * @ClassName: CalculatorImpl
 * @Description:
 * @Author: wty
 * @Date: 2023/1/11
 */

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;
    }
}

创建测试类com.atguigu.spring.proxy.ProxyTest

public class ProxyTest {
    @Test
    public void test() {
        Calculator calculator = new CalculatorImpl();
        CalculatorStaticProxy proxy = new CalculatorStaticProxy(calculator);
        proxy.add(1, 2);
    }
}

执行结果
静态代理
这里要明白静态代理不仅仅只是在目标方法前和后进行非核心方法的调用应该分为以下4种
静态代理通知

静态代理确实实现了解耦但是由于代码都写死了完全不具备任何的灵活性。就拿日志功能来说将来其他地方也需要附加日志那还得再声明更多个静态代理类那就产生了大量重复的代码日志功能还是分散的没有统一管理。
提出进一步的需求将日志功能集中到一个代理类中将来有任何日志需求都通过这一个代理类来实现。这就需要使用动态代理技术了。

3.2.3动态代理

动态代理

生产代理对象的工厂类com.atguigu.spring.proxy.ProxyFactory

package com.atguigu.spring.proxy;

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

/**
 * @ClassName: ProxyFactory
 * @Description:
 * @Author: wty
 * @Date: 2023/1/11
 */

public class ProxyFactory {
    // 目标对象
    private Object target;

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

    public Object getProxy() {
        // JDK动态代理
        /**
         * newProxyInstance 的源码
         * newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
         *
         * ClassLoader loader 指定加载动态生成的代理类的类加载器
         *
         *Class<?>[] interfaces 获取目标对象实现的所有的接口的class对象的数组
         *
         * InvocationHandler 执行处理设置代理类中的抽象方法该如何重写
         */
        ClassLoader classLoader = this.getClass().getClassLoader();
        Class<?>[] interfaces = target.getClass().getInterfaces();
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            /**
             * @description //TODO
             *
             * @param
             * @param: proxy 表示代理对象
             * @param: method 表示要执行的方法
             * @param: args 表示要执行的方法的参数列表
             * @return java.lang.Object
             * @date 2023/1/11 12:50
             * @author wty
             **/
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("[日志] " + method.getName() + " 方法开始了参数是" + Arrays.toString(args));
                Object result = method.invoke(target, args);
                System.out.println("[日志] " + method.getName() + " 方法结束了结果是" + result);

                return result;
            }
        };

        return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
    }
}

修改测试类ProxyTest.java新增一个方法

// 动态代理
    @Test
    public void test2() {
        Calculator calculator = new CalculatorImpl();

        ProxyFactory proxyFactory = new ProxyFactory(calculator);

        Object o = proxyFactory.getProxy();

        // 通过向下转型
        Calculator proxy = (Calculator) o;

        proxy.add(1, 2);

    }

执行测试类ProxyTest.java
执行测试类

3.2.4测试

修改ProxyFactory.java添加try和catry、finally代码块

    public Object getProxy() {
        // JDK动态代理
        /**
         * newProxyInstance 的源码
         * newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
         *
         * ClassLoader loader 指定加载动态生成的代理类的类加载器
         *
         *Class<?>[] interfaces 获取目标对象实现的所有的接口的class对象的数组
         *
         * InvocationHandler 执行处理设置代理类中的抽象方法该如何重写
         */
        ClassLoader classLoader = this.getClass().getClassLoader();
        Class<?>[] interfaces = target.getClass().getInterfaces();
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            /**
             * @description //TODO
             *
             * @param
             * @param: proxy 表示代理对象
             * @param: method 表示要执行的方法
             * @param: args 表示要执行的方法的参数列表
             * @return java.lang.Object
             * @date 2023/1/11 12:50
             * @author wty
             **/
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object result = null;
                try {
                    System.out.println("[日志] " + method.getName() + " 方法开始了参数是" + Arrays.toString(args));
                    result = method.invoke(target, args);
                    System.out.println("[日志] " + method.getName() + " 方法结束了结果是" + result);
                } catch (Exception e) {
                    e.printStackTrace();
                    System.out.println("[日志] " + method.getName() + " 方法结束了异常" + e);
                } finally {
                    System.out.println("[日志] " + method.getName() + " 方法执行完毕");
                }

                return result;
            }
        };

        return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
    }

执行结果
执行结果

那如果抛出异常会如何呢我们来试一下
修改测试类ProxyTest.java调用除法把除数写成0

 // 动态代理
    @Test
    public void test2() {
        Calculator calculator = new CalculatorImpl();

        ProxyFactory proxyFactory = new ProxyFactory(calculator);

        Object o = proxyFactory.getProxy();

        // 通过向下转型
        Calculator proxy = (Calculator) o;

        //proxy.add(1, 2);
        proxy.div(1, 0);
    }

直接运行测试类
直接运行的测试类
发现执行了catch里面的打印语句

总结 动态代理有2种

  • 1.jdk动态代理要求必须有接口最终生成的代理类在com.sun.proxy包下类名为${proxy2}。
  • 2.cglib动态代理,最终生成的代理类会继承目标类并且和目标类在相同的包下。

3.3AOP概念及相关术语

3.3.1概述

AOPAspect Oriented Programming是一种设计思想是软件设计领域中的面向切面编程它是面向对象编程的一种补充和完善它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。

3.3.2相关术语

(1)横切关注点

从每个方法中抽取出来的同一类非核心业务。在同一个项目中我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
这个概念不是语法层面天然存在的而是根据附加功能的逻辑上的需要有十个附加功能就有十个横切关注点。
横切关注点

(2)通知

每一个横切关注点上要做的事情都需要写一个方法来实现这样的方法就叫通知方法

  • 前置通知在被代理的目标方法前执行
  • 返回通知在被代理的目标方法成功结束后执行寿终正寝
  • 异常通知在被代理的目标方法异常结束后执行死于非命
  • 后置通知在被代理的目标方法最终结束后执行盖棺定论
  • 环绕通知使用try…catch…finally结构围绕整个被代理的目标方法包括上面四种通知对应的所有位置。

通知类型

(3)切面

封装通知方法(横切关注点)的类。
切面

(4)目标

被代理的目标对象。

(5)代理

向目标对象应用通知之后创建的代理对象。

(6)连接点

这也是一个纯逻辑概念不是语法定义的。
把方法排成一排每一个横切位置看成x轴方向把方法从上到下执行的顺序看成y轴x轴和y轴的交叉点就是连接点。
连接点

(7)切入点

定位连接点的方式。
每个类的方法中都包含多个连接点所以连接点是类中客观存在的事物从逻辑上来说。
如果把连接点看作数据库中的记录那么切入点就是查询记录的 SQL 语句。
Spring 的 AOP 技术可以通过切入点定位到特定的连接点。
切点通过 org.springframework.aop.Pointcut 接口进行描述它使用类和方法作为连接点的查询条件。

3.3.3作用

  • 简化代码把方法中固定位置的重复的代码抽取出来让被抽取的方法更专注于自己的核心功能提高内聚性。
  • 代码增强把特定的功能封装到切面类中看哪里有需要就往上套被套用了切面逻辑的方法就被切面给增强了。

3.4基于注解的AOP

3.4.1技术说明

AOP是面向切面编程的思想而AspectJ是面向切面编程思想的实现。
基于注解的AOP
动态代理InvocationHandler

  • JDK原生的实现方式需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口兄弟两个拜把子模式。
  • cglib通过继承被代理的目标类认干爹模式实现代理所以不需要目标类实现接口
  • AspectJ本质上是静态代理将代理逻辑“织入”被代理的目标类编译得到的字节码文件所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。

3.4.2准备工作

创建新的Module

Namespring-aop
GroupID:com.atguigu.spring

创建新的Module
在pom.xml中添加打包方式

<packaging>jar</packaging>
(1)添加依赖

在pom.xml中添加依赖

    <dependencies>
        <!-- 基于Maven依赖传递性导入spring-context依赖即可导入当前所需所有jar包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <!--spring上下文 -->
            <artifactId>spring-context</artifactId>
            <version>5.3.1</version>
        </dependency>
        <!-- junit测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- spring-aspects会帮我们传递过来aspectjweaver -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.3.1</version>
        </dependency>
    </dependencies>

查看依赖列表

(2)准备被代理的目标资源

拷贝

接口

package com.atguigu.spring.aop.annotation;

/**
 * @InterfaceName: Calculator
 * @Description:
 * @Author: wty
 * @Date: 2023/1/11
 */

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.spring.aop.annotation;

/**
 * @ClassName: CalculatorImpl
 * @Description:
 * @Author: wty
 * @Date: 2023/1/11
 */

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;
    }
}

3.4.3创建切面类并配置

创建切面类com.atguigu.spring.aop.annotation.LogerAspect
添加注解@Component(因为其它都用这个表示)和切面注解@Aspect

package com.atguigu.spring.aop.annotation;

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

/**
 * @ClassName: LogerAspect
 * @Description:
 * @Author: wty
 * @Date: 2023/1/12
 */
@Component
@Aspect
public class LogerAspect {
}

新建配置文件aop-annotation.xml
在Spring的配置文件中配置

    <!--
        切面类和目标类都需要交给IOC容器,这里是通过注解的方式
        切面类必须通过@Aspect注解标识为一个切面这里是LogerAspect.java
        在spring配置文件中设置aop:aspectj-autoproxy/这里是当前文件aop-annotation.xml
     -->
    <!-- 这里配置扫描自动装配 -->
    <context:component-scan base-package="com.atguigu.spring.aop.annotation"></context:component-scan>

    <!-- aop配置:开启基于注解的AOP -->
    <aop:aspectj-autoproxy/>

    <!-- aop配置:开启基于注解的AOP -->
    <aop:aspectj-autoproxy/>

修改CalculatorImpl.java添加注解@Component
添加注解
以前置通知为例来标记一下注解
修改LogerAspect.java

package com.atguigu.spring.aop.annotation;

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

/**
 * @ClassName: LogerAspect
 * @Description: 在切面中。需要通过指定的注解将方法标识为通知方法
 * Before:前置通知在目标对象方法执行之前执行
 * @Author: wty
 * @Date: 2023/1/12
 */
@Component
// 将当前组件标识为切面
@Aspect
public class LogerAspect {
    @Before("execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))")
    public void beforeAdviceMethod() {
        System.out.println("前置通知");
    }
}

创建测试类com.atguigu.spring.aop.AOPTest
添加测试类我们先尝试一下getBean()里面放目标类的class

   @Test
    public void testAOPByAnnotation() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
        CalculatorImpl calculator = ioc.getBean(CalculatorImpl.class);
        calculator.add(1, 3);
    }

运行测试类后发现抛出异常NoSuchBeanDefinitionException
抛出异常
很自然的想到AOP的AspectJ是采用了静态代理的模式通过代理类间接调用目标类这里继续修改AOPTest.java

    @Test
    public void testAOPByAnnotation() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
        Calculator calculator = ioc.getBean(Calculator.class);
        calculator.add(1, 3);
    }

我们发现其实代理类我们也不知道是啥是由系统自动装配的但是我们知道其接口是Calculator并且我们知道代理类也一定实现了Calculator那么我们就在getBean()中写入接口类。
执行测试类查看结果
查看结果

3.4.4各种通知

  • 前置通知使用@Before注解标识在被代理的目标方法前执行
  • 返回通知使用@AfterReturning注解标识在被代理的目标方法成功结束后执行寿终正寝
  • 异常通知使用@AfterThrowing注解标识在被代理的目标方法异常结束后执行死于非命
  • 后置通知使用@After注解标识在被代理的目标方法最终结束后执行盖棺定论
  • 环绕通知使用@Around注解标识使用try…catch…finally结构围绕整个被代理的目标方法包括上面四种通知对应的所有位置。

当前项目的spring版本是5.3.1遵循Spring版本5.3.x以后的通知顺序
spring版本
各种通知的执行顺序

  • Spring版本5.3.x以前(我们使用的spring版本是5.3.1)
    ⨀ \bigodot 前置通知
    ⨀ \bigodot 目标操作
    ⨀ \bigodot 后置通知
    ⨀ \bigodot 返回通知或异常通知
  • Spring版本5.3.x以后
    ⨀ \bigodot 前置通知
    ⨀ \bigodot 目标操作
    ⨀ \bigodot 返回通知或异常通知
    ⨀ \bigodot 后置通知

3.4.5切入点表达式语法

(1)作用

作用

(2)语法细节
  • 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
  • 在包名的部分一个“”号只能代表包的层次结构中的一层表示这一层是任意的。
    ⨀ \bigodot 例如
    .Hello匹配com.Hello不匹配com.atguigu.Hello
  • 在包名的部分使用“*…”表示包名任意、包的层次深度任意
  • 在类名的部分类名部分整体用*号代替表示类名任意
  • 在类名的部分可以使用*号代替类名的一部分
    ⨀ \bigodot 例如*Service匹配所有名称以Service结尾的类或接口
  • 在方法名部分可以使用*号表示方法名任意
  • 在方法名部分可以使用*号代替方法名的一部分
    ⨀ \bigodot 例如*Operation匹配所有方法名以Operation结尾的方法
  • 在方法参数列表部分使用(…)表示参数列表任意
  • 在方法参数列表部分使用(int,…)表示参数列表以一个int类型的参数开头
  • 在方法参数列表部分基本数据类型和对应的包装类型是不一样的
    切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
  • 在方法返回值部分如果想要明确指定一个返回值类型那么必须同时写明权限修饰符
    ⨀ \bigodot 例如execution(public int …Service.(…, int)) 正确
    ⨀ \bigodot 例如execution(
    int …Service.*(…, int)) 错误

方法返回值

前置通知

切入点表达式总结
设置位置:需要设置在标识通知的注解的value属性中。
例如

@Before("execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))")

可以简写为

@Before("execution(*com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))")

其中
第一个 * 表示任意的访问修饰符和返回值类型
第二个 * 表示当前类中的任意方法
… 表示任意的参数列表
类的地方也可以使用*表示包下所有的类例如

@Before("execution(*com.atguigu.spring.aop.annotation.*.*(..))")

包的地方也可以使用*表示当前包下的所有的子包例如

@Before("execution(*com.atguigu.spring.aop.*.*.*(..))")

修改完后我们测试一下是否目标类其它方法可以调用
我们给测试类AOPTest.java中添加一下目标类的其它方法比如sub()方法看一下
添加新方法
发现运行结果
运行结果
那如何获取到切入点的通知方法的方法名和参数名呢下面我们来修改一下LogerAspect.java

获取连接点的信息
在通知方法的参数位置设置JoinPoint类型的参数就可以获取连接点对应的通知方法的信息。
比如
joinPoint.getSignature()获取连接点通知方法的签名信息
joinPoint.getArgs()获取连接点通知方法的参数信息

修改类

@Component
// 将当前组件标识为切面
@Aspect
public class LogerAspect {
    //@Before("execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))")
    @Before("execution (* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))")
    public void beforeAdviceMethod(JoinPoint joinPoint) {
        // 获取连接点对应方法的方法名
        Signature signature = joinPoint.getSignature();
        System.out.println("连接点对应方法的方法名是" + signature);
        // 获取连接点对应方法的参数
        Object[] args = joinPoint.getArgs();
        System.out.println("连接点对应方法的参数是" + Arrays.toString(args));

        System.out.println("前置通知");
    }
}

之后运行测试类AOPTest.java查看结果
查看结果
紧接着我们来看一下后置通知那就需要用到切入点表达式的重用了。

3.4.6重用切入点表达式

@Pointcut声明一个公共的切入点表达式
声明完之后使用
@通知类型(“方法名称”)

(1)声明

修改LogerAspect.java修改注解中的内容

    @Pointcut("execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))")
    public void pointCut() {}
(2)在同一个切面中使用
后置通知

在LogerAspect.java中添加方法

    @After("pointCut()")
    public void afterAdviceMethod() {
        System.out.println("后置通知");
    }

调用方法
运行测试类AOPTest.java
查看结果
查看结果
这里针对后置通知我们不禁会疑问后置通知的位置在哪儿是finally子句还是方法体返回后执行下面我们来验证一下。
修改测试类AOPTest.java调用除法的方式

    @Test
    public void testAOPByAnnotation() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
        Calculator calculator = ioc.getBean(Calculator.class);
        // calculator.add(1, 3);
        // calculator.sub(1, 3);
        calculator.div(1, 0);
    }

输出结果
结果
得出结论:@After是在目标类方法的finally中执行。

为了看出通知方法的详细信息我们继续修改LogerAspect.java

    @After("pointCut()")
    public void afterAdviceMethod(JoinPoint joinPoint) {
        // 获取连接点对应方法的方法名
        Signature signature = joinPoint.getSignature();
        System.out.println("连接点对应方法的方法名是" + signature);

        // 获取连接点对应方法的参数
        Object[] args = joinPoint.getArgs();
        System.out.println("连接点对应方法的参数是" + Arrays.toString(args));

        System.out.println("后置通知");
    }

运行测试类查看结果
结果
接着我们看返回通知

返回通知

修改LogerAspect.java的方法

    @AfterReturning("pointCut()")
    public void afterReturningAdviceMethod() {
        System.out.println("返回通知");
    }

直接运行测试类AOPTest.java查看结果
结果
可以看出来抛出异常后不会执行返回通知的内容。
那我们执行一次正确的。
修改AOPTest.java

    @Test
    public void testAOPByAnnotation() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
        Calculator calculator = ioc.getBean(Calculator.class);
        // calculator.add(1, 3);
        // calculator.sub(1, 3);
        calculator.div(1, 1);
    }

结果
作为返回通知目标类的方法已经产生了结果那我们如何获取方法的返回值呢下面我们修改一下LogerAspect.java
修改
代码如下

    @AfterReturning(value = "pointCut()", returning = "result")
    public void afterReturningAdviceMethod(JoinPoint joinPoint, Object result) {
        Signature signature = joinPoint.getSignature();
        System.out.println("连接点对应方法的方法名是" + signature);
        System.out.println("返回通知");
        System.out.println("目标对象的返回值:" + result);
    }

运行测试类AOPTest.java
测试类
获取到返回值。

最后看一下异常(例外)通知。

异常(例外)通知

修改类LogerAspect.java

    @AfterThrowing("pointCut()")
    public void afterThrowingAdviceMethod(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        System.out.println("连接点对应方法的方法名是" + signature.getName());
        System.out.println("异常通知");
    }

异常通知在编译器中有闪电图标
异常通知
修改测试类AOPTest.java

    @Test
    public void testAOPByAnnotation() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
        Calculator calculator = ioc.getBean(Calculator.class);
        calculator.div(10, 0);
    }

返回结果
结果
既然是异常通知这里我们想返回异常的信息
修改LogerAspect.java

   @AfterThrowing(value = "pointCut()", throwing = "e")
    public void afterThrowingAdviceMethod(JoinPoint joinPoint, Throwable e) {
        Signature signature = joinPoint.getSignature();
        System.out.println("连接点对应方法的方法名是" + signature.getName());
        System.out.println("异常通知");
        System.out.println("异常信息是:" + e);
    }

这里用Throwable e或者Exception e 都可以。
执行测试类AOPTest.java查看结果。
异常通知的信息

总结 在异常通知中若要获取目标对象方法的异常
只需要通过@AfterThrowing注解的throwing属性
就可以将通知方法的某个参数指定为接收目标对象方法出现的异常的参数。

(3)在不同切面中使用

创建类存放另一个验证切面com.atguigu.spring.aop.annotation.ValidateAspect

package com.atguigu.spring.aop.annotation;

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

/**
 * @ClassName: ValidateAspect
 * @Description:计算器加减乘除的验证
 * @Author: wty
 * @Date: 2023/1/13
 */
@Component
@Aspect
public class ValidateAspect {
    //@Before("execution(* com.atguigu.spring.aop.annotation.Calculator.*(..))")
    @Before("com.atguigu.spring.aop.annotation.LogerAspect.pointCut()")
    public void BeforeAdviceMethod(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        System.out.println("ValidateAspect通知方法:" + signature.getName() + "前置通知");
    }
}

修改之前的切面LogerAspect.java将输出语句加上切面类的类名

    //@Before("execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))")
    @Before("execution (* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))")
    public void beforeAdviceMethod(JoinPoint joinPoint) {
        // 获取连接点对应方法的方法名
        Signature signature = joinPoint.getSignature();
        System.out.println("LogerAspect连接点对应方法的方法名是" + signature.getName());
        // 获取连接点对应方法的参数
        Object[] args = joinPoint.getArgs();
        System.out.println("LogerAspect连接点对应方法的参数是" + Arrays.toString(args));

        System.out.println("LogerAspect前置通知");
    }

执行测试类AOPTest.java

 @Test
    public void testAOPByAnnotation() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
        Calculator calculator = ioc.getBean(Calculator.class);
        // calculator.add(1, 3);
        // calculator.sub(1, 3);
        calculator.div(10, 1);
    }

执行结果
执行结果
那切面的执行顺序是怎样 的呢见下面。

3.4.7获取通知的相关信息

(1)获取连接点信息

获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参
例如:

    @Before("execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))")
    public void beforeAdviceMethod(JoinPoint joinPoint) {
        // 获取连接点对应方法的方法名
        Signature signature = joinPoint.getSignature();
        System.out.println("连接点对应方法的方法名是" + signature.getName());
        // 获取连接点对应方法的参数
        Object[] args = joinPoint.getArgs();
        System.out.println("连接点对应方法的参数是" + Arrays.toString(args));

        System.out.println("前置通知");
    }
(2)获取目标方法的返回值

@AfterReturning中的属性returning用来将通知方法的某个形参接收目标方法的返回值例如

    @AfterReturning(value = "pointCut()", returning = "result")
    public void afterReturningAdviceMethod(JoinPoint joinPoint, Object result) {
        Signature signature = joinPoint.getSignature();
        System.out.println("连接点对应方法的方法名是" + signature.getName());
        System.out.println("返回通知");
        System.out.println("目标对象的返回值:" + result);
    }
(3)获取目标方法的异常

@AfterThrowing中的属性throwing用来将通知方法的某个形参接收目标方法的异常例如

@AfterThrowing(value = "pointCut()", throwing = "e")
    public void afterThrowingAdviceMethod(JoinPoint joinPoint, Throwable e) {
        Signature signature = joinPoint.getSignature();
        System.out.println("连接点对应方法的方法名是" + signature.getName());
        System.out.println("异常通知");
        System.out.println("异常信息是:" + e);
    }

3.4.8环绕通知

在LogerAspect.java中添加方法

 @Around("pointCut()")
    public Object aroundAdviceMethod(ProceedingJoinPoint joinPoint) {
        Object result = null;
        // 表示目标对象方法的执行
        try {
            System.out.println("环绕通知 → 前置通知");
            result = joinPoint.proceed();
            System.out.println("环绕通知 → 返回通知");

        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println("环绕通知 → 异常通知");

        } finally {
            System.out.println("环绕通知 → 后置通知");
        }
        return result;
    }

在测试类AOPTest.java中添加方法

    @Test
    public void testAOPByAnnotation() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
        Calculator calculator = ioc.getBean(Calculator.class);
        // calculator.add(1, 3);
        // calculator.sub(1, 3);
        calculator.div(10, 1);
    }

查看结果
环绕通知
修改AOPTest.java看一下异常的结果

    @Test
    public void testAOPByAnnotation() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
        Calculator calculator = ioc.getBean(Calculator.class);
        calculator.div(10, 0);
    }

返回结果

3.4.9切面的优先级

相同目标方法上同时存在多个切面时切面的优先级控制切面的内外嵌套顺序。

  • 优先级高的切面外面
  • 优先级低的切面里面
    使用@Order注解可以控制切面的优先级
  • @Order(较小的数)优先级高
  • @Order(较大的数)优先级低

切面的优先级

之前多个切面的时候先输出的是LogerAspect紧接着才是ValidateAspect那我们想调换一下切面的执行顺序该怎么做呢
执行顺序
我们可以用到@Order注解看一下源码
源码
Order默认是Integer的最大值而我们知道Order里面的值越小优先级越高那我们设置成1
修改ValidateAspect.java想让验证切面先跑

package com.atguigu.spring.aop.annotation;

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

/**
 * @ClassName: ValidateAspect
 * @Description:计算器加减乘除的验证
 * @Author: wty
 * @Date: 2023/1/13
 */
@Component
@Aspect
@Order(1)
public class ValidateAspect {
    //@Before("execution(* com.atguigu.spring.aop.annotation.Calculator.*(..))")
    @Before("com.atguigu.spring.aop.annotation.LogerAspect.pointCut()")
    public void BeforeAdviceMethod(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        System.out.println("ValidateAspect通知方法:" + signature.getName() + "前置通知");
    }
}

运行测试类AOPTest.java
运行测试类
发现成功改变了切面的优先级。

3.5基于XML的AOP了解

3.5.1准备工作

参考基于注解的AOP环境
新建包com.atguigu.spring.aop.xml然后将四个类拷贝到新包之中。
在这里插入图片描述
删除LogerAspect.java、ValidateAspect.java中与AOP相关的注解
@Aspect、@Pointcut、 @Before、@After、@AfterReturning、@AfterThrowing
创建配置文件aop-xml.xml
创建配置文件
新建测试类com.atguigu.spring.aop.XMLTest

    @Test
    public void testAOPByXml() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-xml.xml");
        Calculator calculator = ioc.getBean(Calculator.class);
        calculator.add(1, 2);
    }

3.5.2实现

aop-xml.xml中添加相关bean

 <!-- 扫描组件 -->
    <context:component-scan base-package="com.atguigu.spring.aop.xml"></context:component-scan>
    <aop:config>
        <!-- 设置一个公共的切入点表达式 -->
        <aop:pointcut id="pointCut" expression="execution(* com.atguigu.spring.aop.xml.CalculatorImpl.*(..))"/>
        <!--
            aop:aspect 将IOC容器中的某个组件设置成切面将组件设置成切面
            aop:pointcut 设置切入点表达式
            aop:advisor 设置通知很少用声明式事务中使用
         -->
        <aop:aspect ref="logerAspect">
            <!--
                aop:before 前置通知
                aop:after 后置通知
                aop:after-returning 返回通知
                aop:after-throwing 异常通知
                aop:around 环绕通知
             -->
            <aop:before method="beforeAdviceMethod" pointcut-ref="pointCut"></aop:before>
            <aop:after method="afterAdviceMethod" pointcut-ref="pointCut"></aop:after>
            <aop:after-returning method="afterReturningAdviceMethod" pointcut-ref="pointCut"
                                 returning="result"></aop:after-returning>
            <aop:after-throwing method="afterThrowingAdviceMethod" pointcut-ref="pointCut"
                                throwing="e"></aop:after-throwing>
            <aop:around method="aroundAdviceMethod" pointcut-ref="pointCut"></aop:around>
        </aop:aspect>
    </aop:config>

执行测试类XMLTest
测试类
设置另一个切面修改aop-xml.xml
设置切面

 <!-- 设置另一个切面-->
        <aop:aspect ref="validateAspect">
            <aop:before method="BeforeAdviceMethod" pointcut-ref="pointCut"></aop:before>
        </aop:aspect>

执行测试类XMLTest.java
结果
设置优先级aop-xml.xml里面有个order属性

        <!-- 设置另一个切面 order设置优先级-->
        <aop:aspect ref="validateAspect" order="1">
            <aop:before method="BeforeAdviceMethod" pointcut-ref="pointCut"></aop:before>
        </aop:aspect>

再次执行测试类XMLTest.java
优先级

4声明式事务

4.1JdbcTemplate

创建新的Module

Namespring-transaction
GroupId:com.atguigu.spring

创建spring工程

4.1.1简介

Spring 框架对 JDBC 进行封装使用 JdbcTemplate 方便实现对数据库操作

4.1.2准备工作

(1)加入依赖

pom.xml添加打包方式

<packaging>jar</packaging>

pom.xml加入依赖

    <dependencies>
        <!-- 基于Maven依赖传递性导入spring-context依赖即可导入当前所需所有jar包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.1</version>
        </dependency>

        <!-- Spring 持久化层支持jar包 -->
        <!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中需要使用orm、jdbc、tx三个
        jar包 -->
        <!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>5.3.1</version>
        </dependency>

        <!-- Spring 测试相关 可以不用手动生成IOC容器 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.3.1</version>
        </dependency>

        <!-- junit测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <!-- MySQL驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.20</version>
        </dependency>

        <!-- 数据源Druid连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.31</version>
        </dependency>


    </dependencies>

依赖如下
依赖

(2)创建jdbc.properties

创建数据库配置文件

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm
jdbc.username=root
jdbc.password=hsp
(3)配置Spring的配置文件

创建spring-jdbc.xml因为都是引入的第三方jar包不是自己写的类所以不能用扫描组件的方式要手动配置。

在这里插入代码片

4.1.3测试

(1)在测试类装配 JdbcTemplate
    <!--引入jdbc.properties 其中location最好加上classpath: -->
    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>

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

    <!-- 这里id的设置可以省略因为ioc获取当前类可以通过byType -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

打开sqlyog操作几张表
截断表t_user
截断
插入一条数据
插入数据
创建测试类com.atguigu.spring.test.JdbcTemplateTest

(2)测试增删改功能

修改JdbcTemplateTest.java

package com.atguigu.spring.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @ClassName: JdbcTemplateTest
 * @Description:
 * @Author: wty
 * @Date: 2023/1/13
 */
// 设置当前类的测试环境:在spring的测试环境中执行,此时就可以通过注入的方式直接获取IOC容器中的bean
@RunWith(SpringJUnit4ClassRunner.class)
// classpath:类路径
@ContextConfiguration("classpath:spring-jdbc.xml")
public class JdbcTemplateTest {

    // 自动装配的方式进行属性的注入
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    public void testInsert() {
        // jdbcTemplate.update()能实现增删改
        String sqlStr = "insert into t_user values(null,?,?,?,?,?)";
        int i = jdbcTemplate.update(sqlStr, "hsp", "1234", 22, "男", "hsp@126.com");
        System.out.println("增加了:" + i + "条数据");
    }
}

执行测试类
执行成功
看一下数据库
插入数据

(3)查询一条数据为实体类对象

创建实体类com.atguigu.spring.pojo.User

package com.atguigu.spring.pojo;

/**
 * @ClassName: User
 * @Description:
 * @Author: wty
 * @Date: 2023/1/13
 */

public class User {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private String gender;
    private String email;

    public User() {
    }

    public User(Integer id, String username, String password, Integer age, String gender, String email) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.age = age;
        this.gender = gender;
        this.email = email;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", age=" + age +
                ", gender='" + gender + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
}


修改测试类JdbcTemplateTest.java

    /**
     * @param
     * @return void
     * @description //获取单个对象
     * @date 2023/1/13 17:01
     * @author wty
     **/
    @Test
    public void getUserByUserId() {
        String sqlStr = "select * from t_user where id = ?";
        User user = jdbcTemplate.queryForObject(sqlStr, new BeanPropertyRowMapper<>(User.class), 2);
        System.out.println(user);
    }

测试结果
测试结果

(4)查询多条数据为一个list集合

在JdbcTemplateTest.java中新增方法

    @Test
    public void getAllUser() {
        String sqlStr = "select * from t_user";
        List<User> list = jdbcTemplate.query(sqlStr, new BeanPropertyRowMapper<>(User.class));
        list.forEach(System.out::println);
    }

并执行测试方法
查询结果

(5)查询单行单列的值

在JdbcTemplateTest.java中新增方法

    @Test
    public void getCount() {
        String sqlStr = "select count(*) from t_user";
        Integer count = jdbcTemplate.queryForObject(sqlStr, Integer.class);
        System.out.println(count);
    }

查询结果
查询结果

4.2声明式事务概念

4.2.1编程式事务

事务功能的相关操作全部通过自己编写代码来实现

        Connection conn = null;
        try {
            // 开启事务关闭事务的自动提交
            conn.setAutoCommit(false);
            // 核心操作
            // 提交事务
            conn.commit();
        } catch (Exception e) {
            // 回滚事务
            conn.rollBack();
        } finally {
        }
        // 释放数据库连接
        conn.close();

编程式的实现方式存在缺陷

  • 细节没有被屏蔽具体操作过程中所有细节都需要程序员自己来完成比较繁琐。
  • 代码复用性不高如果没有有效抽取出来每次实现功能都需要自己编写代码代码就没有得到复用。

4.2.2声明式事务

既然事务控制的代码有规律可循代码的结构基本是确定的所以框架就可以将固定模式的代码抽取出来进行相关的封装。

封装起来后我们只需要在配置文件中进行简单的配置即可完成操作。

  • 好处1提高开发效率
  • 好处2消除了冗余的代码
  • 好处3框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题进行了健壮性、性能等各个方面的优化。

所以我们可以总结下面两个概念

  • 编程式自己写代码实现功能
  • 声明式通过配置让框架实现功能

4.3基于注解的声明式事务

4.3.1准备工作

(1)加入依赖

沿用上一个项目即可

<dependencies>
        <!-- 基于Maven依赖传递性导入spring-context依赖即可导入当前所需所有jar包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.1</version>
        </dependency>

        <!-- Spring 持久化层支持jar包 -->
        <!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中需要使用orm、jdbc、tx三个
        jar包 -->
        <!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>5.3.1</version>
        </dependency>

        <!-- Spring 测试相关 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.3.1</version>
        </dependency>

        <!-- junit测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <!-- MySQL驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.20</version>
        </dependency>

        <!-- 数据源Druid连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.31</version>
        </dependency>


    </dependencies>
(2)创建jdbc.properties

沿用上一个项目

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm
jdbc.username=root
jdbc.password=hsp
(3)配置Spring的配置文件

创建新的配置文件:tx-annotation

    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

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

    <!-- 扫描组件 -->
    <context:component-scan base-package="com.atguigu.spring"></context:component-scan>
(4)创建表

无符号UNSIGNED可以理解为无符号从mysql层面解决负数问题

首先删除t_user表然后执行下面的sql

CREATE TABLE `t_book` (
`book_id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`book_name` VARCHAR(20) DEFAULT NULL COMMENT '图书名称',
`price` INT(11) DEFAULT NULL COMMENT '价格',
`stock` INT(10) UNSIGNED DEFAULT NULL COMMENT '库存无符号',
PRIMARY KEY (`book_id`)
) ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

INSERT INTO `t_book`(`book_id`,`book_name`,`price`,`stock`) VALUES (1,'斗破苍
穹',80,100),(2,'斗罗大陆',50,100);

CREATE TABLE `t_user` (
`user_id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` VARCHAR(20) DEFAULT NULL COMMENT '用户名',
`balance` INT(10) UNSIGNED DEFAULT NULL COMMENT '余额无符号',
PRIMARY KEY (`user_id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO `t_user`(`user_id`,`username`,`balance`) VALUES (1,'admin',50);

(5)创建组件

创建BookController

package com.atguigu.spring.controller;

import com.atguigu.spring.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

/**
 * @ClassName: BookController
 * @Description:
 * @Author: wty
 * @Date: 2023/1/14
 */
@Controller
public class BookController {
    @Autowired
    private BookService bookService;

    /**
     * @param
     * @return void
     * @description //模拟用户买书的功能
     * @param: userId
     * @param: bookId
     * @date 2023/1/14 13:19
     * @author wty
     **/
    public void buyBook(Integer userId, Integer bookId) {
        bookService.buyBook(userId, bookId);
    }
}

创建接口BookService

package com.atguigu.spring.service;

import org.springframework.stereotype.Service;

/**
 * @InterfaceName: BookService
 * @Description:
 * @Author: wty
 * @Date: 2023/1/14
 */
public interface BookService {
    void buyBook(Integer userId, Integer bookId);
}

创建实现类BookServiceImpl

package com.atguigu.spring.service.impl;

import com.atguigu.spring.dao.BookDao;
import com.atguigu.spring.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @ClassName: BookServiceImpl
 * @Description:
 * @Author: wty
 * @Date: 2023/1/14
 */
@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;

    @Override
    public void buyBook(Integer userId, Integer bookId) {
        // 查询图书价格
        Integer price = bookDao.getPriceByBookId(bookId);

        // 更新图书库存
        Integer stock = bookDao.updateStock(bookId);

        // 更新用户的余额
        Integer balance = bookDao.updateBalance(userId, price);
    }
}

创建接口BookDao

package com.atguigu.spring.dao;

/**
 * @InterfaceName: BookDao
 * @Description:
 * @Author: wty
 * @Date: 2023/1/14
 */

public interface BookDao {
    /**
     * @param
     * @return java.lang.Integer
     * @description //根据图书Id查询价格
     * @param: bookId
     * @date 2023/1/14 14:24
     * @author wty
     **/
    Integer getPriceByBookId(Integer bookId);

    /**
     * @param
     * @return java.lang.Integer
     * @description //更新图书库存
     * @param: bookId
     * @date 2023/1/14 14:25
     * @author wty
     **/
    Integer updateStock(Integer bookId);

    /**
     * @param
     * @return java.lang.Integer
     * @description //更新用户余额
     * @param: userId
     * @param: price
     * @date 2023/1/14 14:25
     * @author wty
     **/
    Integer updateBalance(Integer userId, Integer price);

}

创建实现类BookDaoImpl

package com.atguigu.spring.dao.impl;

import com.atguigu.spring.dao.BookDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;

/**
 * @ClassName: BookDaoImpl
 * @Description:
 * @Author: wty
 * @Date: 2023/1/14
 */
@Repository
public class BookDaoImpl implements BookDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Integer getPriceByBookId(Integer bookId) {
        String sqlStr = "select price from t_book where book_id = ?";
        Integer price = jdbcTemplate.queryForObject(sqlStr, Integer.class, bookId);
        return price;
    }

    @Override
    public Integer updateStock(Integer bookId) {
        String sqlStr = "update t_book set stock = stock - 1 where book_id = ?";
        int stock = jdbcTemplate.update(sqlStr, bookId);
        return stock;
    }

    @Override
    public Integer updateBalance(Integer userId, Integer price) {
        String sqlStr = "update t_user set balance = balance - ? where user_id = ?";
        int balance = jdbcTemplate.update(sqlStr, price, userId);
        return balance;
    }
}

类图如下:
类图

4.3.2测试无事务情况

(1)创建测试类

创建测试类com.atguigu.spring.test.TxByAnnotationTest

package com.atguigu.spring.test;

import com.atguigu.spring.controller.BookController;
import com.atguigu.spring.pojo.Book;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @ClassName: TxByAnnotationTest
 * @Description:
 * @Author: wty
 * @Date: 2023/1/14
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:tx-annotation.xml")
public class TxByAnnotationTest {
    @Autowired
    private BookController bookController;

    public void test() {
        bookController.buyBook(1, 1);
    }
}

(2)模拟场景

用户购买图书先查询图书的价格再更新图书的库存和用户的余额
假设用户id为1的用户购买id为1的图书
用户余额为50而图书价格为80
购买图书之后用户的余额为-30数据库中余额字段设置了无符号因此无法将-30插入到余额字段。此时执行sql语句会抛出SQLException

(3)观察结果

直接运行测试类TxByAnnotationTest.java
报错
t_book的库存减少
库存减少
t_user的余额没有变更
余额没有变更
因为没有添加事务图书的库存更新了但是用户的余额没有更新
显然这样的结果是错误的购买图书是一个完整的功能更新库存和更新余额要么都成功要么都失败。

4.3.3加入事务

(1)添加事务配置

在Spring的配置文件中添加配置
tx-annotation.xml中修改

    <!-- 扫描组件 -->
    <context:component-scan base-package="com.atguigu.spring"></context:component-scan>

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

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

注意导入的名称空间需要 tx 结尾的那个。
名称空间
最后配置文件如下
环绕通知

(2)添加事务注解

因为service层表示业务逻辑层一个方法表示一个完成的功能因此处理事务一般在service层处理
在BookServiceImpl的buybook()添加注解@Transactional
增加注解
修改数据库中的库存
更新库存

(3)观察结果

再次执行测试类TxByAnnotationTest.java
结果
库存没有减少
库存
余额也没有变更
余额没有变更

由于使用了Spring的声明式事务更新库存和更新余额都没有执行

(4)声明式事务的配置步骤
  1. 在spring的配置文件中配置事务管理器
  2. 开启事务的注解驱动
    在需要被事务管理的方法上添加@Transactional注解该方法就会被事务管理。

那不禁会问类上能添加@Transactional注解吗?

4.3.4@Transactional注解标识的位置

@Transactional标识在方法上只会影响该方法
@Transactional标识的类上会影响类中所有的方法
注解在类上
@Transactional事务中的属性
注解中的方法

4.3.5事务属性只读

(1)介绍

对一个查询操作来说如果我们把它设置成只读就能够明确告诉数据库这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
默认值

(2)使用方式

在@Transactional后添加(readOnly = true)

    @Transactional(readOnly = true)
    public void buyBook(Integer userId, Integer bookId) {
        // 查询图书价格
        Integer price = bookDao.getPriceByBookId(bookId);

        // 更新图书库存
        Integer stock = bookDao.updateStock(bookId);

        // 更新用户的余额
        Integer balance = bookDao.updateBalance(userId, price);
    }
(3)注意

直接在TxByAnnotationTest.java中运行
报错

对增删改操作设置只读会抛出下面异常

Caused by: java.sql.SQLException: Connection is read-only. Queries
leading to data modification are not allowed

4.3.6事务属性超时

(1)介绍

事务在执行过程中有可能因为遇到某些问题导致程序卡住从而长时间占用数据库资源。而长时间占用资源大概率是因为程序运行出现了问题可能是Java程序或MySQL数据库或网络连接等等。
此时这个很可能出问题的程序应该被回滚撤销它已做的操作事务结束把资源让出来让其他正常程序可以执行。
概括来说就是一句话超时回滚释放资源

超时默认值

(2)使用方式

在@Transactional后增加(timeout = 时间)
这里设置的3就是3秒的意思

 @Transactional(timeout = 3)
    public void buyBook(Integer userId, Integer bookId) {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 查询图书价格
        Integer price = bookDao.getPriceByBookId(bookId);

        // 更新图书库存
        Integer stock = bookDao.updateStock(bookId);

        // 更新用户的余额
        Integer balance = bookDao.updateBalance(userId, price);
    }

程序解读

(3)观察结果

执行TxByAnnotationTest.java
结果

执行过程中抛出异常

org.springframework.transaction.TransactionTimedOutException:
Transaction timed out: deadline was Fri Jun 04 16:25:39 CST 2022

4.3.7事务属性回滚策略

(1)介绍

声明式事务默认只针对运行时异常回滚编译时异常不回滚。
可以通过@Transactional中相关属性设置回滚策略

  • rollbackFor属性需要设置一个Class类型的对象
  • rollbackForClassName属性需要设置一个字符串类型的全类名。
  • noRollbackFor属性需要设置一个Class类型的对象
  • rollbackFor属性需要设置一个字符串类型的全类名

回滚

(2)使用方式

更改t_user表的余额更改为100
更改余额

    public void buyBook(Integer userId, Integer bookId) {
        // 查询图书价格
        Integer price = bookDao.getPriceByBookId(bookId);

        // 更新图书库存
        Integer stock = bookDao.updateStock(bookId);

        // 更新用户的余额
        Integer balance = bookDao.updateBalance(userId, price);

        System.out.println(1 / 0);
    }

因为加了1/0这种默认无事务的情况下是不回滚都执行的查看结果。
执行TxByAnnotationTest.java
抛出异常
t_user中余额减少
余额减少
t_book中库存减少
库存减少
这个时候我们更改BookServiceImpl.java采用注解@Transactional的默认策略对任意的运行时异常回滚

 @Transactional
    public void buyBook(Integer userId, Integer bookId) {
        // 查询图书价格
        Integer price = bookDao.getPriceByBookId(bookId);

        // 更新图书库存
        Integer stock = bookDao.updateStock(bookId);

        // 更新用户的余额
        Integer balance = bookDao.updateBalance(userId, price);

        System.out.println(1 / 0);
    }

恢复数据库中的数据
恢复数据1
恢复数据2
再执行TxByAnnotationTest.java
异常信息不变
发现库存不变
库存不变
发现余额也不变
余额不变

现在想要出现算数异常的时候不回滚我们怎么办呢看下面的操作。

(3)观察结果

更改BookServiceImpl.java注解中用noRollbackFor意思是算术类型异常不回滚。

 @Transactional(noRollbackFor = ArithmeticException.class)
    public void buyBook(Integer userId, Integer bookId) {

        // 查询图书价格
        Integer price = bookDao.getPriceByBookId(bookId);

        // 更新图书库存
        Integer stock = bookDao.updateStock(bookId);

        // 更新用户的余额
        Integer balance = bookDao.updateBalance(userId, price);

        System.out.println(1 / 0);
    }

再执行TxByAnnotationTest.java
异常信息不变
查看数据库结果
库存减少
余额减少
这个时候我们继续更改BookServiceImpl.java采用noRollbackForClassName

@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
    public void buyBook(Integer userId, Integer bookId) {
        // 查询图书价格
        Integer price = bookDao.getPriceByBookId(bookId);

        // 更新图书库存
        Integer stock = bookDao.updateStock(bookId);

        // 更新用户的余额
        Integer balance = bookDao.updateBalance(userId, price);

        System.out.println(1 / 0);
    }

我们先恢复数据库的数据。把库存和余额恢复
再运行测试类TxByAnnotationTest.java查看结果我们发现还是不会回滚。
库存减少
余额减少

虽然购买图书功能中出现了数学运算异常ArithmeticException但是我们设置的回滚策略是当出现ArithmeticException不发生回滚因此购买图书的操作正常执行。

4.3.8事务属性事务隔离级别

(1)介绍

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

隔离级别一共有四种

  • 读未提交READ UNCOMMITTED
    允许Transaction01读取Transaction02未提交的修改。
  • 读已提交READ COMMITTED、
    要求Transaction01只能读取Transaction02已提交的修改。
  • 可重复读REPEATABLE READ
    确保Transaction01可以多次从一个字段中读取到相同的值即Transaction01执行期间禁止其它事务对这个字段进行更新。
  • 串行化SERIALIZABLE
    确保Transaction01可以多次从一个表中读取到相同的行在Transaction01执行期间禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题但性能十分低下。
    各个隔离级别解决并发问题的能力见下表
隔离级别脏读不可重复读幻读
READ UNCOMMITTED
READ COMMITTED×
REPEATABLE READ××
SERIALIZABLE×××

各种数据库产品对事务隔离级别的支持程度

隔离级别OracleMySQL
READ UNCOMMITTED×
READ COMMITTED
REPEATABLE READ×√(默认)
SERIALIZABLE
(2)使用方式
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化

读取
枚举类型
枚举类型

4.3.9事务属性事务传播行为

(1)介绍

当事务方法被另一个事务方法调用时必须指定事务应该如何传播。例如方法可能继续在现有事务中运行也可能开启一个新事务并在自己的事务中运行。

(2)测试

创建接口CheckoutService

package com.atguigu.spring.service;

/**
 * @InterfaceName: CheckOutService
 * @Description:
 * @Author: wty
 * @Date: 2023/1/15
 */

public interface CheckOutService {
    void checkOut(Integer userId, Integer[] bookIds);
}

创建实现类CheckoutServiceImpl注意checkOut方法是@Transactional修饰的。

/**
 * @ClassName: CheckOutServiceImpl
 * @Description:
 * @Author: wty
 * @Date: 2023/1/15
 */
@Service
public class CheckOutServiceImpl implements CheckOutService {
    @Autowired
    private BookService bookService;

    @Override
    @Transactional
    public void checkOut(Integer userId, Integer[] bookIds) {
        for (Integer bookId : bookIds) {
            bookService.buyBook(userId, bookId);
        }
    }
}

在BookController中添加方法

 /**
     * @param
     * @return void
     * @description //结账
     * @date 2023/1/15 17:57
     * @author wty
     **/
    public void checkOut(Integer userId, Integer[] bookIds) {
        checkOutService.checkOut(userId, bookIds);
    }

恢复BookServiceImpl.java中的信息不要抛出异常。注意buyBook方法是@Transactional修饰的。

/**
 * @ClassName: BookServiceImpl
 * @Description:
 * @Author: wty
 * @Date: 2023/1/14
 */
@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;

    @Override
    @Transactional(isolation = Isolation.DEFAULT)
    public void buyBook(Integer userId, Integer bookId) {

        // 查询图书价格
        Integer price = bookDao.getPriceByBookId(bookId);

        // 更新图书库存
        Integer stock = bookDao.updateStock(bookId);

        // 更新用户的余额
        Integer balance = bookDao.updateBalance(userId, price);
    }
}

在数据库中将用户(t_user)的余额修改为100元
在数据库中将图书(t_book)的库存修改为100本
在测试类TxByAnnotationTest.java中添加方法

    @Test
    public void test2() {
        Integer[] nums = {1, 2};
        bookController.checkOut(1, nums);
    }
(3)观察结果

运行测试类为TxByAnnotationTest.java
报错信息
看一下数据库的情况
t_user无变化
在这里插入图片描述
t_book无变化
在这里插入图片描述
无变化说明即在checkOut中设置了事务又在buyBook方法上设置了事务最后有效的是checkOut中的这保证了所有书要买就都得成功要有一本书买不了所有书都有问题那么我们如果想更改一下呢只让事务对buyBook有效每本书各买各的。
修改BookServiceImpl.java增加注解的属性

    @Transactional(isolation = Isolation.DEFAULT, propagation = Propagation.REQUIRES_NEW)

再次执行测试类TxByAnnotationTest.java
报错
余额
库存

可以通过@Transactional中的propagation属性设置事务传播行为
修改BookServiceImpl中buyBook()上注解@Transactional的propagation属性

@Transactional(propagation = Propagation.REQUIRED)

默认情况表示如果当前线程上有已经开启的事务可用那么就在这个事务中运行。经过观察购买图书的方法buyBook()在checkout()中被调用checkout()上有事务注解因此在此事务中执行。所购买的两本图书的价格为80和50而用户的余额为100因此在购买第二本图书时余额不足失败导致整个checkout()回滚即只要有一本书买不了就都买不了。

@Transactional(propagation = Propagation.REQUIRES_NEW)

表示不管当前线程上是否有已经开启的事务都要开启新事务。同样的场景每次购买图书都是在buyBook()的事务中执行因此第一本图书购买成功事务结束第二本图书购买失败只在第二次的buyBook()中回滚购买第一本图书不受影响即能买几本就买几本。

4.4基于XML的声明式事务

4.4.1场景模拟

参考基于注解的声明式事务
删除(注释)CheckOutServiceImpl.java中checkOut方法的注解@Transactional
注释

删除(注释)BookServiceImpl.java中buyBook方法的注解@Transactional
注释

4.3.2修改Spring配置文件

新建tx-xml.xml配置文件大体拷贝以前的配置文件tx-xml.xml即可。

将Spring配置文件中去掉tx:annotation-driven 标签并添加配置
去掉标签

<!--
        配置事务的通知
        id 设置事务通知的唯一标识
        transaction-manager:设置事务管理器的id如果事务管理器的id是transactionManager那么下面不用写
    -->
    <tx:advice id="tx" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="buyBook"/>
        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:advisor advice-ref="tx" pointcut="execution(* com.atguigu.spring.service.impl.*.*(..))"></aop:advisor>
    </aop:config>
在这里插入代码片

在数据库中将用户(t_user)的余额修改为100元
在数据库中将图书(t_book)的库存修改为100本
新建测试类TxByXmlTest.java大体拷贝TxByAnnotationTest.java即可
执行测试类的test方法报错
抛出异常

package com.atguigu.spring.test;

import com.atguigu.spring.controller.BookController;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @ClassName: TxByAnnotationTest
 * @Description:
 * @Author: wty
 * @Date: 2023/1/14
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:tx-xml.xml")
public class TxByXmlTest {
    @Autowired
    private BookController bookController;

    @Test

    public void test() {
        bookController.buyBook(1, 1);
    }

    @Test
    public void test2() {
        Integer[] nums = {1, 2};
        bookController.checkOut(1, nums);
    }
}

注意基于xml实现的声明式事务必须引入aspectJ的依赖

在pom.xml中添加依赖

<!-- 导入aspectJ的依赖 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.3.1</version>
        </dependency>

依赖
此时再执行测试类的test方法发现正常执行没有问题。
库存减少
库存减少
余额减少
余额减少
那每次配置文件tx-xml.xml都要配置方法根据不同方法配置事务很麻烦我们想到了根据方法名的规则来统一管理

<!--
        配置事务的通知
        id 设置事务通知的唯一标识
        transaction-manager:设置事务管理器的id如果事务管理器的id是transactionManager那么下面不用写
    -->
    <tx:advice id="tx" transaction-manager="transactionManager">
        <tx:attributes>
            <!-- 如果所有方法都要事务管理就用* -->
            <!--<tx:method name="*"/>-->
            <tx:method name="buyBook"/>
            <!-- 如果以get、query、find开头的方法都是查询只读 -->
            <tx:method name="get*" read-only="true"></tx:method>
            <tx:method name="query*" read-only="true"></tx:method>
            <tx:method name="find*" read-only="true"></tx:method>

            <!-- read-only属性设置只读属性 -->
            <!-- rollback-for属性设置回滚的异常 -->
            <!-- no-rollback-for属性设置不回滚的异常 -->
            <!-- isolation属性设置事务的隔离级别 -->
            <!-- timeout属性设置事务的超时属性 -->
            <!-- propagation属性设置事务的传播行为 -->

            <!-- 如果以update开头的方法,都以修改 -->
            <tx:method name="update*" read-only="false" rollback-for="java.lang.Exception"
                       propagation="REQUIRES_NEW"></tx:method>

            <!-- 如果以delete开头的方法都是删除 -->
            <tx:method name="delete*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>

            <!-- 如果以save开头的方法都是插入  -->
            <tx:method name="save*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>


        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:advisor advice-ref="tx" pointcut="execution(* com.atguigu.spring.service.impl.*.*(..))"></aop:advisor>
    </aop:config>

管理

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Spring