当前位置: 首页 > article >正文

Spring02 - 代理和事务篇

Spring入门(中)-代理和事务篇

文章目录

  • Spring入门(中)-代理和事务篇
    • 一:AOP为什么需要代理
    • 二:代理模式
      • 0:代理三要素
      • 1:静态代理及其问题
      • 2:动态代理
        • 2.1:开发过程
          • 2.1.1:引入对应的依赖
          • 2.1.2:创建原始目标类
          • 2.1.3:编写额外功能
          • 2.1.4:定义切入点并整合代理
        • 2.2:开发细节
        • 2.3:MethodInterceptor
      • 3:切入点pointcut详解
        • 3.1:切点标志符
        • 3.2:常见的切点表达式
    • 三:AOP编程
      • 1:相关术语
        • 1.1:join point
        • 1.2:Advice
        • 1.3:point cut
        • 1.4:Aspect
        • 1.5:Target
      • 2:底层实现
        • 2.1:JDK动态代理
        • 2.2:CGlib动态代理
        • 2.3:总结对比
      • 3:Spring AOP
        • 3.1:SpringAOP中CGlib代理的实现
        • 3.2:SpringAOP中JDK代理的实现
        • 3.3:Spring AOP默认使用CGlib
      • 4:基于AspectJ注解的AOP编程
        • 4.1:AspectJ注解编程
        • 4.2:Spring AOP & AspectJ AOP
      • 5:AOP总结
    • 四:持久层事务
      • 1:事务简介
        • 1.1:介绍
        • 1.2:事务特点
        • 1.3:事务的实现方式
          • 1.3.1:MySQL
          • 1.3.2:SpringBoot
      • 2:@Transactional详解
        • 2.1:基本原理
        • 2.2:@Transactional常用配置
        • 2.3:事务传播行为(7种)
        • 2.4:事务5种隔离级别
      • 3:事务使用事项与场景
        • 3.1:事务使用注意事项
        • 3.2:事务使用场景
          • 3.2.1:自动回滚
          • 3.2.2:手动回滚
          • 3.2.3:回滚部分异常
          • 3.2.4:手动创建、提交、回滚
        • 3.3:事务其他情况
          • 3.3.1:事务提交方式
          • 3.3.2:事务并发经典情况

一:AOP为什么需要代理

在J2EE开发过程中最为重要的便是业务层Service,而在业务层中又分为额外功能和核心功能

  • 额外功能:事务,日志,性能,代码量小且和业务无关
  • 核心业务:业务运算逻辑 + DAO操作

在这里插入图片描述

以现实中租房为例说一下引入代理【中介】的好处 -> 代理类能帮助我们实现额外功能,而具体的房东Service只需要关注核心业务出租房屋

在这里插入图片描述

二:代理模式

所谓代理模式就是通过代理类,为原始类(目标)增加格外的功能,这样有利于目标类更好更专注的维护核心业务而不用关系额外功能

0:代理三要素

代理开发的三个要素是 = 原始目标类 + 额外功能 + 和原始目标类实现相同的接口

在这里插入图片描述

1:静态代理及其问题

所谓静态代理就是为每一个原始类,手工编写对应的静态代理方法。

userServiceImpl -> UserServiceProxy;AImpl -> AProxy

静态代理有如下两个问题:

  • 因为每一个目标类都要写一个静态代理方法,导致静态代理文件过多,不利于项目的管理
  • 同时使用静态代理额外的功能维护性差,额外功能修改复杂

2:动态代理

动态代理和静态代理在概念上没有任何的区别,和静态代理的区别就是开发流程和底层原理的不同

2.1:开发过程
2.1.1:引入对应的依赖
<!-- dynamic proxy -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.1.14.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.8</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.3</version>
</dependency>
2.1.2:创建原始目标类
/**
 * @author cui haida
 * @date 2024/01/07/9:55
 */
public class UserServiceImpl implements UserService {
    @Override
    public void register(User user) {
        System.out.println("这是register核心业务功能 + dao");
    }

    @Override
    public void login(String name, String password) {
        System.out.println("这是login核心业务功能 + dao");
    }
}
<bean id="userService" class="org.example.proxy.UserServiceImpl"/>
2.1.3:编写额外功能
/**
 * <p>
 * 功能描述:前置代理
 * </p>
 *
 * @author cui haida
 * @date 2024/01/07/9:58
 */
public class Before implements MethodBeforeAdvice {
    /**
     * 需要将运行在原始方法执行之前的额外功能书写在before中
     * method -> 额外功能所增加给的那个原始方法,例如login, register
     * objects -> 额外功能所增加给的那个原始方法的参数数组
     * target -> 额外功能所增加给的那个原始对象
     */
    @Override
    public void before(Method method, Object[] objects, Object target) throws Throwable {
        System.out.println("this is a before aop log");
    }
}
<bean id="before" class="org.example.proxy.dynamic.Before"/>
2.1.4:定义切入点并整合代理

有程序员根据需要决定额外功能加入给原始的哪个原始方法

<!-- config point cut -->
<aop:config>
    <aop:pointcut id="pc" expression="execution(* *(..))"/>
    <!-- 整合额外功能 -->
    <aop:advisor advice-ref="before" pointcut-ref="pc"/>
</aop:config>
2.2:开发细节
  1. 动态代理类是在Spring框架在运行时,通过动态字节码技术[CGlib],在JVM创建的,运行在JVM内部,等程序结束时一起消失

  2. 所谓动态字节码技术就是:通过第三方动态字节码框架,在JVM中创建对应类的对应的动态字节码,进而创建独享,当虚拟机结束时,动态字节码跟着一起消失,所以动态代理不用定义类文件,都是JVM在运行时期动态创建的,所以不会造成静态代理类文件数量过多影响项目管理的问题

