Spring框架学习笔记,超详细!!(4)

Java小白开始学习Spring框架,一方面,跟着视频学习,并记录下学习笔记,方便以后复习回顾。另一方面,发布学习笔记来约束自己,学习路程还很遥远,继续加油坚持!!!希望能帮助到大家!

另外还有我的牛客Java专项练习笔记专栏也在同步更新,希望大家多多关注,一起学习!!!

本次更新了GoF之代理模式、面向切面编程AOP、Spring对事务的支持等相关知识点。

上期链接Spring框架学习笔记,超详细!!(3)

11. GoF之代理模式

11.1 对代理模式的理解

场景一:你刚到北京,要租房子,可以自己找,也可以找链家帮你找。其中链家是代理类,你是目标类。你们两个都有共同的行为:找房子。不过链家除了满足你找房子,另外会收取一些费用的。(现实生活中的房产中介)【在程序中,功能需要增强时。】

场景二:生活场景1:牛村的牛二看上了隔壁村小花,牛二不好意思直接找小花,于是牛二找来了媒婆王妈妈。这里面就有一个非常典型的代理模式。牛二不能和小花直接对接,只能找一个中间人。其中王妈妈是代理类,牛二是目标类。王妈妈代替牛二和小花先见个面。(现实生活中的婚介所)【在程序中,对象A和对象B无法直接交互时。】

场景三:八戒和高小姐的故事。八戒要强抢民女高翠兰。悟空得知此事之后怎么做的?悟空幻化成高小姐的模样。代替高小姐与八戒会面。其中八戒是客户端程序。悟空是代理类。高小姐是目标类。那天夜里,在八戒眼里,眼前的就是高小姐,对于八戒来说,他是不知道眼前的高小姐是悟空幻化的,在他内心里这就是高小姐。所以悟空代替高小姐和八戒亲了嘴儿。这是非常典型的代理模式实现的保护机制。**==代理模式中有一个非常重要的特点:对于客户端程序来说,使用代理对象时就像在使用目标对象一样。==**【在程序中,目标需要被保护时】

场景四:系统中有A、B、C三个模块,使用这些模块的前提是需要用户登录,也就是说在A模块中要编写判断登录的代码,B模块中也要编写,C模块中还要编写,这些判断登录的代码反复出现,显然代码没有得到复用,可以为A、B、C三个模块提供一个代理,在代理当中写一次登录判断即可。代理的逻辑是:请求来了之后,判断用户是否登录了,如果已经登录了,则执行对应的目标,如果没有登录则跳转到登录页面。【在程序中,目标不但受到保护,并且代码也得到了复用。】

代理模式是23种设计模式之一。属于结构型设计模式。

作用:

  1. 为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用。

  1. 代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不应该看到的内容和服务或者添加客户需要的额外服务。

代理模式中的角色:

  • 代理类(代理主题):提供与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能

  • 目标类(真实主题):实现了抽象主题中的具体业务,是代理对象的真实对象,是最终要引用的对象

  • 代理类和目标类的公共接口(抽象主题):客户端在使用代理类时就像在使用目标类,不被客户端所察觉,所以代理类和目标类要有共同的行为,也就是实现共同的接口。

Spring框架学习笔记,超详细!!(4)_第1张图片

代理模式在代码实现上,包括两种形式:

  • 静态代理

  • 动态代理

11.2 静态代理

  • 接口和实现类

//订单接口
public interface OrderService {
    /**
     * 生成订单
     */
    void generate();

    /**
     * 查看订单详情
     */
    void detail();

    /**
     * 修改订单
     */
    void modify();
}