在这里插入图片描述

  1. 动态代理类会简化代理的开发,在额外功能不改变的情况下,创建其他的目标类的代理对象的时候, 只需要指定原始的对象即可
<!-- config point cut -->
<aop:config>
    <aop:pointcut id="pc" expression="execution(* *(..))"/>
    <!-- 整合额外功能 -->
    <aop:advisor advice-ref="before" pointcut-ref="pc"/>
</aop:config>
  1. 动态代理开发的四个步骤:目标对象 -> 额外功能书写 -> 切入点声明 -> 组装切入点和额外功能
2.3:MethodInterceptor

MethodBeforeAdvice的问题是只能进行前置的额外功能实现,因此使用范围不是很广泛

Spring提供了MethodInterceptor可以进行前后额外功能

在编写中只需要实现MethodInterceptor接口然后实现invoke方法就可以了

package org.example.proxy.dynamic;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

/**
 * <p>
 * 功能描述:方法拦截器示例
 * </p>
 *
 * @author cui haida
 * @date 2024/01/07/12:01
 */
public class Around implements MethodInterceptor {
    /**
     * 方法作用:额外功能书写在invoke方法中,对于原始方法的运行,可以使用methodInvocation.proceed()
     * @param methodInvocation 额外功能所增加给的那个原始的方法
     * @return 原始方法的返回值
     * @throws Throwable 异常
     */
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        try {
            // ----- 前置操作 -----
            System.out.println("this is before log");
            // 方法本身的调用
            Object res = methodInvocation.proceed();
            // ----- 后置操作 -----
            System.out.println("this is after log");
            return res;
        } catch(Exception e) {
            System.out.println("this is error log");
            return null;
        }
    }
}
<bean id="around" class="org.example.proxy.dynamic.Around"/>

<!-- config point cut -->
<aop:config>
    <aop:pointcut id="pc" expression="execution(* *(..))"/>
    <!-- 整合额外功能,其实就是指定切入点的范围为around -->
    <aop:advisor advice-ref="around" pointcut-ref="pc"/>
</aop:config>

🎉 原始方法的返回值直接作为invoke方法的返回值,不会影响原始方法的返回值,如果想要改变,就不要直接返回原始方法proceed的运行结果

3:切入点pointcut详解

一个 pointcut 的声明由两部分组成:

  • 一个方法签名, 包括方法名和相关参数
  • 一个 pointcut 表达式, 用来指定哪些方法执行是我们感兴趣的(即因此可以织入 advice).

在这里插入图片描述

上述操作参数各个意义:

  • 第一个* -> 表示修饰符和返回值,如果是*表示任意修饰符和返回值
  • com.xys.service.UserService.* -> 表示关注的方法是 com.xys.service.UserService类下的所有的方法
  • (..) -> 表示的是参数表,..表示对参数没有任何的要求,什么类型都行,有没有都行

🎉 execution(* *(..))表示对任何都没有要求,也就是切入所有的方法

在这里插入图片描述

3.1:切点标志符

execution

匹配 join point 的执行, 例如 “execution(* hello(…))” 表示匹配所有目标类中的 hello() 方法

execution是最基本的 pointcut 标志符.

within

匹配特定包下的所有 join point例如

  • within(com.xys.*) 表示 com.xys 包中的所有连接点, 即包中的所有类的所有方法.
  • within(com.xys.service.*Service) 表示在 com.xys.service 包中所有以 Service 结尾的类的所有的连接点.

bean

匹配 bean 名字为指定值的 bean 下的所有方法, 例如:

bean(*Service) // 匹配名字后缀为 Service 的 bean 下的所有方法
bean(myService) // 匹配名字为 myService 的 bean 下的所有方法

args

匹配参数满足要求的的方法.例如:

@Pointcut("within(com.xys.demo2.*)")
public void pointcut2() {
}

@Before(value = "pointcut2()  &&  args(name)")
public void doSomething(String name) {
    logger.info("---page: {}---", name);
}
@Service
public class NormalService {
    private Logger logger = LoggerFactory.getLogger(getClass());
    public void someMethod() {
        logger.info("---NormalService: someMethod invoked---");
    }
    
    // 当 NormalService.test 执行时, 则 advice doSomething 就会执行
    // test 方法的参数 name 就会传递到 doSomething 中.
    public String test(String name) {
        logger.info("---NormalService: test invoked---");
        return "服务一切正常";
    }
}

@annotation

匹配由指定注解所标注的方法, 例如:

// 则匹配由注解 AuthChecker 所标注的方法.
@Pointcut("@annotation(com.xys.demo1.AuthChecker)")
public void pointcut() {
}
3.2:常见的切点表达式

匹配方法签名

// 匹配指定包中的所有的方法
execution(* com.xys.service.*(..))
    
// 匹配当前包中的指定类的所有方法
execution(* UserService.*(..))
    
// 匹配指定包中的所有 public 方法
execution(public * com.xys.service.*(..))
    
// 匹配指定包中的所有 public 方法, 并且返回值是 int 类型的方法
execution(public int com.xys.service.*(..))
    
// 匹配指定包中的所有 public 方法, 并且第一个参数是 String, 返回值是 int 类型的方法
execution(public int com.xys.service.*(String name, ..))

匹配类型签名

// 匹配指定包中的所有的方法, 但不包括子包
within(com.xys.service.*)
    
// 匹配指定包中的所有的方法, 包括子包
within(com.xys.service..*)
    
// 匹配当前包中的指定类中的方法
within(UserService)
    
// 匹配一个接口的所有实现类中的实现的方法
within(UserDao+)

切点表达式组合

// 匹配以 Service 或 ServiceImpl 结尾的 bean
bean(*Service || *ServiceImpl)
    
// 匹配名字以 Service 结尾, 并且在包 com.xys.service 中的 bean
bean(*Service) && within(com.xys.service.*)

三:AOP编程

AOP(Aspect-Oriented Programming), 即 面向切面【切面 = 切入点 + 额外功能】编程, 它与 OOP( 面向对象编程) 相辅相成

AOP 提供了与 OOP 不同的抽象软件结构的视角, 在 OOP 中, 我们以类作为我们的基本单元, 而 AOP 中的基本单元是Aspect(切面)

1:相关术语

1.1:join point

(连接点,所有候选方法)

程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理.

在 Spring AOP 中, join point 总是方法的执行点, 即只有方法连接点.

1.2:Advice

(增强,操作)

由 aspect 添加到特定的 join point(即满足 point cut 规则的 join point) 的一段代码

许多 AOP 框架, 会将 advice 模拟为一个拦截器(interceptor), 并且在 join point 上维护多个 advice, 进行层层拦截

例如 HTTP 鉴权的实现, 我们可以为每个使用 RequestMapping 标注的方法织入 advice

当 HTTP 请求到来时, 首先进入到 advice 代码中, 在这里我们可以分析这个 HTTP 请求是否有相应的权限

  • 如果有, 则执行 Controller
  • 如果没有, 则抛出异常

这里的 advice 就扮演着鉴权拦截器的角色了

advice的类型:

  • before advice, 在 join point 前被执行的 advice.
    • 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常
  • after return advice, 在一个 join point 正常返回后执行的 advice
  • after throwing advice, 当一个 join point 抛出异常后执行的 advice
  • after(final) advice, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice.
  • around advice, 在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice.
1.3:point cut

(切点,规则特征)

point cut 是匹配 join point 的谓词,就是通过point cut能找到对应的 join point

Advice 是和特定的 point cut 关联的, 并且在 point cut 相匹配的 join point 中执行

在 Spring 中, 所有的方法都可以认为是 joinpoint, 但是我们并不希望在所有的方法上都添加 Advice

pointcut 的作用就是提供一组规则来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice.

1.4:Aspect

(切面,point cut + advice)

aspect 由 point cut 和 advice 组成, 它既包含了横切逻辑的定义, 也包括了连接点的匹配规则定义

Spring AOP 就是负责实施切面的框架, 它将切面所定义的横切逻辑织入到切面所指定的连接点中

在这里插入图片描述

AOP 的工作重心在于如何将增强织入目标对象的连接点上, 这里包含两个工作:

  1. 如何通过 pointcut(切点) 和 advice(增强)定位到特定的 joinpoint(连接点) 上
  2. 如何在 advice(增强) 中编写切面代码.

🎉 可以简单地认为, 使用 @Aspect 注解的类就是切面【advice增强代码】

1.5:Target

(目标对象)

织入 advice 的目标对象,目标对象也被称为 advised object.

因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 adviced object 总是一个代理对象(proxied object)

🎉 adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类.

2:底层实现

AOP底层实现有两大核心问题:

  • AOP如何创建动态代理类(动态字节码技术)
  • Spring工厂如何加工代理对象
2.1:JDK动态代理

JDK动态代理是有JDK提供的工具类Proxy实现的,动态代理类是在运行时生成指定接口的代理类

每个代理实例都有一个关联的调用处理程序对象,此对象实现了InvocationHandler

最终的业务逻辑是在InvocationHandler实现类的invoke方法上

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

这三个参数都是什么意思呢。首先再次强调代理开发的三个要素是 = 原始目标类 + 额外功能 + 和原始目标类实现相同的接口

其中原始目标类,需要我们自己声明

而额外功能需要我们通过第三个参数InvocationHandler h来完成, 这个接口里有一个invoke方法,这个方法可以通过来实现额外功能的工作

在这里插入图片描述

第三个要求和原始目标类实现相同的接口需要我们通过第二个参数完成

而第一个参数classLoader就是借用一个加载器,创建代理类的一个class对象,进而可以创建代理对象【动态字节码】

在这里插入图片描述

package proxy;

import service.JdkUserServiceImpl;
import service.UserService;

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

/**
 * @author cuihaida
 */
public class JdkUserLogProxy {
    /**
     * 代理目标
     */
    private UserService target;

    /**
     * init
     * @param target 代理目标
     */
    public JdkUserLogProxy(JdkUserServiceImpl target) {
        super();
        this.target = target;
    }