//实现类
public class OrderServiceImpl implements OrderService {
    @Override
    public void generate() {
        try {
            Thread.sleep(1234);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已生成");
    }

    @Override
    public void detail() {
        try {
            Thread.sleep(2541);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单信息如下:******");
    }

    @Override
    public void modify() {
        try {
            Thread.sleep(1010);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已修改");
    }
}

项目已上线,并且运行正常,只是客户反馈系统有一些地方运行较慢,要求项目组对系统进行优化。于是项目负责人就下达了这个需求。首先需要搞清楚是哪些业务方法耗时较长,于是让我们统计每个业务方法所耗费的时长。

  • 第一种方案:直接修改Java源代码,在每个业务方法中添加统计逻辑,如下:

public class OrderServiceImpl implements OrderService {
    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(1234);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已生成");
        long end = System.currentTimeMillis();
        System.out.println("耗费时长"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(2541);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单信息如下:******");
        long end = System.currentTimeMillis();
        System.out.println("耗费时长"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(1010);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已修改");
        long end = System.currentTimeMillis();
        System.out.println("耗费时长"+(end - begin)+"毫秒");
    }
}

需求可以满足,但是违背了OCP开闭原则。

  • 第二种方案:编写一个子类继承OrderServiceImpl,在子类中重写每个方法

public class OrderServiceImplSub extends OrderServiceImpl{
    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        super.generate();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        super.detail();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        super.modify();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }
}

这种方式可以解决,但是存在两个问题:

  1. 第一个问题:假设系统中有100个这样的业务类,需要提供100个子类,并且之前写好的创建Service对象的代码,都要修改为创建子类对象。

  1. 第二个问题:由于采用了继承的方式,导致代码之间的耦合度较高。

这种方案也不可取。

  • 第三种方案:使用代理类(静态代理)

为OrderService接口提供一个代理类

public class OrderServiceProxy implements OrderService{ // 代理对象

    // 目标对象
    private OrderService orderService;

    // 通过构造方法将目标对象传递给代理对象
    public OrderServiceProxy(OrderService orderService) {
        this.orderService = orderService;
    }

    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.generate();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.detail();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.modify();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }
}

这种方式的优点:

  1. 符合OCP开闭原则,

  1. 将目标对象作为代理对象的一个属性,这种关系叫做关联关系,所以程序的耦合度较低。

客户端程序:

public class Client {
    public static void main(String[] args) {
        // 创建目标对象
        OrderService target = new OrderServiceImpl();
        // 创建代理对象
        OrderService proxy = new OrderServiceProxy(target);
        // 调用代理对象的代理方法
        proxy.generate();
        proxy.modify();
        proxy.detail();
    }
}

如果系统中业务接口很多,一个接口对应一个代理类,显然也是不合理的,会导致类爆炸。怎么解决这个问题?动态代理可以解决。因为在动态代理中可以在内存中动态的为我们生成代理类的字节码。代理类不需要我们写了。类爆炸解决了,而且代码只需要写一次,代码也会得到复用。

11.3 动态代理

在程序运行阶段,在内存中动态生成代理类,被称为动态代理,目的是为了减少代理类的数量。解决代码复用的问题。

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

  • JDK动态代理技术:只能代理接口。

  • CGLIB动态代理技术:CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。它既可以代理接口,又可以代理类,底层是通过继承的方式实现的。性能比JDK动态代理要好。(底层有一个小而快的字节码处理框架ASM

  • Javassist动态代理技术:Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。

11.3.1 JDK动态代理
  • 接口和实现类

public interface OrderService {
    /**
     * 生成订单
     */
    void generate();

    /**
     * 查看订单详情
     */
    void detail();

    /**
     * 修改订单
     */
    void modify();

    String getName();
}


public class OrderServiceImpl implements OrderService{
    @Override
    public void generate() {
        try {
            Thread.sleep(1234);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已生成");
    }

    @Override
    public void detail() {
        try {
            Thread.sleep(2541);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单信息如下:******");
    }

    @Override
    public void modify() {
        try {
            Thread.sleep(1010);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已修改");
    }

    @Override
    public String getName() {
        System.out.println("getName");
        return "张三";
    }
}
  • 在动态代理中UserServiceProxy代理类是可以动态生成的。这个类不需要写。我们直接写客户端程序

public class Client {
    public static void main(String[] args) {

        //创建目标对象
        OrderService target = new OrderServiceImpl();
        //创建代理对象
        /**
         * newProxyInstance 可以创建代理对象
         * 本质上做了两件事:
         *              在内存中动态的生成了一个代理类的字节码class
         *              new对象了。通过内存中生成的代理类这个代码,实例化了对象
         * 第一个参数:ClassLoader loader
         *           类加载器:在内存中生成的字节码也是class文件,要执行也得先加载到内存中。加载类就需要类加载器。所以这里
         *           需要指定类加载器。并且JDK要求,目标类的类加载器必须和代理类的类加载器是同一个
         * 第二个参数:Class[] interfaces
         *           代理类和目标类要实现同一个(一些)接口,在内存中生成代理类的时候,这个代理类需要知道实现哪些接口
         * 第三个参数:InvocationHandler h
         *           调用处理器,是一个接口。在调用处理器接口中编写的就是增强代码。既然是接口,就要写接口的实现类
         */
        
        /*OrderService proxyObj = (OrderService)Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new TimeInvocationHandler(target)
                );*/

        //封装成工具类
        OrderService proxyObj = (OrderService) ProxyUtil.newProxyInstance(target);
        
        //调用代理对象的代理方法
        proxyObj.generate();
        proxyObj.modify();
        proxyObj.detail();
        String name = proxyObj.getName();
        System.out.println(name);
    }
}
  • 所以接下来我们要写一下java.lang.reflect.InvocationHandler接口的实现类,并且实现接口中的方法,我们将来肯定是要调用“目标方法”的,但要调用目标方法的话,需要“目标对象”的存在,“目标对象”从哪儿来呢?我们可以给TimerInvocationHandler提供一个构造方法,可以通过这个构造方法传过来“目标对象”,代码如下:代码如下:

public class TimeInvocationHandler implements InvocationHandler {
    //目标对象
    private Object target;

    //通过构造方法来穿目标对象
    public TimeInvocationHandler(Object target) {
        this.target = target;
    }

    /**
     * invoke方法社么时候被调用?
     * 当代理对象调用代理方法的时候,注册在InvocationHandler调用处理器中的invoke()方法被调用
     *
     * 第一个参数:Object proxy:代理对象的引用
     * 第二个参数:Method method:目标对象上的目标方法
     * 第三个参数:Object[] args:目标方法上的实参
     *
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        long begin = System.currentTimeMillis();

        //调用目标对象上的目标方法
        Object value = method.invoke(target, args);
        long end = System.currentTimeMillis();
        System.out.println("耗时:" +(end - begin) + "ms");

        //如果代理对象调用代理方法之后,需要返回结果的话,invoke防范必须将目标对象的目标方法执行结果继续返回
        return value;
    }
}
  • 封装ProxyUtil工具类

public class ProxyUtil {
    public static Object newProxyInstance(Object target) {
        return  Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new TimeInvocationHandler(target)
        );
    }
}
  • 结果:

Spring框架学习笔记,超详细!!(4)_第2张图片
11.3.2 CGLIB动态代理

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

使用CGLIB,需要引入它的依赖:


  cglib
  cglib
  3.3.0
  • 没有实现接口的类

public class UserService {

    public void login(){
        System.out.println("用户正在登录系统....");
    }

    public void logout(){
        System.out.println("用户正在退出系统....");
    }
}
  • 使用CGLIB在内存中为UserService类生成代理类,并创建对象:

public class Client {
    public static void main(String[] args) {
        // 创建字节码增强器
        Enhancer enhancer = new Enhancer();
        // 告诉cglib要继承哪个类
        enhancer.setSuperclass(UserService.class);
        // 设置回调接口
        enhancer.setCallback(方法拦截器对象);
        // 生成源码,编译class,加载到JVM,并创建代理对象
        UserService userServiceProxy = (UserService)enhancer.create();

        userServiceProxy.login();
        userServiceProxy.logout();
    }
}

和JDK动态代理原理差不多,在CGLIB中需要提供的不是InvocationHandler,而是:net.sf.cglib.proxy.MethodInterceptor

  • 编写MethodInterceptor接口实现类:

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

MethodInterceptor接口中有一个方法intercept(),该方法有4个参数:

  1. 第一个参数:目标对象

  1. 第二个参数:目标方法

  1. 第三个参数:目标方法调用时的实参

  1. 第四个参数:代理方法

  • 在MethodInterceptor的intercept()方法中调用目标以及添加增强:

public class TimerMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 前增强
        long begin = System.currentTimeMillis();
        // 调用目标
        Object retValue = methodProxy.invokeSuper(target, objects);
        // 后增强
        long end = System.currentTimeMillis();
        System.out.println("耗时" + (end - begin) + "毫秒");
        // 一定要返回
        return retValue;
    }
}
  • 客户端程序

public class Client {
    public static void main(String[] args) {
        // 创建字节码增强器
        Enhancer enhancer = new Enhancer();
        // 告诉cglib要继承哪个类
        enhancer.setSuperclass(UserService.class);
        // 设置回调接口
        enhancer.setCallback(new TimerMethodInterceptor());
        // 生成源码,编译class,加载到JVM,并创建代理对象
        UserService userServiceProxy = (UserService)enhancer.create();

        userServiceProxy.login();
        userServiceProxy.logout();
    }
}
  • 对于高版本的JDK,如果使用CGLIB,需要在启动项中添加两个启动参数:

Spring框架学习笔记,超详细!!(4)_第3张图片

--add-opens java.base/java.lang=ALL-UNNAMED

--add-opens java.base/sun.net.util=ALL-UNNAMED

  • 结果

Spring框架学习笔记,超详细!!(4)_第4张图片

12. 面向切面编程AOP

IoC使软件组件解耦合。AOP可以捕捉系统中经常使用的功能,把他转化成组件。在不惊动原始设计的基础上为其进行功能增强。简单的说就是在==不改变方法源代码的基础上对方法进行功能增强。==

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

AOP底层使用的就是动态代理来实现的

Spring的AOP使用的动态代理是:JDK动态代理+CGLIB动态代理技术。Spring在这两种动态代理中灵活切换,如果是代理接口,会默认使用JDK动态代理;如果要代理某个类,这个类没有实现接口,就会切换到CGLIB。当然,也可以强制配置让Spring只使用CGLIB。

12.1 AOP介绍

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

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

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

  • 第一:交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复用。并且修改这些交叉业务代码的话,需要修改多处。

  • 第二:程序员无法专注核心业务代码的编写,在编写核心业务代码的同时还需要处理这些交叉业务。

使用AOP可以很轻松的解决以上问题。

请看下图,可以帮助你快速理解AOP的思想:

Spring框架学习笔记,超详细!!(4)_第5张图片

总结:将核心业务无关的代码独立的抽取出来,形成一个酥里的组件,然后以横向交叉的方式应用到业务流程当中的过程被称为AOP

AOP的优点:

  • 代码复用性强

  • 代码易维护

  • 使开发者更关注业务逻辑

12.2 AOP的七大术语

Spring框架学习笔记,超详细!!(4)_第6张图片
  • 连接点(Joinpoint):在程序的整个执行流程中,可以织入切面的**==位置==**。方法的执行前后,异常抛出之后等位置。例如:update()、delete()、select()等都是连接点。

  • 切点(Pointcut):在程序执行流程中,真正织入切面的**==方法==**。(一个切点对应多个连接点)。例如:update()、delete()方法,select()方法没有被增强所以不是切入点,但是是连接点。

  • 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法

  • 通知(Advice):通知又叫增强,就是具体你要织入的**==代码==**。

  • 通知又包括:

  • 前置通知:@Before,目标方法执行之前的通知

  • 后置通知:@AfterReturning,目标方法执行之后的通知

  • 环绕通知:@Around,目标方法之前添加通知,同时目标方法执行之后添加通知。

  • 异常通知:@AfterThrowing,发生异常之后执行的通知

  • 最终通知:@After 放在finally,放在finally语句块中的通知

  • 切面(Aspect):描述通知与切入点的对应关系,也就是哪些通知方法对应哪些切入点方法。

  • 织入(Weaving):把通知应用到目标对象上

  • 代理对象(Proxy):一个目标对象被织入通知后产生新对象

  • 目标对象(Target):被织入通知的对象

12.3 切点表达式

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

格式:

execution([访问控制权限修饰符] 返回值类型 [全限定类名] 方法名 (形式参数列表) [异常])

访问修饰符(可选项):

  • public,private等,可以省略

返回值类型(必填项):

  • ”*“表示返回任意类型

全限定名(可选项):

  • 两个点”..“代表当前包以及子包下的所有类

  • 省略时表示所有的类

方法名(必填项):

  • ”*“表示所有方法

  • ”set*“表示所有set开头的方法

形参参数列表(必填项):

  • ()表示没有参数的方法

  • (..)参数类型和个数任意的方法

  • (*)只有一个参数的方法

  • (*, String)第一个参数类型任意,第二个参数是String的

异常(可选项):

  • 省略时表示任意异常类型

示例:

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

12.4 使用Spring的AOP

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

  1. Spring框架结合AspectJ框架实现的AOP,基于注解方式

  1. Spring框架结合AspectJ框架实现的AOP,基于XML方式

  1. Spring框架自己实现的AOP,基于XML方式

实际开发中,都是Spring+AspectJ来实现AOP。什么是AspectJ?(Eclipse组织的一个支持AOP的框架。AspectJ框架是独立于Spring框架之外的一个框架,Spring框架用了AspectJ)

12.4.1 准备工作

使用Spring+AspectJ的AOP需要引入的依赖如下:



  org.springframework
  spring-context
  6.0.0-M2



  org.springframework
  spring-aspects
  6.0.0-M2

Spring配置文件中添加context命名空间和aop命名空间(全注解开发不需要配置)




12.4.2 基于AspectJ的AOP注解式开发

【第一步】定义类以及目标方法

@Service
public class UserService {
    public void login(){
        System.out.println("正在登陆......");
    }
    
    public void logout(){
        System.out.println("正在退出......");
    }
}

【第二步】定义切面类并添加通知

@Component
@Aspect  //切面类需要@Aspect注解进行标注
public class LogAspect {//切面

    //切面 = 通知 + 切点
    //通知就是增强,就是具体的增强代码

    @Before("execution(* com.heihei.springaop.service.UserService.*(..))")      //前置通知
    public void enhance() {
        System.out.println("增强代码......");
    }
}

【第三步】在配置类中进行Spring注解包扫描和开启AOP功能

@Configuration
@ComponentScan("com.heihei.springaop")
//开启注解开发AOP功能
@EnableAspectJAutoProxy
public class SpringConfig {
}

【运行测试类和结果】

@Test
    public void testAOP() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        UserService userService = applicationContext.getBean("userService",UserService.class);
        userService.login();
        userService.logout();
    }
Spring框架学习笔记,超详细!!(4)_第7张图片
12.4.3 AOP通知
  • 前置通知 @Before:当前通知方法在原始切入点方法前运行

  • 后置通知 @AfterReturning:当前通知方法在原始切入点方法后运行

  • 异常通知 @AfterThrowing:发生异常之后执行的通知

  • 最终通知 @After:放在finally语句块中的通知

  • 环绕通知 @Around:目标方法之前添加通知,同时目标方法执行之后添加通知。

示例:

// 切面类
@Component
@Aspect
public class MyAspect {

    @Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知开始");
        // 执行目标方法。
        proceedingJoinPoint.proceed();
        System.out.println("环绕通知结束");
    }

    @Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void beforeAdvice(){
        System.out.println("前置通知");
    }

    @AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void afterReturningAdvice(){
        System.out.println("后置通知");
    }

    @AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void afterThrowingAdvice(){
        System.out.println("异常通知");
    }

    @After("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void afterAdvice(){
        System.out.println("最终通知");
    }

}

结果:

Spring框架学习笔记,超详细!!(4)_第8张图片

【注意】

  • 结果中没有异常通知,这是因为目标程序执行过程中没有发生异常。

  • 模拟发生异常,结果:出现异常之后,后置通知和环绕通知的结束部分不会执行。

Spring框架学习笔记,超详细!!(4)_第9张图片
  • 环绕通知方法**==形参必须是ProceedingJoinPoint==,表示正在执行的连接点,使用该对象的proceed()方法表示对原始对象方法进行调用,返回值为原始对象方法的返回值。其他通知可以使用==JoinPoint==**

例如:Signature signature = proceedingJoinPoint.getSignature();可以获取目标方法的签名,通过方法的签名可以获取到一个方法的具体信息
获取目标方法的方法名:proceedingJoinPoint.getSignature().getName();
  • 环绕通知方法的返回值建议写成Object类型,用于将原始对象方法的返回值进行返回,哪里使用代理对象就返回到哪里。

12.4.4 切面的先后顺序

业务流程当中不一定只有一个切面,可能有的切面控制事务,有的记录日志,有的进行安全控制,如果多个切面的话,顺序如何控制:**==可以使用@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小,优先级越高。==**

@Component
@Aspect  //切面类需要@Aspect注解进行标注
@Order(2)  //设置优先级
public class LogAspect {//切面

    //切面 = 通知 + 切点
    //通知就是增强,就是具体的增强代码

    @Before("execution(* com.heihei.springaop.service.UserService.*(..))")      //前置通知
    public void enhance() {
        System.out.println("增强代码......");
    }
}
12.4.5 优化使用切点表达式
@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
@After("execution(* com.powernode.spring6.service.OrderService.*(..))")

缺点是:

  • 第一:切点表达式重复写了多次,没有得到复用。

  • 第二:如果要修改切点表达式,需要修改多处,难维护。

【优化】将切点表达式独立的定义出来,在需要的位置引用

@Component
@Aspect
@Order(2)
public class MyAspect {
    
    //注意这个@Pointcut注解标注的方法随意,只是起到一个能够让@Pointcut注解编写的位置。
    @Pointcut("execution(* com.powernode.spring6.service.OrderService.*(..))")
    public void pointcut(){}

    @Around("pointcut()")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知开始");
        // 执行目标方法。
        proceedingJoinPoint.proceed();
        System.out.println("环绕通知结束");
    }

    @Before("pointcut()")
  	...

    @AfterReturning("pointcut()")
	...

    @AfterThrowing("pointcut()")
    ...

    @After("pointcut()")
  	...
}

12.5 AOP案例:事务处理

项目中的事务控制是在所难免的。在一个业务流程当中,可能需要多条DML语句共同完成,为了保证数据的安全,这多条DML语句要么同时成功,要么同时失败。这就需要添加事务控制的代码。例如以下伪代码:

class 业务类1{
    public void 业务方法1(){
        try{
            // 开启事务
            startTransaction();
            
            // 执行核心业务逻辑
            ....
            
            // 提交事务
            commitTransaction();
        }catch(Exception e){
            // 回滚事务
            rollbackTransaction();
        }
    }
    public void 业务方法2(){
        try{
            // 开启事务
            startTransaction();
            
            // 执行核心业务逻辑
            ....
            
            // 提交事务
            commitTransaction();
        }catch(Exception e){
            // 回滚事务
            rollbackTransaction();
        }
    }
}

class 业务类2{
    public void 业务方法1(){
        try{
            // 开启事务
            startTransaction();
            
            // 执行核心业务逻辑
            ....
            
            // 提交事务
            commitTransaction();
        }catch(Exception e){
            // 回滚事务
            rollbackTransaction();
        }
    }
    public void 业务方法2(){
        try{
            // 开启事务
            startTransaction();
            
            // 执行核心业务逻辑
            ....
            
            // 提交事务
            commitTransaction();
        }catch(Exception e){
            // 回滚事务
            rollbackTransaction();
        }
    }
}
//......

这些业务类中的每一个业务方法都是需要控制事务的,而控制事务的代码又是固定的格式,都是:

try{
    // 开启事务
    startTransaction();

    // 执行核心业务逻辑
    ......

    // 提交事务
    commitTransaction();
}catch(Exception e){
    // 回滚事务
    rollbackTransaction();
}

这个控制事务的代码就是和业务逻辑没有关系的“交叉业务”。以上伪代码当中可以看到这些交叉业务的代码没有得到复用,并且如果这些交叉业务代码需要修改,那必然需要修改多处,难维护,怎么解决?可以采用AOP思想解决。可以把以上控制事务的代码作为环绕通知,切入到目标类的方法当中。

【示例】

  • 银行的业务类

@Component
// 业务类
public class AccountService {
    // 转账业务方法
    public void transfer(){
        System.out.println("正在进行银行账户转账");
    }
    // 取款业务方法
    public void withdraw(){
        System.out.println("正在进行取款操作");
    }
}
  • 订单业务类

@Component
// 业务类
public class OrderService {
    // 生成订单
    public void generate(){
        System.out.println("正在生成订单");
    }
    // 取消订单
    public void cancel(){
        System.out.println("正在取消订单");
    }
}
  • 使用AOP给两个业务类添加事务控制

@Aspect
@Component
public class TransatcionAspect {

    @Around("execution(* com.heihei.springaop.bank..*(..))")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("开启事务");
            //执行目标
            joinPoint.proceed();
            System.out.println("提交事务");
        } catch (Throwable e) {
            System.out.println("回滚事务");
        }
    }
}
  • 测试

@Test
    public void testTransaction() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        AccountService accountService = applicationContext.getBean(AccountService.class);
        OrderService orderService = applicationContext.getBean(OrderService.class);
        //转账
        accountService.transfer();
        //取款
        accountService.withdraw();
        //生成订单
        orderService.generate();
        //取消订单
        orderService.cancel();
    }}
  • 结果

Spring框架学习笔记,超详细!!(4)_第10张图片

通过测试可以看到,所有的业务方法都添加了事务控制的代码。

12.6 AOP案例:安全日志

项目开发结束了,已经上线了。运行正常。客户提出了新的需求:凡事在系统中进行修改操作的,删除操作的,新增操作的,都要把这个人记录下来。因为这几个操作是属于危险行为。

【示例】

  • 用户业务类

@Component
//用户业务
public class UserService {
    public void getUser(){
        System.out.println("获取用户信息");
    }
    public void saveUser(){
        System.out.println("保存用户");
    }
    public void deleteUser(){
        System.out.println("删除用户");
    }
    public void modifyUser(){
        System.out.println("修改用户");
    }
}
  • 商品业务类

// 商品业务类
@Component
public class ProductService {
    public void getProduct(){
        System.out.println("获取商品信息");
    }
    public void saveProduct(){
        System.out.println("保存商品");
    }
    public void deleteProduct(){
        System.out.println("删除商品");
    }
    public void modifyProduct(){
        System.out.println("修改商品");
    }
}
  • 使用AOP解决问题:

@Component
@Aspect
public class SecurityAspect {

    @Pointcut("execution(* com.heihei.springaop.log..save*(..))")
    public void savePointcut(){}

    @Pointcut("execution(* com.heihei.springaop.log..delete*(..))")
    public void deletePointcut(){}

    @Pointcut("execution(* com.heihei.springaop.log..modify*(..))")
    public void modifyPointcut(){}

    @Before("savePointcut() || deletePointcut() || modifyPointcut()")
    public void beforeAdvice(JoinPoint joinPoint) {
        System.out.println("XXX操作员正在操作" + joinPoint.getSignature().getName() + "方法");
    }
}
  • 测试类

@Test
    public void testLog() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        UserService userService = applicationContext.getBean(UserService.class);
        ProductService productService = applicationContext.getBean(ProductService.class);

        userService.getUser();
        userService.saveUser();
        userService.deleteUser();
        userService.modifyUser();
        productService.getProduct();
        productService.saveProduct();
        productService.deleteProduct();
        productService.modifyProduct();
    }
  • 结果

Spring框架学习笔记,超详细!!(4)_第11张图片

13. Spring对事务的支持

13.1 事务概述

  • 什么是事务

  • 在一个业务流程当中,通常需要多条DML(insert delete update)语句共同联合才能完成,这多条DML语句必须同时成功,或者同时失败,这样才能保证数据的安全。

  • 多条DML要么同时成功,要么同时失败,这叫做事务。

  • 事务:Transaction(tx)

  • 事务的四个处理过程:

  • 第一步:开启事务(strat transaction)

  • 第二步:执行核心业务代码

  • 第三步:提交事务(如果核心业务中没有出现异常)commit transaction

  • 第四步:回滚事务(如果核心业务中出现异常)rollback transaction

  • 事务的四大特性

  • A原子性:事务是最小的工作单元,不可再分

  • C一致性:事务要求要么同时成功,要么同时失败。事务前和事务后的总量不变

  • I隔离性:事务和事务之间因为有隔离性,才可以保证互不干扰

  • D持久性:持久性是事务结束的标志

  • Spring事务作用

  • 在数据层或业务层保障一系列的数据库操作同时成功或者同时失败

13.2 需求和分析

  • 需求:实现任意两个账户间转账操作

  • 需求微缩:A账户减钱,B账户加钱

  • 分析:

①:数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)

②:业务层提供转账操作(transfer),调用减钱与加钱的操作

③:提供2个账号和操作金额执行转账操作

④:基于Spring整合MyBatis环境搭建上述操作

  • 结果分析:

①:程序正常执行时,账户金额A减B加,没有问题

②:程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败

13.3 引入事务场景

【前置工作】

public interface AccountDao {

    @Update("update tbl_account set money = money + #{money} where name = #{name}")
    void inMoney(@Param("name") String name, @Param("money") Double money);

    @Update("update tbl_account set money = money - #{money} where name = #{name}")
    void outMoney(@Param("name") String name, @Param("money") Double money);
}

public interface AccountService {
    /**
     * 转账操作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    public void transfer(String out,String in ,Double money) ;
}

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;

    public void transfer(String out,String in ,Double money) {
        accountDao.outMoney(out,money);
        int i = 1/0;
        accountDao.inMoney(in,money);
    }
}

【第一步】在业务层接口上添加Spring事务管理

public interface AccountService {
    //配置当前接口方法具有事务
    @Transactional
    public void transfer(String out,String in ,Double money) ;
}

注意事项:

  1. Spring注解式事务通常添加在业务层接口而不会添加到业务层实现类上,降低耦合

  1. 注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口的所有方法开启事务

【第二步】设置事务管理器(将事务管理器添加到IoC容器中)

//配置事务管理器,mybatis使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
    DataSourceTransactionManager dtm = new DataSourceTransactionManager();
    transactionManager.setDataSource(dataSource);
    return transactionManager;
}

注意事项:

  1. 事务管理器要根据实现技术进行选择

  1. MyBatis框架使用的是JDBC事务

【第三步】开启注解式事务驱动

@Configuration
@ComponentScan("com.heihei")
@PropertySource("classpath:jdbc.properties")         //加载配置文件
@Import({JdbcConfig.class, MybatisConfig.class})     //导入JDBC类
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}

【第四步】运行测试类

@RunWith(SpringJUnit4ClassRunner.class)                   //设定类运行器
@ContextConfiguration(classes = SpringConfig.class)       //spring环境配置
public class AccountServiceTest {

    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer() throws IOException {
        accountService.transfer("Tom","Jerry",100D);
    }
}

13.4 Spring对事务的支持

13.4.1 Spring事务管理API

Spring对事务的管理底层实现方式是基于AOP实现的,采用AOP的方式进行了封装,核心接口是PlatformTransactionManager:

Spring框架学习笔记,超详细!!(4)_第12张图片
13.4.2 Spring事务角色
  • 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法

  • 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法

Spring框架学习笔记,超详细!!(4)_第13张图片

13.5 事务的属性

13.5.1 事务配置

属性

作用

示例

readOnly

设置是否为只读业务

readOnly=true 只读业务

timeout

设置事务超时时间

timeout=-1 永不超时

rollbackFor

设置事务回滚异常(class)

rollbackFor={NullPointException.class}

rollbackForClassName

设置事务回滚异常(String)

同上格式为字符串

noRollbackFor

设置事务不回滚异常(class)

noRollbackFor={NullPointException.class}

noRollbackForClassName

设置事务不回滚异常(String)

同上格式为字符串

isolation

设置事务隔离级别

isolation=Isolation.DEFAULT

propagation

设置事务传播行为

......

说明:对于RuntimeException类型异常或者Error错误,Spring事务能够进行回滚操作。但是对于编译器异常,Spring事务是不进行回滚的,所以需要使用rollbackFor来设置要回滚的异常。
13.5.2 事务的传播行为

什么是事务的传播行为?

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

事务传播行为在spring框架中被定义为枚举类型

一共有七种传播行为:

  • REQUIRED:支持当前事务,如果不存在就新建一个(默认)**==【没有就新建,有就加入】==**

  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行**==【有就加入,没有就不管了】==**

  • MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常**==【有就加入,没有就抛异常】==**

  • REQUIRES_NEW:开启一个新事务,如果一个事务已经存在,则将这个事务挂起**==【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前的事务被挂起】==**

  • NOT_SUPPORTED:以非事务方式运行,如有事务存在,挂起当前事务**==【不支持事务,存在就挂起】==**

  • NEVER:以非事务方式运行,如有事务存在,抛出异常**==【不支持事务,存在就抛异常】==**

  • NESTED:如果当前正有一个事务正在进行,则该方法应当运行在一个嵌套式事务中,被嵌套的事物可以独立运行于外层事务进行提交或者回滚。如果外层事务不存在,行为就像REQUIRED一样**==【有事务的话,就在这个是物理再嵌套一个完全独立的事务,嵌套的事务可以独立提交和回滚。没有事务就和REQUIRED一样】==**

测试传播行为:

  • 1号service

@Transactional(propagation = Propagation.REQUIRED)
public void save(Account act) {

    // 这里调用dao的insert方法。
    accountDao.insert(act); // 保存act-003账户

    // 创建账户对象
    Account act2 = new Account("act-004", 1000.0);
    try {
        accountService.save(act2); // 保存act-004账户
    } catch (Exception e) {

    }
    // 继续往后进行我当前1号事务自己的事。
}
  • 2号service

@Override
//@Transactional(propagation = Propagation.REQUIRED)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(Account act) {
    accountDao.insert(act);
    // 模拟异常
    String s = null;
    s.toString();

    // 事儿没有处理完,这个大括号当中的后续也许还有其他的DML语句。
}
13.5.3 事务隔离级别

事务隔离级别类似于教室A和教室B之间的那道墙,隔离级别越高表示墙体越厚。隔音效果越好。

数据库中读取数据存在的三大问题:

  • 脏读:读取到没有提交到数据库的数据

  • 不可重复读:在同一个事务中,第一次和第二次读取的数据不一样

  • 幻读:读到的数据是假的

事务隔离级别包括四个级别:

  • 读未提交:READ_UNCOMMITTED

  • 这种隔离级别,存在脏读问题,所谓的脏读表示能够读取到其他食物未提交的数据

  • 读提交:READ_COMMITTED

  • 解决的脏读的问题,其他事务提交之后才能读到,但是存在不可重复读问题

  • 可重复读:REPEATABLE_READ

  • 解决了不可重复读,可以达到可重复读效果,只要当前事务不结束,读取到的数据一直都是一样的。但存在幻读问题

  • 序列化:SERIALIZABLE

  • 解决了幻读问题,事务排队执行。不支持并发

隔离级别

脏读

不可重复读

幻读

读未提交

读提交

可重复读

序列化

【测试】一个service负责插入,一个service负责查询。负责插入的service要模拟延迟。

@Service("i1")
public class IsolationService1 {

    @Resource(name = "accountDao")
    private AccountDao accountDao;

    // 1号
    // 负责查询
    // 当前事务可以读取到别的事务没有提交的数据。
    //@Transactional(isolation = Isolation.READ_UNCOMMITTED)
    // 对方事务提交之后的数据我才能读取到。
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void getByActno(String actno) {
        Account account = accountDao.selectByActno(actno);
        System.out.println("查询到的账户信息:" + account);
    }
}

@Service("i2")
public class IsolationService2 {

    @Resource(name = "accountDao")
    private AccountDao accountDao;

    // 2号
    // 负责insert
    @Transactional
    public void save(Account act) {
        accountDao.insert(act);
        // 睡眠一会
        try {
            Thread.sleep(1000 * 20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

【测试程序】

@Test
public void testIsolation1(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    IsolationService1 i1 = applicationContext.getBean("i1", IsolationService1.class);
    i1.getByActno("act-004");
}

@Test
public void testIsolation2(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    IsolationService2 i2 = applicationContext.getBean("i2", IsolationService2.class);
    Account act = new Account("act-004", 1000.0);
    i2.save(act);
}

通过执行结果可以清晰的看出隔离级别不同,执行效果不同。

13.5.4 事务超时

代码:设置事务的超时时间为10秒。

@Transactional(timeout = 10)

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

默认值是-1,表示没有时间限制

这里有个坑,事务的超时时间指的是哪段时间?

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

  • 以下代码的超时不会被计入超时时间

@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
    accountDao.insert(act);   //最后一条DML语句
    // 睡眠一会
    try {
        Thread.sleep(1000 * 15);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
  • 以下代码的超时会被计入超时时间

@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
    // 睡眠一会
    try {
        Thread.sleep(1000 * 15);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    accountDao.insert(act);    //最后一条DML语句
}

当然,如果想让整个方法的所有代码都计入超时时间的话,可以在方法最后一行添加一行无关紧要的DML语句。

13.5.5 只读事务
@Transactional(readOnly = true)

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

该特性的作用是:启动spring的优化策略。提高select语句执行效率。

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

13.5.6 设置哪些异常回滚事务
@Transactional(rollbackFor = RuntimeException.class)

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

13.5.7 设置哪些异常不回滚事务
@Transactional(noRollbackFor = NullPointerException.class)

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

你可能感兴趣的:(Spring框架学习笔记,学习,java,开发语言,spring,后端)