    public UserService getLoggingProxy() {
        UserService proxy;
        ClassLoader loader = target.getClass().getClassLoader(); // 借用获取类加载器
        Class[] interfaces = new Class[]{UserService.class}; // 实现相同的接口
        
        InvocationHandler h = new InvocationHandler() {
            /**
             * proxy: 代理对象。 一般不使用该对象 
             * method: 正在被调用的方法 
             * args: 调用方法传入的参数
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                String methodName = method.getName();
                // log - before method
                System.out.println("[before] execute method: " + methodName);
                // call method
                Object result = null;
                try {
                    // 前置通知
                    result = method.invoke(target, args);
                    // 返回通知, 可以访问到方法的返回值
                } catch (NullPointerException e) {
                    e.printStackTrace();
                    // 异常通知, 可以访问到方法出现的异常
                }
                // 后置通知. 因为方法可以能会出异常, 所以访问不到方法的返回值
                // log - after method
                System.out.println("[after] execute method: " + methodName + ", return value: " + result);
                return result;
            }
        };
        proxy = (UserService) Proxy.newProxyInstance(loader, interfaces, h);
        return proxy;
    }
}

JDK代理自动生成的class是由sun.misc.ProxyGenerator来生成的

/**
    * Generate a proxy class given a name and a list of proxy interfaces.
    *
    * @param name        the class name of the proxy class
    * @param interfaces  proxy interfaces
    * @param accessFlags access flags of the proxy class
*/
public static byte[] generateProxyClass(final String name, Class<?>[] interfaces, int accessFlags)
{
    ProxyGenerator gen = new ProxyGenerator(name, interfaces, accessFlags);
    final byte[] classFile = gen.generateClassFile();
    ...
}

generateClassFile()的实现

一共三个步骤(把大象装进冰箱分几步?):

  • 第一步:(把冰箱门打开)准备工作,将所有方法包装成ProxyMethod对象
  • 第二步:(把大象装进去)为代理类组装字段,构造函数,方法,static初始化块等
  • 第三步:(把冰箱门带上)写入class文件

从生成的Proxy代码看执行流程(了解)

从上述sun.misc.ProxyGenerator类中可以看到,这个类里面有一个配置参数sun.misc.ProxyGenerator.saveGeneratedFiles

可以通过这个参数将生成的Proxy类保存在本地,比如设置为true 执行后,生成的文件如下:

在这里插入图片描述

在这里插入图片描述

主要流程是:

  • ProxyGenerator创建Proxy的具体类$Proxy0
  • 由static初始化块初始化接口方法:2个IUserService接口中的方法,3个Object中的接口方法
  • 由构造函数注入InvocationHandler
  • 执行的时候,通过ProxyGenerator创建的Proxy,调用InvocationHandler的invoke方法,执行我们自定义的invoke方法
2.2:CGlib动态代理

然而,有些目标类没有对应的接口,这样就不能通过JDK动态代理来实现了,此时可以使用CGlib动态代理来解决这类目标类的代理问题

CGlib的核心原理就是通过创建目标类的子类来保证和目标类保持相同的方法,依次来完成动态代理

在这里插入图片描述

  • 最底层是字节码
  • ASM是操作字节码的工具
  • cglib基于ASM字节码工具操作字节码(即动态生成代理,对方法进行增强)
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

而cglib实现的代理核心方法是:Enhancer enhancer = new Enhancer();

package proxy;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class UserLogProxy implements MethodInterceptor {

    /**
     * 业务类对象,供代理方法中进行真正的业务方法调用
     */
    private Object target;

    public Object getUserLogProxy(Object target) {
        // 给业务对象进行赋值
        this.target = target;
        // 创建加强器,用来创建动态代理类
        Enhancer enhancer = new Enhancer();
        // 为加强器指定要代理的业务类(即:为下面生成的代理类指定父类)
        enhancer.setSuperclass(this.target.getClass()); 
        //设置回调:对于代理类上所有方法的调用,都会调用CallBack,而Callback则需要实现intercept()方法进行拦截
        enhancer.setCallback(this); // 对比于JDK动态代理的Invokationhandle
        // 创建动态代理类对象并返回
        return enhancer.create();
    }

    /**
     * 实现回调方法
     *
     * @param o           方法
     * @param method      方法元信息
     * @param objects     方法参数
     * @param methodProxy 代理方法,将使用这个的invokeSuper()调用方法
     * @return object
     * @throws Throwable 异常
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // log - before method
        System.out.println("[before] execute method: " + method.getName());
        // call method
        Object result = methodProxy.invokeSuper(o, objects);
        // log - after method
        System.out.println("[after] execute method: " + method.getName() + ", return value: " + result);
        return result;
    }
}

在这里插入图片描述

2.3:总结对比

JDK 动态代理主要是针对类实现了某个接口,AOP 则会使用 JDK 动态代理。

JDK 动态代理基于反射的机制实现,生成一个实现同样接口的一个代理类,然后通过重写方法的方式,实现对代码的增强。

而如果某个类没有实现接口,AOP 则会使用 CGLIB 代理。

CGlib代理的底层原理是基于 asm 第三方框架,通过修改字节码生成成成一个子类,然后重写父类的方法,实现对代码的增强。

3:Spring AOP

3.1:SpringAOP中CGlib代理的实现

SpringAOP封装了cglib,通过其进行动态代理的创建

CglibAopProxy的getProxy方法

@Override
public Object getProxy() {
  return getProxy(null);
}

@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
  if (logger.isTraceEnabled()) {
    logger.trace("Creating CGLIB proxy: " + this.advised.getTargetSource());
  }

  try {
    Class<?> rootClass = this.advised.getTargetClass();
    Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");

    // 上面流程图中的目标类(rootClass)
    Class<?> proxySuperClass = rootClass;
    if (rootClass.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) {
      proxySuperClass = rootClass.getSuperclass();
      Class<?>[] additionalInterfaces = rootClass.getInterfaces();
      for (Class<?> additionalInterface : additionalInterfaces) {
        this.advised.addInterface(additionalInterface);
      }
    }

    // Validate the class, writing log messages as necessary.
    validateClassIfNecessary(proxySuperClass, classLoader);

    // 重点看这里,设置各种参数来构建
    Enhancer enhancer = createEnhancer();
    if (classLoader != null) {
      enhancer.setClassLoader(classLoader);
      if (classLoader instanceof SmartClassLoader &&
          ((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
        enhancer.setUseCache(false);
      }
    }
    enhancer.setSuperclass(proxySuperClass);
    enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
    enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
    enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));

    // 设置callback回调接口,即方法的增强点
    Callback[] callbacks = getCallbacks(rootClass);
    Class<?>[] types = new Class<?>[callbacks.length];
    for (int x = 0; x < types.length; x++) {
      types[x] = callbacks[x].getClass();
    }
    // filter
    enhancer.setCallbackFilter(new ProxyCallbackFilter(
        this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
    enhancer.setCallbackTypes(types);

    // 重点:创建proxy和其实例
    return createProxyClassAndInstance(enhancer, callbacks);
  }
  catch (CodeGenerationException | IllegalArgumentException ex) {
    throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
        ": Common causes of this problem include using a final class or a non-visible class",
        ex);
  }
  catch (Throwable ex) {
    // TargetSource.getTarget() failed
    throw new AopConfigException("Unexpected AOP exception", ex);
  }
}
3.2:SpringAOP中JDK代理的实现

Spring AOP JDK代理的创建

代理的创建比较简单,调用getProxy方法

然后直接调用JDK中Proxy.newProxyInstance()方法将classloader和被代理的接口方法传入即可

@Override
public Object getProxy() {
    return getProxy(ClassUtils.getDefaultClassLoader());
}

@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
    if (logger.isTraceEnabled()) {
        logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
    }
    return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this);
}

Spring AOP JDK代理的执行

/**
    * Implementation of {@code InvocationHandler.invoke}.
    * <p>Callers will see exactly the exception thrown by the target,
    * unless a hook method throws an exception.
    */
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object oldProxy = null;
    boolean setProxyContext = false;

    TargetSource targetSource = this.advised.targetSource;
    Object target = null;

    try {
        // 执行的是equal方法
        if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
            // The target does not implement the equals(Object) method itself.
            return equals(args[0]);
        }
        // 执行的是hashcode方法
        else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
            // The target does not implement the hashCode() method itself.
            return hashCode();
        }
        // 如果是包装类,则dispatch to proxy config
        else if (method.getDeclaringClass() == DecoratingProxy.class) {
            // There is only getDecoratedClass() declared -> dispatch to proxy config.
            return AopProxyUtils.ultimateTargetClass(this.advised);
        }
        // 用反射方式来执行切点
        else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
                method.getDeclaringClass().isAssignableFrom(Advised.class)) {
            // Service invocations on ProxyConfig with the proxy config...
            return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
        }

        Object retVal;

        if (this.advised.exposeProxy) {
            // Make invocation available if necessary.
            oldProxy = AopContext.setCurrentProxy(proxy);
            setProxyContext = true;
        }

        // Get as late as possible to minimize the time we "own" the target,
        // in case it comes from a pool.
        target = targetSource.getTarget();
        Class<?> targetClass = (target != null ? target.getClass() : null);

        // 获取拦截链
        List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

        // Check whether we have any advice. If we don't, we can fallback on direct
        // reflective invocation of the target, and avoid creating a MethodInvocation.
        if (chain.isEmpty()) {
            // We can skip creating a MethodInvocation: just invoke the target directly
            // Note that the final invoker must be an InvokerInterceptor so we know it does
            // nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
            Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
            retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
        }
        else {
            // We need to create a method invocation...
            MethodInvocation invocation =
                    new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
            // Proceed to the joinpoint through the interceptor chain.
            retVal = invocation.proceed();
        }

        // Massage return value if necessary.
        Class<?> returnType = method.getReturnType();
        if (retVal != null && retVal == target &&
                returnType != Object.class && returnType.isInstance(proxy) &&
                !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
            // Special case: it returned "this" and the return type of the method
            // is type-compatible. Note that we can't help if the target sets
            // a reference to itself in another returned object.
            retVal = proxy;
        }
        else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
            throw new AopInvocationException(
                    "Null return value from advice does not match primitive return type for: " + method);
        }
        return retVal;
    }
    finally {
        if (target != null && !targetSource.isStatic()) {
            // Must have come from TargetSource.
            targetSource.releaseTarget(target);
        }
        if (setProxyContext) {
            // Restore old proxy.
            AopContext.setCurrentProxy(oldProxy);
        }
    }
}
3.3:Spring AOP默认使用CGlib
  • 性能和速度:CGlib动态代理在性能上通常比标准的JDK动态代理更加的快,因为直接通过生成子类来实现代理,避免了反射操作
  • 无需接口:JDK动态代理必须实现一个接口,而CGlib动态代理不需要
  • 无侵入性:因为无需实现任何接口或者继承特定的类,从而减少了对源代码的侵入性
  • 方便集成:Spring Boot默认提供了Cglib相关的依赖,在应用程序中使用CGlib动态代理十分的方便

4:基于AspectJ注解的AOP编程

4.1:AspectJ注解编程

AspectJ通过切面类定义了额外功能,@Around,定义了切入点@Around("execution(* login(..))"),声明切面类@Aspect

@Aspect
public class MyAspect {
    @Around("execution(* login(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("这是一个环绕注解(上)");
        Object ret = joinPoint.proceed(); // 执行原始方法
        System.out.println("这是一个环绕注解(下)");
        return ret;
    }
}
<bean id="around" class="org.example.aspectj.MyAspect"/>
<!-- 告知Spring基于注解进行AOP编程 -->
<aop:aspectj-autoproxy/>

可以进行切入点的复用:在切面类中定义一个函数,上面@Pointcut注解,通过这种方式,定义切入点表达式,后续更加有利于切入点

@Aspect
public class MyAspect {
    
    @Pointcut("execution(* login(..))")
    public void myPointer() {}
    
    
    @Around("myPointer()")
    public Object around1(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("这是一个环绕注解(上)");
        Object ret = joinPoint.proceed(); // 执行原始方法
        System.out.println("这是一个环绕注解(下)");
        return ret;
    }

    @Around("myPointer()")
    public Object around2(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("这是一个环绕注解(上)");
        Object ret = joinPoint.proceed(); // 执行原始方法
        System.out.println("这是一个环绕注解(下)");
        return ret;
    }
}
4.2:Spring AOP & AspectJ AOP
  • Spring AOP 基于动态代理实现,属于运行时增强。

  • AspectJ AOP 则属于编译时增强

AspectJ AOP 主要有3种方式:

  1. 编译时织入:指的是增强的代码和源代码我们都有,直接使用 AspectJ 编译器编译就行了,编译之后生成一个新的类,他也会作为一个正常的 Java 类装载到JVM。
  2. 编译后织入:指的是代码已经被编译成 class 文件或者已经打成 jar 包,这时候要增强的话,就是编译后织入,比如你想对依赖的第三方的类库增强。
  3. 加载时织入:指的是在 JVM 加载类的时候进行织入。

在这里插入图片描述

总结:

Spring AOP 只能在运行时织入,不需要单独编译,性能相比 AspectJ 编译织入的方式慢

AspectJ 只支持编译前后和类加载时织入,性能更好,功能更加强大。

5:AOP总结

在这里插入图片描述

四:持久层事务

1:事务简介

1.1:介绍

事务,就是一组操作数据库的动作集合。事务是现代数据库理论中的核心概念之一。

如果一组处理步骤或者全部发生或者一步也不执行,我们称该组处理步骤为一个事务。

当所有的步骤像一个操作一样被完整地执行,我们称该事务被提交。

由于其中的一部分或多步执行失败,导致没有步骤被提交,则事务必须回滚到最初的系统状态。

1.2:事务特点

一个ACID事务是一个工作单元,它要保证4个属性:

  • 原子性(Atomicity): 事务『要么全部完成,要么全部取消』,即使它持续运行10个小时。如果事务崩溃,状态回到事务之前(事务回滚)。
  • 一致性(Consistency): 只有合法的数据(依照关系约束和函数约束)能写入数据库,一致性与原子性和隔离性有关。
  • 隔离性(Isolation): 如果2个事务 A 和 B 同时运行,事务 A 和 B 最终的结果是相同的,不管 A 是结束于 B 之前/之后/运行期间。
  • 持久性(Durability): 一旦事务提交(也就是成功执行),不管发生什么(崩溃或者出错),数据要保存在数据库中。

在同一个事务内,你可以运行多个SQL查询来读取、创建、更新和删除数据。当两个事务使用相同的数据,麻烦就来了。

经典的例子是从账户A到账户B的汇款。假设有2个事务:

  • 事务1(T1)从账户A取出100美元给账户B
  • 事务2(T2)从账户A取出50美元给账户B

我们回来看看ACID属性:

  • 原子性确保不管 T1 期间发生什么(服务器崩溃、网络中断…),你不能出现账户A 取走了100美元但没有给账户B 的现象(这就是数据不一致状态)。
  • 隔离性确保如果 T1 和 T2 同时发生,最终A将减少150美元,B将得到150美元,而不是其他结果,比如因为 T2 部分抹除了 T1 的行为,A减少150美元而B只得到50美元
  • 持久性确保如果 T1 刚刚提交,数据库就发生崩溃,T1 不会消失得无影无踪。
  • 一致性确保钱不会在系统内生成或灭失。
1.3:事务的实现方式
1.3.1:MySQL

原子性: -> undoLog

  • 主要依靠undo.log日志实现,即在事务失败时执行回滚。
  • undo.log日志会记录事务执行的sql,当事务需要回滚时,通过反向补偿回滚数据库状态

持久性:-> redoLog

  • 主要依靠redo.log日志实现。
  • 首先,mysql持久化通过缓存来提高效率,即在select时先查缓存,再查磁盘;在update时先更新缓冲,再更新磁盘。
  • 但由于缓存断电就没了,所以需要redo.log日志。
  • 在执行修改操作时,sql会先写入到redo.log日志,再写入缓存中。这样即使断电,也能保证数据不丢失,达到持久性

隔离性:-> 锁 + mvcc

  • 多线程时多事务之间互相产生了影响,要避免这个影响,那就加锁。
  • mysql的锁有表锁,行锁,间隙锁写。写操作通过加锁实现隔离性,渎操作通过MVCC实现

一致性:-> 上面三个为的就是保证一致性

  • 就是事务再执行的前和后数据库的状态都是正常的,表现为没有违反数据完整性,参照完整性和用户自定义完整性等等。
  • 而上面三种特性就是为了保证数据库的有一致性
1.3.2:SpringBoot

Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为编码式和声明式的两种方式。

  • 编程式事务管理:编程式事务管理使用 TransactionTemplate 或者直接使用底层的 PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate

  • 声明式事务管理:建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务

    在这里插入图片描述

    • 一种是在配置文件(xml)中做相关的事务规则声明
    • 另一种是基于 @Transactional 注解的方式。注释配置是目前流行的使用方式,推荐使用

声明式事务管理不需要入侵代码,更快捷而且简单,推荐使用

2:@Transactional详解

2.1:基本原理

在应用系统调用声明了 @Transactional 的目标方法时,Spring Framework 默认使用 AOP 代理

在代码运行时生成一个代理对象,根据 @Transactional 的属性配置信息,这个代理对象决定该声明 @Transactional 的目标方法是否由拦截器 TransactionInterceptor来使用拦截

public class TransactionIntereceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
    public TransactionIntereceptor() {
        
    }
    
    public TransactionInterceptor(TransactionManager ptm, TransactionAttributeSource tas) {
        this.setTransactionManager(ptm);
        this.setTransactionAttributeSource(tas);
    }
}

在 TransactionInterceptor拦截时,会在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑

最后根据执行情况是否出现异常,利用抽象事务管理器 AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务

2.2:@Transactional常用配置
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.transaction.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    String[] label() default {};

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    String timeoutString() default "";

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}
参数名称功能描述
readOnly该属性用于设置当前事务是否为只读事务
设置为true表示只读,false则表示可读写,默认值为false
rollbackFor用于设置需要进行回滚的异常类数组
当方法中抛出指定异常数组中的异常时,则进行事务回滚
例如:指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)
指定多个异常类:@Transactional(rollbackFor={RuntimeException.class,Exception.class})
rollbackForClassName该属性用于设置需要进行回滚的异常类名称数组
当方法中抛出指定异常名称数组中的异常时,则进行事务回滚
例如:@Transactional(rollbackForClassName={"RuntimeException","Exception"})
noRollbackFor该属性用于设置不需要进行回滚的异常类数组
当方法中抛出指定异常数组中的异常时,不进行事务回滚。
noRollbackForClassName该属性用于设置不需要进行回滚的异常类名称数组
当方法中抛出指定异常名称数组中的异常时,不进行事务回滚
propagation该属性用于设置事务的传播行为
isolation设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置
timeout该属性用于设置事务的超时秒数,默认值为-1表示永不超时
2.3:事务传播行为(7种)

就是上述中的propagation属性,常用的三项已经加粗

  • required -> 需要事务(默认) -> 没有新建,有则加入
  • required_new -> 有没有自己都新建一个
  • nested -> 嵌套事务
事务的传播行为类型说明
PROPAGATION_REQUIRED需要事务(默认)。
若当前无事务,新建一个事务;若当前有事务,加入此事务中
PROPAGATION_SUPPORTS支持事务。
若当前没有事务,以非事务方式执行;若当前有事务,加入此事务中
PROPAGATION_MANDATORY强制使用事务。
若当前有事务,就使用当前事务;
若当前没有事务,抛出IllegalTransactionStateException异常
PROPAGATION_REQUIRES_NEW新建事务。
无论当前是否有事务,都新建事务运行
PROPAGATION_NOT_SUPPORTED不支持事务。
若当前存在事务,把当前事务挂起,然后运行方法
PROPAGATION_NEVER不使用事务。
若当前方法存在事务,则抛出IllegalTransactionStateException异常
PROPAGATION_NESTED嵌套。
如果当前存在事务,则在嵌套事务内执行;
如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作
2.4:事务5种隔离级别

这个在MySQL部分已经说过了,就是RU, RC, RR, SERIALIZABLE

隔离级别含义
DEFAULT这是一个PlatfromTransactionManager默认的隔离级别
使用数据库默认的事务隔离级别另外四个与JDBC的隔离级别相对应
READ_UNCOMMITTED读未提交,最低的隔离级别,只能解决脏写问题【写排它锁】
READ_COMMITTED读已提交,可以解决脏写【写排它锁】和脏读问题【MVCC】
但不能解决不可重复读【每一次select都会生成新的readView导致的】
REPEATABLE_READ可重复读
可以解决脏写【写排它锁】和脏读问题【MVCC】和不可重复读问题【一直用第一次select生成的readView进行快照读,保证同一事务中每一次读的内容相同】
配合临键锁可以解决幻读问题,如果只用间隙锁和行锁无法解决幻读问题
SERIALIZABLE序列化,串行操作事务,一定安全

3:事务使用事项与场景

3.1:事务使用注意事项
  • 在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上

  • @Transactional 注解应该只被应用在 public 修饰的方法上

  • @Transactional是基于动态代理的,这使得需要一个类调用另一个类才生效,类内调用会失效

  • 被外部调用的公共方法A有两个进行了数据操作的子方法B和子方法C的事务注解说明:

    • 被外部调用的公共方法A声明事务@Transactional,无论子方法B和C是不是本类的方法、是否声明事务,事务均由公共方法A控制

    • 被外部调用的公共方法A未声明事务@Transactional,子方法B和C若是其他类的方法且各自声明事务:事务由子方法B和C各自控制

    • 被外部调用的公共方法A未声明事务@Transactional,子方法B和C若是本类的方法,会报错(没有可用的transactional)

    • 被外部调用的公共方法A声明事务@Transactional,子方法运行异常,但运行异常被子方法自己 try-catch 处理了,则事务回滚是不会生效的!

      • 解决方案1:子方法中不用 try-catch 处理运行异常
      • 解决方案2:子方法的catch里面将运行异常抛出throw new RuntimeException();
  • 默认情况下,Spring会对unchecked异常进行事务回滚,也就是默认对 RuntimeException() 异常或是其子类进行事务回滚;

    • 若想对所有异常(包括自定义异常)都起作用,注解上面需配置异常类型:@Transactional(rollbackFor = Exception.class)
  • 事务@Transactional由spring控制时,它会在抛出异常的时候进行回滚。

    • 如果自己使用try-catch捕获处理了,是不生效的。
    • 如果想事务生效可以进行手动回滚或者在catch里面将异常抛出throw new RuntimeException();有两种方法
try {
    ....  
} catch(Exception e) {
    logger.error("fail",e);
    throw new RuntimeException; // 抛出unchecked异常
}
try {
    ...
} catch (Exception e) {
    log.error("fail",e);
    // 手动回滚
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    return false;
}

总之:如下场景下@Transactional注解将会失效

其实就是动态代理原理

  • @Transactional 应用在非 public 修饰的方法上,@Transactional 将会失效。protected、private 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。【因为动态代理类只在public方法上生效】
  • @Transactional 注解属性 propagation 设置错误。PROPAGATION_SUPPORTS、PROPAGATION_NOT_SUPPORTED、PROPAGATION_NEVER这三种用了它们事务是不会发生回滚,加了等于没加。
  • @Transactional 注解属性 rollbackFor 设置错误,默认的只会对 RuntimeException 类型和 Error 类型的才进行回滚。如果在事务中抛出其他类型的异常,却希望 Spring 能够回滚事务,就需要指定 rollbackFor 属性。
  • 同一个类中方法调用,导致 @Transactional 失效。总方法调子方法的时候,要放在不同的 service 里面,如果放在一个类里面,事务调用是不会生效的。【放在同一个类里,不能正确的使用动态代理】
  • ----------- 下面的不太容易发生,但是也要注意 -------
  • 异常被吃了,有些时候 try catch 反倒会画蛇添足。
  • 数据库引擎不支持事务,这一点很简单,myisam 引擎是不支持事务的,innodb 引擎支持事务。
  • 要使用事务肯定要配事务管理器。JDBC 和 Mybatis 用的是 DataSourceTransactionManager。
  • 一个方法内多数据库(多数据源)的情况下会失效
3.2:事务使用场景
3.2.1:自动回滚

直接抛出,不try...catch

@Override
@Transactional(rollbackFor = Exception.class)
public Object submitOrder() throws Exception {  
     success();  
     //假如exception这个操作数据库的方法会抛出异常,方法success()对数据库的操作会回滚
     exception(); 
     return ApiReturnUtil.success();
}
3.2.2:手动回滚

进行try...catch,回滚并抛出

@Override
@Transactional(rollbackFor = Exception.class)
public Object submitOrder (){  
    success();  
    try {  
        exception(); 
     } catch (Exception e) {  
        e.printStackTrace();     
        //手工回滚异常
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        return ApiReturnUtil.error();
     }  
    return ApiReturnUtil.success();
}
3.2.3:回滚部分异常
@Override
@Transactional(rollbackFor = Exception.class)
public Object submitOrder (){  
    success();  
    //只回滚以下异常,设置回滚点
    Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
    
    try {  
        exception(); 
     } catch (Exception e) {  
        e.printStackTrace();     
        //手工回滚异常,回滚到savePoint
        TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
        return ApiReturnUtil.error();
     }  
    return ApiReturnUtil.success();
}

在这里插入图片描述

3.2.4:手动创建、提交、回滚

PlatformTransactionManager 这个接口中定义了三个方法

  • getTransaction创建事务
  • commit 提交事务
  • rollback 回滚事务。

它的实现类是 AbstractPlatformTransactionManager

package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface PlatformTransactionManager extends TransactionManager {
    // 手动创建事务
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;

    // 手动提交事务
    void commit(TransactionStatus status) throws TransactionException;

    // 手动回滚事务。(最好是放在catch 里面,防止程序异常而事务一直卡在哪里未提交)
    void rollback(TransactionStatus status) throws TransactionException;
}
3.3:事务其他情况
3.3.1:事务提交方式

默认情况下,数据库处于自动提交模式

每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。

在使用 spring 进行事务管理的时候,spring 会将是否自动提交设置为false,等价于JDBC中的 connection.setAutoCommit(false);

在执行完之后在进行提交 connection.commit();

spring事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。

spring事务管理器会捕捉任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。

3.3.2:事务并发经典情况

第一类丢失更新(脏写)

在没有事务隔离的情况下,两个事务都同时更新一行数据,但是第二个事务却中途失败退出,导致对数据的两个修改都失效了

在这里插入图片描述

脏读

脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

在这里插入图片描述

不可重复读

在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读

在这里插入图片描述

第二类丢失更新

不可重复读的特例,有两个并发事务同时读取同一行数据,然后其中一个对它进行修改提交,而另一个也进行了修改提交。这就会造成第一次写操作失效。

幻读

当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样

在这里插入图片描述


http://www.kler.cn/a/454261.html

相关文章:

  • 使 el-input 内部的内容紧贴左边
  • 前端开发 -- 自动回复机器人【附完整源码】
  • leetcode 27. 移除元素
  • 在 Vue3 项目中实现计时器组件的使用(Vite+Vue3+Node+npm+Element-plus,附测试代码)
  • C++系列之指针总结
  • 洛谷 P1014:Cantor 表
  • ModbusTCP从站转Profinet主站案例
  • LangChain教程 - 表达式语言 (LCEL) -构建智能链
  • windows下Redis的使用
  • Python vs PHP:哪种语言更适合网页抓取
  • 计算机基础复习12.22
  • 记录jvm进程号
  • jangow-01-1.0.1靶机
  • 16.3、网络安全风险评估项目流程与工作内容
  • 骑砍2霸主MOD开发(26)-Mono脚本系统
  • 《VQ-VAE》:Stable Diffusion设计的架构源泉
  • 在 Ubuntu 服务器上添加和删除用户
  • Redis篇--应用篇4--自动提示,自动补全
  • Oracle怎么写存储过程的定时任务执行语句
  • 骁龙 8 至尊版:AI 手机的变革先锋
  • 青少年编程与数学 02-005 移动Web编程基础 02课题、视口与像素
  • QT--模型/视图
  • 如何使用 Django 框架创建简单的 Web 应用?
  • Android native+html5的混合开发
  • 我的 2024 年终总结
  • 设计模式の命令访问者迭代器模式