SpringAop详解

文章目录

  • 一、Spring自定义注解
    • 1、什么是注解👨‍🏫
    • 2、注解的目的或作用💞
    • 3、JDK内置注解💫 【内置+元注解 == 一共八个固定注解】
    • 4、元注解 🎯
    • 5、自定义注解📸
    • 5+、Java反射API和类加载过程
      • 5+1、什么是反射+基本原理
      • 5+2、反射的应用场景有哪些
      • 5+3、API方法
        • 1、获得Class对象
        • 2、从 Class 中获取信息
      • 5+4、应用实例
      • 参考
    • 6、自定义注解实战🐱‍🏍
  • 二、AOP切面开发
    • 2.1 为什么用AOP编程
    • 2.2 AOP的基础术语
    • 2.3 常见的AOP五种通知
    • 2.4 实际运用+环绕通知
    • AOP参考:
  • 三、【事务踩坑8+12场景】Spring声明事务&编程事务(最好)
    • Transaction参考

首先了解基本的自定义注解(配置项参考网址) + 切面(配置项参考) + 切面由浅入深 + 具体的@Transaction详解

在这里插入图片描述

一、Spring自定义注解

在这里插入图片描述
第 1-5 小节均偏向于理论知识,若只是想要了解如何自定义注解和如何应用注解,请跳转至第6小节开始阅读。
在本篇中,主要是针对注解的概念和运行时注解进行解释说明,附带有三个实战的案例,尽可能的让大家能够理解透彻并且能够加以应用。

1、什么是注解👨‍🏫

Java 注解(Annotation)用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 Java5 开始添加到 Java 的。–官方文档

1.1、注解
Annotion(注解)是一个接口,程序可以通过反射来获取指定程序元素的Annotion对象,然后通过Annotion对象来获取注解里面的元数据。
我们常常使用的注解,@Data、@Controller等等,这些都是注解,创建一个注解,也很简单,创建一个类,然后将class改为 @interface就是一个注解啦。

1.2、注解出现的位置
Java代码中的包、类型、构造方法、方法、成员变量、参数、本地变量的声明都可以用注解来修饰。注解本质上可以看作是一种特殊的标记,程序在编译或者运行时可以检测到这些标记而进行一些特殊的处理。

1.3、关于注解的处理
我们一般将利用反射来处理注解的方式称之为运行时注解。
另外一种则是编译时注解,如我们常常使用的 lombok 里的注解,@Data,它能够帮我们省略set/get方法,我们在Class上加上这个注解后,在编译的时候,lombok其实是修改了.class文件的,将set/get方法放进去了,不然的话,你可以看看编译完后的.class文件。诸如这种,我们常称为编译时注解,也就是使用javac处理注解。
在这里插入图片描述

–图:来自于极客学院
这幅图就是从.java文件到class文件的,再到class文件被 JVM 加载的过程。
而其中的注解抽象语法树这一阶段,就是去解析注解,然后根据定义的注解处理器进行相关的逻辑处理。

这一块不是我的关注点,略过略过啦,朋友们,好奇可以去研究研究噢

2、注解的目的或作用💞

  • 生成文档。这是最常见的,也是 Java 最早提供的注解。如@param、@return等等
  • 跟踪代码依赖性,实现替代配置文件功能。**作用就是减少配置,如 Spring中Bean的装载注入,而且现在的框架基本上都是使用注解来减少配置文件的数量,同时这样也使得编程更加简洁,代码更加清晰。
  • 在编译时进行格式检查。**如@Override放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出;
  • 标识作用。**当Java编译时或运行时,检测到这里的注解,做什么的处理,自定义注解一般如此。
    携带信息。 注解的成员提供了程序元素的关联信息,Annotation 的成员在 Annotation类型中以无参数的方法的形式被声明。其方法名和返回值定义了该成员的名字和类型。在此有一个特定的默认 语法:允许声明任何Annotation成员的默认值。一个Annotation可以将name=value对作为没有定义默认值的Annotation 成员的值,当然也可以使用name=value对来覆盖其它成员默认值。这一点有些近似类的继承特性,父类的构造函数可以作为子类的默认构造函数,但是也 可以被子类覆盖。
    这么一大段话,其实就是关于注解中成员的解释。

说了这么多,其实一句话也能表达完。
注解就是一张便利贴,它贴在那里,你看到的那一刻,就明白该做什么事啦。

如出门前,门上贴着一张便利贴📌,上面写着"出门记得带钥匙",当你看到的那一刻,你就会去检查一下自己是否带钥匙啦。
在Java中也是一样的,你定义了一个注解,注解上可以写一些东西,然后你再将它贴在某个上面,说明白执行规则,当编译到这里的时候需要干嘛干嘛,又或者是当运行到这里的时候需要干嘛干嘛。
因为注解写的东西的不同,或者是处理注解的规则不同,而产生了不同的注解及作用。

3、JDK内置注解💫 【内置+元注解 == 一共八个固定注解】

Java中 内置的注解有5类,具体包括:
@Deprecated:过时注解,用于标记已过时 & 被抛弃的元素(类、方法等)
@Override:复写注解,用于标记该方法需要被子类复写
@SuppressWarnings:阻止警告注解,用于标记的元素会阻止编译器发出警告提醒
@SafeVarargs:参数安全类型注解,用于提醒开发者不要用参数做不安全的操作 & 阻止编译器产生 unchecked警告,Java 1.7 后引入

4、元注解 🎯

何为元注解?就是注解的注解,就是给你自己定义的注解添加注解,你自己定义了一个注解,但你想要你的注解有什么样的功能,此时就需要用元注解对你的注解进行说明了。
接着上一个比喻

注解有很多很多吗,门上贴一个,冰箱上贴一个,书桌上贴一个等等
元注解勒就是把他们整合起来称呼的,像上面这些可以统称为生活类注解啊。所以也就是注解的注解。

4.1、@Target
在 @Target 注解中指定的每一个 ElementType 就是一个约束,它告诉编译器,这 个自定义的注解只能用于指定的类型。
说明了注解所修饰的对象范围:注解可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。
4.2、@Retention
定义了该注解的生命周期:

某些注解仅出现在源代码中,而被编译器丢弃; (源码级)
而另一些却被编译在class文件中; (字节码级)

编译在class文件中的注解可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为注解与class在使用上是被分离的)。绝大多数开发者都是使用RUNTIME,因为我们期望在程序运行时,能够获取到这些注解,并干点有意思的事儿,而只有RetentionPolicy.RUNTIME,能确保自定义的注解在运行时依然可见。(运行级)

使用这个元注解可以对自定义注解的“生命周期”进行限制。
RetentionPolicy.SOURCE 一般开发者很少用到,大都是Java内置的注解。如@Override

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
}

这些注解只是在编译的时候用到,一旦编译完成后,运行时没有任何意义,所以他们被称作源码级别注解。
如果有了解过 lombok 一些简单原理的开发者, 都知道它是通过注解在编译时自动生成一部分代码,让源码看起来更简洁,字节码却很强大。
当然,这种方式有它自身的缺陷,譬如不一致性,问题排解时的困扰,以及依赖问题,不是本篇重点,扯回来。

提供信息给编译器: 编译器可以利用注解来检测出错误或者警告信息,打印出日志。
编译阶段时的处理: 软件工具可以用来利用注解信息来自动生成代码、文档或者做其它相应的自动处理。
运行时处理: 某些注解可以在程序运行的时候接受代码的提取,自动做相应的操作。

4.3、@Documented
用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如 javadoc此类的工具文档化。是一个标记注解,没有成员。
4.4、@Inherited
是一个标记注解阐述了某个被标注的类型是被继承的。使用了@Inherited修饰的注解类型被用于一个class时该class的子类也有了该注解。
4.5、@Repeatable
允许一个注解可以被使用一次或者多次(Java 8)。

5、自定义注解📸

自定义注解实际上就是一种类型而已,也就是引用类型(Java中除了8种基本类型之外,我们见到的任何类型都是引用类型)
5.1、定义注解
自定义注解过程:

声明一个类MyAnnotation
把class关键字改为@interface

这样我们就声明了一个自定义的注解,当我们用@interface声明一个注解的时候,实际上是声明了一个接口,这个接口自动的继承了java.lang.annotation.Annotation,但是我们只需要@interface这个关键字来声明注解,编译器会自动的完成相关的操作,不需要我们手动的指明继承Annotation接口
另外在定义注解时,不能再继承其他的注解或接口。
我举了四个例子,这四个注解分别是放在 类(接口、枚举类上)、构造函数、方法级别、成员属性上的。

@Documented    //定义可以被文档工具文档化
@Retention(RetentionPolicy.RUNTIME)//声明周期为runtime,运行时可以通过反射拿到
@Target(ElementType.TYPE)//注解修饰范围为类、接口、枚举
public @interface ClassAnnotation {
    public String name() default "defaultService";
    public String version() default "1.1.0";
}

@Documented
@Target(ElementType.CONSTRUCTOR)
@Retention(RetentionPolicy.RUNTIME)
public @interface ConstructorAnnotatin {
    String constructorName() default "";
    String remark() default "构造器";
}

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldAnnotation {
    public String name() default "defaultName";

    public String value() default "defaultValue";
}

 @Documented
 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.METHOD)
 public @interface MethodAnnotation {
     public String name() default "defaultName";
     public MethodTypeEnum type() default MethodTypeEnum.TYPE1;
 }

public enum MethodTypeEnum {
    TYPE1,TYPE2
}

5.2、注解的成员变量

成员以无参数无异常的方式声明 String constructorName() default “”;
可以使用default为成员指定一个默认值 public String name() default “defaultName”;
成员类型是受限的,合法的类型包括原始类型以及String、Class、Annotation、Enumeration (JAVA的基本数据类型有8种:byte(字节)、short(短整型)、int(整数型)、long(长整型)、float(单精度浮点数类型)、double(双精度浮点数类型)、char(字符类型)、boolean(布尔类型)
public MethodTypeEnum type() default MethodTypeEnum.TYPE1;
注解类可以没有成员,没有成员的注解称为标识注解,例如JDK注解中的@Override、@Deprecation
如果注解只有一个成员,并且把成员取名为value(),则在使用时可以忽略成员名和赋值号“=”
例如JDK注解的@SuppviseWarnings ;如果成员名 不为value,则使用时需指明成员名和赋值号"="

5.3、使用注解
因为我们在注解中声明了属性,所以在使用注解的时候必须要指明属性值 ,多个属性之间没有顺序,多个属性之间通过逗号分隔

@ClassAnnotation(name = "personBean", version = "1.2.1")
public class Person {

//    告诉大家是可以用的,但是影响我测试,我就又注释掉了.
//    @ConstructorAnnotatin(constructorName="Person()")
//    public Person(String description) {
//        this.description = description;
//    }

    @FieldAnnotation(name = "description", value = "This is my personal annotation")
    private String description;

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @MethodAnnotation(name = "sayHello", type = MethodTypeEnum.TYPE2)
    public void sayHello() {
        System.out.println("Hello Annotation!");
    }
}

5+、Java反射API和类加载过程

在这里插入图片描述
类的三个主要特性是它的字段(变量)、方法和构造函数。为了描述和访问对象,这三个特性在反射API中由单独的类表示:java.lang.reflect.Fieldjava.lang.reflect.Method,以及java.lang.reflect.Constructor。我们可以通过class对象查找类的这些成员。

5+1、什么是反射+基本原理

Java反射API,位于java.lang.reflect包。顾名思义,反射是类或对象检查自身的能力。反射允许Java代码查看对象(更准确地说,对象的类)并确定其结构。在安全管理器所施加的限制内,您可以找出一个类有哪些构造函数、方法和字段,以及它们的属性。

在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java的反射机制。

在这里插入图片描述
Class加载过程·:
Java类加载主要分为这么几个阶段

编译阶段
将.java源文件编译成.class字节码文件。

运行阶段

  • 当JVM第一次加载某一个用到的类时,会使用ClassLoader类加载器加.class文件加载到内存中。
  • 连接-验证阶段:对class进行验证 头文件元数据等。
  • 连接-准备阶段:收集字节码中的静态属性与静态代码块,并把静态属性赋值默认值。
  • 连接-解析阶段:解析阶段主要是把符号引用转换为地址应用。
  • 初始化:初始化阶段主要是运行class的静态代码块内容。

所有阶段都运行完成之后,此时JVM会把二进制的class文件加载到方法区,同时创建一个Class对象放入堆区。这个时候仅仅是完成了类的加载过程,之后才是创建对象。

5+2、反射的应用场景有哪些

  1. 在运行时判断任意一个对象所属的类。

  2. 在运行时构造任意一个类的对象。

  3. 在运行时判断任意一个类所具有的成员变量和方法.

  4. 在运行时调用任意一个对象的方法。

  5. 生成动态代理。

  6. Spring 框架中的反射机制主要是通过 Java 的反射 API
    实现的,它允许在运行时动态地加载、检查、创建、访问和修改类、对象、属性和方法等信息。

    在 Spring 中,反射机制常常用于实现依赖注入、AOP、事务管理等功能。具体来说,通过反射机制可以实现以下几个方面的功能:

    • 实例化对象:通过 Class 对象的 newInstance() 方法可以创建一个类的实例对象。

    • 获取类信息:通过 Class 对象的 getMethods()、getFields()、getConstructors()
      等方法可以获取类的属性、方法、构造函数等信息。

    • 调用方法:通过 Method 对象的 invoke() 方法可以调用方法。

    • 访问属性:通过 Field 对象的 get()、set() 方法可以访问和修改对象的属性。

    在 Spring 中,反射机制常用于实现依赖注入。例如,通过读取 XML 配置文件中的 bean
    定义信息,可以获取类的全限定名、构造函数参数、属性值等信息,然后使用反射机制动态地创建类的实例对象,并将其注入到其他对象中。

    另外,在 Spring AOP 中也常常用到反射机制。例如,在基于方法的 AOP 中,通过获取目标对象的 Method
    对象,可以在方法执行前后进行一些增强操作。

    总之,Spring 框架中的反射机制为实现各种高级特性提供了基础支持,也为开发人员提供了方便的工具和技术。

5+3、API方法

1、获得Class对象

每个类被加载之后,系统就会为该类生成一个对应的Class对象。通过该Class对象就可以访问到JVM中的这个类。

在Java程序中获得Class对象通常有如下三种方式:

使用 Class 类的forName(String clazzName)静态方法。该方法需要传入字符串参数,该字符串参数的值是某个类的全限定名(必须添加完整包名)。

调用某个类的class属性来获取该类对应的 Class 对象。

调用某个对象的getClass()方法。该方法是java.lang.Object类中的一个方法。

//第一种方式 通过Class类的静态方法——forName()来实现
class1 = Class.forName("com.lvr.reflection.Person");
//第二种方式 通过类的class属性
class1 = Person.class;
//第三种方式 通过对象getClass方法
Person person = new Person();
Class<?> class1 = person.getClass();

对于方式一和方式二都是直接根据类来取得该类的 Class 对象,相比之下,方式二有如下的两种优势:

代码跟安全。程序在编译阶段就能够检查需要访问的 Class 对象是否存在。
线程性能更好。因为这种方式无须调用方法,所以性能更好。
可以通过类的类类型创建该类的对象实例。

Class.newInstance();
//
Foot foot = (Foot) c1.newInstance();
2、从 Class 中获取信息

一旦获得了某个类所对应的Class 对象之后,就可以调用 Class 对象的方法来获得该对象的和该类的真实信息了。

1、获取 Class 对应类的成员变量
Field[] getDeclaredFields(); // 获取 Class 对象对应类的所有属性,与成员变量的访问权限无关。
Field[] getFields(); // 获取 Class 对象对应类的所有 public 属性。
Field getDeclaredField(String name); // 获取 Class 对象对应类的指定名称的属性,与成员变量的访问权限无关。
Field getField(String name); // 获取 Class 对象对应类的指定名称的 public 属性。

2、获取 Class 对应类的方法
Method[] getDeclaredMethods(); // 获取 Class 对象对应类的所有声明方法,于方法的访问权限无关。
Method[] getMethods(); // 获取 Class 对象对应类的所有 public 方法,包括父类的方法。
Method getMethod(String name, Class<?>...parameterTypes); // 返回此 Class 对象对应类的、带指定形参列表的 public 方法。 Method getDeclaredMethod(String name, Class<?>…parameterTypes); // 返回此 Class 对象对应类的、带指定形参列表的方法,与方法的访问权限无关。

3、获取 Class 对应类的构造函数
Constructor<?>[] getDeclaredConstructors(); // 获取 Class 对象对应类的所有声明构造函数,于构造函数的访问权限无关。 Constructor<?>[] getConstructors(); // 获取 Class 对象对应类的所有 public 构造函数。
Constructor getDeclaredConstructor(Class<?>...parameterTypes); // 返回此 Class 对象对应类的、带指定形参列表的构造函数,与构造函数的访问权限无关。 Constructor getConstructor(Class<?>…parameterTypes); // 返回此 Class 对象对应类的、带指定形参列表的 public 构造函数。

4、获取 Class 对应类的 Annotation(注释)
A getAnnotation(Class annotationClass); // 尝试获取该 Class 对对象对应类存在的、指定类型的 Annotation;如果该类型的注解不存在,则返回 null。
A getDeclaredAnnotation(Class annotationClass); // 这是Java8新增的方法,该方法尝试获取直接修饰该 Class 对象对应类、指定类型的Annotation;如果该类型的注解不存在,则返回 null。
Annotation[] getAnnotations(); // 返回修饰该 Class 对象对应类存在的所有Annotation。
Annotation[] getDeclaredAnnotations(); // 返回直接修饰该 Class 对应类的所有Annotation。
A[] getAnnotationsByType(Class annotationClass); // 获取修饰该类的、指定类型的多个Annotation。
A[] getDeclaredAnnotationsByType(Class annotationClass); // 获取直接修饰该类的、指定类型的多个Annotation。

5、获取 Class 对应类的内部类
Class<?>[] getDeclaredClasses(); // 返回该 Class 对象对应类包含的全部内部类。

6、获取 Class 对应类的外部类
Class<?> getDeclaringClass(); // 返回该 Class 对象对应类所在的外部类。

7、获取 Class 对应类所实现的接口
Class<?>[] getInterfaces();

8、获取 Class 对应类所继承的父类
Class<? super T> getSuperClass();

9、获取 Class 对应类的修饰符、所在包、类名等基本信息
int getModifiers(); // 返回此类或接口的所有修饰符。修饰符由 public、protected、private、final、static、abstract 等对应的常量组成,返回的整数应使用 Modifier 工具类的方法来解码,才可以获取真实的修饰符。
Package getPackage() // 获取该类的包。
String getName() // 以字符串的形式返回此 Class 对象所表示的类的名称。
String getSimpleName() // 以字符串的形式返回此 Class 对象所表示的类的简称。

10、判断该类是否为接口、枚举、注解类型等
boolean isAnnotation() // 返此 Class 对象是否是一个注解类型(由@interface定义)。
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) // 判断此 Class 对象是否使用了Annotation修饰。
boolean isAnonymousClass() // 此 Class 对象是否是一个匿名类。
boolean isArray() //此 Class 对象是否是一个数组。
boolean isEnum() // 此 Class 对象是否是一个枚举(由 enum 关键字定义)。
boolean isInterface() // 此 Class 对象是否是一个接口(由 interface 关键字定义)。
boolean isInstance(Object obj) // 判断 obj 是否是此 Class 对象的实例。该方法完全可以替代 instanceof 操作符。

5+4、应用实例

获取Class对象的几种方式

  //一、获取class的几种方式
    Class<?> cls1 = Class.forName("com.example.module01.reflect.Human"); //常用于动态的加载某个class,例如从配置文件获取
    Class<Human> cls2 = Human.class;//常用于调用某个需要class类型参数的方法时的参数传递
    Class<? extends Human> cls3 = new Human().getClass();//常用于在明确类型的情况下获取class
    Class<?> cls4 = RefTest01.class.getClassLoader().loadClass("com.example.module01.reflect.Human");//常用于动态

    System.out.println(cls1 == cls2 ? cls1 == cls3 ? cls1 == cls4 : "不相等" : "不相等"); //true

Constructor类API

        // 2.1 构造方法相关
        Class<Student> clazz = Student.class;
        Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
        System.out.println("声明的构造方法个数:" + declaredConstructors.length);
        Constructor<?>[] constructors = clazz.getConstructors();//获取所有的构造方法
        for (Constructor<?> constructor : constructors) {
            Class<?> declaringClass = constructor.getDeclaringClass();//获取此构造方法的声明类
            Class<?>[] parameterTypes = constructor.getParameterTypes();//获取不带泛型的构造方法
            Type[] genericParameterTypes = constructor.getGenericParameterTypes();//获取带泛型的构造方法
            Object instance = null;
//            instance = constructor.newInstance();//调用newInstance
            System.out.println("构造方法的声明位置:" + declaringClass + ",构造方法参数个数:" + parameterTypes.length
             + ",带泛型的构造方法参数个数:" + genericParameterTypes.length + ",创建的对象:" + instance);
        }

Method API

//2.2 method方法API
Method[] methods = clazz.getMethods(); //可以获取所有的公开的方法,包括本类的和继承的
for (Method method : methods) {
    System.out.println("公开的方法:" + method.getName() + "," + method.getDeclaringClass());
}
Method[] declaredMethods = clazz.getDeclaredMethods();//获取声明在本类中的所有方法,包括静态方法
for (Method declaredMethod : declaredMethods) {
    declaredMethod.setAccessible(true);//破解private权限
    declaredMethod.invoke(clazz.newInstance());//反射执行方法
    System.out.println("本类所有方法:" + declaredMethod);
}

System.out.println("========================Field=======================");

Field API

        //2.3 属性相关
        Field[] fields = clazz.getFields();
        for (Field field : fields) {
            System.out.println("公开的属性:" + field.getName());
        }
        for (Field declaredField : clazz.getDeclaredFields()) {
            Student student = clazz.newInstance();
            declaredField.setAccessible(true);//设置可访问
            declaredField.set(student,1);//给属性赋值
            Object value = declaredField.get(student);
            System.out.println("本类的所有属性:" + declaredField.getName()  + "  val ===> " + value);
        }

获取父类 接口API相关信息,通过获取superClass给继承的父类

属性赋值
   //2.4获取父类、接口信息
        Class<? super Student> superclass = clazz.getSuperclass();
        Class<?>[] interfaces = clazz.getInterfaces();
        System.out.println("父类是:" + superclass); //Object类的superClass是null。
        System.out.println("本类实现的接口:" + interfaces.length);

        //2.5 实现Spring自动注入效果,将父类的protected类型属性也自动注入。
        Class<Student> stuClass = Student.class;
        //通过反射创建对象
        Student instance = stuClass.newInstance();
        //获取本类的方法
        for (Field field : stuClass.getDeclaredFields()) {
            //开启访问权限
            field.setAccessible(true);
            //动态赋值
            field.set(instance,111);
        }
        //继续获取父类的class对象
        Class<? super Student> superClass = stuClass.getSuperclass();
        //继续获取父类的成员属性
        Field[] declaredFields = superClass.getFields();
        for (Field declaredField : declaredFields) {
            //给父类属性赋值
            declaredField.set(instance,222);
        }

        //打印输出
        System.out.println(instance);

参考

  1. Java–反射机制(一)——反射 API
  2. Java反射API详解
  3. Java反射-反射API、类加载过程

5.4、浅提一下反射【重点】
想要去获取注解就不得不提到反射啦,但 Java 反射会带来一定的耗时,因此使用运行注解需要考虑对性能的影响。
我们声明一个Student类用来描述学生对象的信息的

class Student{
   String name;
   String school;
   //...set/get
}

当我们创建一个学生对象时,学生对象的信息是保存在 Student 类中,所以 Student 类会提供获取这些信息的方法。
在Java类中,每个类都会有对应的Class,要想执行反射操作,必须先要获取指定类名的Class

了解Class对象:
类是程序的一部分,每个类都有一个 Class 对象。换言之,每当我们编写并且编译 了一个新类,就会产生一个 Class 对象(更恰当的说,是被保存在一个同名的 .class 文件中)。为了生成这个类的对象,Java 虚拟机 (JVM) 先会调用 “类加载器” 子系统把 这个类加载到内存中。
Class类:简单说就是用来描述类对象的信息的
类对象的信息包括:

类的基本信息:包名、修饰符、类名、基类,实现的接口
属性的信息:修饰符、属性类型、属性名称、属性值,
方法的信息:修饰符、返回类型、方法名称、参数列表、抛出的异常
构造方法的信息:修饰符、类名、参数列表、抛出的异常
注解的相关信息:
因为:类对象的相关信息全部保存在Class类
所以:Class类会提供获取这些信息的方法

一旦某个类的 Class 对象被载入内存,它就可以用来创建这个类的所有对象。

通过 Class 获取类的相关信息,通过Class创建对象,通过 Class 调用对象上面的属性,调用对象上面的方法,这种操作就称为反射,要想执行反射操作,必须先要获取到指定的类名的 Class
获取Class的不同方式

获取基本类型的Class

1)基本类型Class:如 int.Class获取的就是 int 类型的 Class
获取引用类型的Class1)引用类型的Class:如String.Class获取的就是String类对应的Class
2)通过对象来获取:如:String obj="hello"Class calz = obj.getClass(),获取的就是String类对应的Class
3Class.forName("java.lang.String"),获取的就是对应的Class

5.5、提取注解

public class TestClassAnnotation {

    private static Person person = new Person();

    public static void main(String[] args) {
        Class<?> clazz = person.getClass();
        //因为注解是作用于类上面的,所以可以通过isAnnotationPresent来判断是否是一个具有指定注解的类
        if (clazz.isAnnotationPresent(ClassAnnotation.class)) {
            System.out.println("This is a class with annotation ClassAnnotation!");
            //通过getAnnotation可以获取注解对象
            ClassAnnotation annotation = clazz.getAnnotation(ClassAnnotation.class);
            if (null != annotation) {
                System.out.println("BeanName = " + annotation.name());
                System.out.println("BeanVersion = " + annotation.version());
            } else {
                System.out.println("the annotation that we get is null");
            }
        } else {
            System.out.println("This is not the class that with ClassAnnotation");
        }
    }
}
This is a class with annotation ClassAnnotation!
BeanName = personBean
BeanVersion = 1.2.1

scss复制代码public class AnnotationTest {
 
  public static void main(String[] args) throws ClassNotFoundException {
    Class<?> clazz = Class.forName("com.nzc.my_annotation.shang.Person");
    System.out.println("==============类注解解析==============");
    printClassAnno(clazz);
    
    System.out.println("==============成员变量注解解析==============");
    printFieldAnno(clazz);
    
    System.out.println("==============成员方法注解解析==============");
    printMethodAnno(clazz);
    
    System.out.println("==============构造器注解解析==============");
    printConstructorAnno(clazz);
    
  }
  
  /**
   * 打印类的注解
   */
  private static void printClassAnno(Class<?> clazz) throws ClassNotFoundException {
    //判断是否有AuthorAnnotatin注解
    if(clazz.isAnnotationPresent(ClassAnnotation.class)) {
      //获取AuthorAnnotatin类型的注解
      ClassAnnotation annotation = clazz.getAnnotation(ClassAnnotation.class);
      System.out.println(annotation.name()+"\t"+annotation.version());
    }
  }
  
  
  /**
   * 打印成员变量的注解
   */
  private static void printFieldAnno(Class<?> clazz) throws ClassNotFoundException {
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
      if(field.isAnnotationPresent(FieldAnnotation.class)) {
        FieldAnnotation annotation = field.getAnnotation(FieldAnnotation.class);
        System.out.println(annotation.name()+"\t"+annotation.value());
      }
    }
  }
  
  /**
   * 打印成员变量的注解
   */
  private static void printMethodAnno(Class<?> clazz) throws ClassNotFoundException {
    Method[] methods = clazz.getDeclaredMethods();
    for (Method method : methods) {
      if(method.isAnnotationPresent(MethodAnnotation.class)) {
        MethodAnnotation annotation = method.getAnnotation(MethodAnnotation.class);
        System.out.println(annotation.name()+"\t"+annotation.type());
      }
    }
  }
  
  /**
   * 打印成员变量的注解
   */
  private static void printConstructorAnno(Class<?> clazz) throws ClassNotFoundException {
    Constructor<?>[] constructors = clazz.getDeclaredConstructors();
    for (Constructor<?> constructor : constructors) {
      if(constructor.isAnnotationPresent(ConstructorAnnotatin.class)) {
        ConstructorAnnotatin annotation = constructor.getAnnotation(ConstructorAnnotatin.class);
        System.out.println(annotation.constructorName()+"\t"+annotation.remark());
      }
    }
    System.out.println("无");
  }
  
}

ini复制代码==============类注解解析==============
personBean  1.2.1
==============成员变量注解解析==============
description  This is my personal annotation
==============成员方法注解解析==============
sayHello  TYPE2
==============构造器注解解析==============

6、自定义注解实战🐱‍🏍

注解大多时候与反射或者 AOP 切面结合使用,它的作用有很多,比如标记和检查,最重要的一点就是简化代码,降低耦合性,提高执行效率。
6.1、自定义注解 + SpringMVC 拦截器实现权限控制功能
还有一种应用场景,权限判断或者说是登录校验。

这个是我当时还没有学习市面上的权限框架,就是使用了这种自定义注解+拦截器的方式来实现简单的权限控制。

注意:此案例不可CV直接运行,代码很容易实现,大家理解思路即可。

定义注解:

@Target({ElementType.METHOD,ElementType.TYPE}) // 这个注解可以放在也可以放在方法上的。
@Retention(RetentionPolicy.RUNTIME)
public @interface Authority {
    Role[] roles() ;
}

arduino复制代码public enum Role {
    SADMIN,
    ADMIN,
    TEACHER,
    STUDENT
}

使用注解:

@Authority(roles = {Role.ADMIN, Role.SADMIN}) // 放在类上 说明这个类下所有的方法都需要有这个权限才可以进行访问
@RestController
@RequestMapping("/admin")
public class AdminController {
    
    @GetMapping("/hello")
    public String Hello(){
        return "hello 你最近还好吗";
    }
}
@Controller
@RequestMapping("/student")
public class StudentController {


  @Authority(roles = {Role.STUDENT}) // 放在方法上则说明此方法需要注解上的权限才能进行访问
    @GetMapping("/test")
    public String test(){
        return "你好,我已经不是一名学生啦";
    }

}

编写 SpringMVC 拦截器及处理注解的Handler
在其中进行 Token 的判断,和访问方法的权限判断,看方法上是否有注解,有的话,
就和当前用户对比,成功就可以访问,失败就直接拒绝。

当时用的是SSM框架,所以才会看到有 response.sendRedirect(contextPath + “/login”);这样的。

public class LoginInterceptor extends HandlerInterceptorAdapter {

    private static final Logger log = LoggerFactory.getLogger(WebExceptionHandler.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String url = request.getRequestURI();
//        log.info(request.getMethod()+" 请求URL:"+url);

        //从Token中解析User信息
        User user = TokenUtil.verifyToken(request);

        String contextPath = request.getContextPath();
        //user 为空则 表示 Token 不存在
        if (user != null) {
            if (user.getRole().equals("sadmin")) {
                //检查方法上 是否有注解的 Role.SADMIN 或者 Role.ADMIN 权限 , 没有则检查类上有没有 如果符合要求则放行
                if (HandlerUitl.checkAuthority(handler, new Role[]{Role.SADMIN, Role.ADMIN})) {
                    request.setAttribute("user", user);
                    return true;
                }
            }
            if (user.getRole().equals("admin")) {
                if (HandlerUitl.checkAuthority(handler, new Role[]{Role.ADMIN})) {
                    request.setAttribute("user", user);
                    return true;
                }else {
                    response.sendRedirect(contextPath + "/login");
                }
            }

            if (user.getRole().equals("teacher")) {
                if (HandlerUitl.checkAuthority(handler, new Role[]{Role.TEACHER})) {

                    return true;
                } else {
                    response.sendRedirect(contextPath + "/login");
                }
            }
            if (user.getRole().equals("student")) {
                if (HandlerUitl.checkAuthority(handler, new Role[]{Role.STUDENT})) {

                    return true;
                } else {

                    response.sendRedirect(contextPath + "/student/login");
                }
            }
        }else {
            response.sendRedirect(contextPath + "/login");
        }


        return false;
    }
}

用于检查 方法 或者 类 是否需要权限
并和 拥有的权限做对比
如果方法上有 ,则以方法的 优先

public class HandlerUitl {

    public static boolean checkAuthority(Object handler, Role[] roles1){
            if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            // 获取方法上的注解
            Authority authority = handlerMethod.getMethod().getAnnotation(Authority.class);
            // 如果方法上的注解为空 则获取类的注解
            if (authority == null) {
                authority = handlerMethod.getMethod().getDeclaringClass().getAnnotation(Authority.class);
            }
            // 如果标记了注解,则判断权限
            if (authority != null) {
                Role[] roles = authority.roles();
                //如果 方法权限为 0 则通过
                if(roles.length==0){
                    return true;
                }
                //判断 拥有的权限 是否 符合 方法所需权限
                for(int i = 0; i < roles.length; i++){
                    for(int j = 0; j < roles1.length; j++){
                        if(roles[i]==roles1[j]){
//                            System.out.println("可以访问");
                            return true;
                        }
                    }
                }

            }
            return false;
        }
        return true;

    }

}

6.2、自定义注解+AOP+Redis 防止重复提交
先简单说一下防止重复提交注解的逻辑:

在需要防止重复提交的接口的方法,加上注解。
发送请求写接口携带 Token
请求的路径+ Token 拼接程 key,value 值为生成的 UUID 码
然后 set Redis 分布式锁,能获取到就顺利提交(分布式锁默认 5 秒过期),不能获取就是重复提交了,报错。

定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {

    /**
     * 设置请求锁定时间
     * @return
     */
    int lockTime() default 5;
}

定义处理注解的切面类
java复制代码import com.eshop.api.ApiResult;
import com.eshop.common.aop.NoRepeatSubmit;
import com.eshop.common.util.RedisLock;
import com.eshop.common.util.RequestUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;

/**
 * 重复提交aop
 */
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {

    @Autowired
    private RedisLock redisLock;

    @Pointcut("@annotation(noRepeatSubmit)")
    public void pointCut(NoRepeatSubmit noRepeatSubmit) {
    }

    @Around("pointCut(noRepeatSubmit)")
    public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {
        int lockSeconds = noRepeatSubmit.lockTime();

        HttpServletRequest request = RequestUtils.getRequest();
        Assert.notNull(request, "request can not null");

        String bearerToken = request.getHeader("Authorization");
        String[] tokens = bearerToken.split(" ");
        String token = tokens[1];
        String path = request.getServletPath();
        String key = getKey(token, path);
        String clientId = getClientId();

        boolean isSuccess = redisLock.tryLock(key, clientId, lockSeconds);
        log.info("tryLock key = [{}], clientId = [{}]", key, clientId);

        if (isSuccess) {
            log.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
            // 获取锁成功
            Object result;

            try {
                // 执行进程
                result = pjp.proceed();
            } finally {
                // 解锁
                redisLock.releaseLock(key, clientId);
                log.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);
            }
            return result;
        } else {
            // 获取锁失败,认为是重复提交的请求
            log.info("tryLock fail, key = [{}]", key);
            return  ApiResult.fail("重复请求,请稍后再试");
        }
    }

    private String getKey(String token, String path) {
        return token + path;
    }

    private String getClientId() {
        return UUID.randomUUID().toString();
    }

}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

import java.util.Collections;

/**
 * Redis 分布式锁实现
 */
@Service
public class RedisLock {

    private static final Long RELEASE_SUCCESS = 1L;
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    // 当前设置 过期时间单位, EX = seconds; PX = milliseconds
    private static final String SET_WITH_EXPIRE_TIME = "EX";
    // if get(key) == value return del(key)
    private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 该加锁方法仅针对单实例 Redis 可实现分布式加锁
     * 对于 Redis 集群则无法使用
     *
     * 支持重复,线程安全
     *
     * @param lockKey   加锁键
     * @param clientId  加锁客户端唯一标识(采用UUID)
     * @param seconds   锁过期时间
     * @return
     */
    public boolean tryLock(String lockKey, String clientId, long seconds) {
        return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            SetParams setParams = new SetParams();
            String result = jedis.set(lockKey, clientId, setParams.nx().px(seconds));
            if (LOCK_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        });
    }

    /**
     * 与 tryLock 相对应,用作释放锁
     *
     * @param lockKey
     * @param clientId
     * @return
     */
    public boolean releaseLock(String lockKey, String clientId) {
        return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey),
                    Collections.singletonList(clientId));
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        });
    }
}

使用注解

/**
     * 添加收藏
     */
@NoRepeatSubmit
@PostMapping("/collect/add")
@ApiOperation(value = "添加收藏",notes = "添加收藏")
public ApiResult<Boolean> collectAdd(@Validated @RequestBody StoreProductRelationQueryParam param){
    // 处理业务逻辑
    return ApiResult.ok();
}

本篇主要是针对Java运行时的注解的讲解及应用,(但是你想一想,我们使用lombok的注解时,它的实现原理又是什么样的呢?为什么可以帮我们自动生成代码呢?是谁给我们做了这件事情呢?)

二、AOP切面开发

结合@AspectJ 切入点指示符 + 自定义注解实现各种 面向切面+反射 的去冗余话的业务编程。
类似于 在任何时候都可以按照 切点+通知(增强的代码) === 面向切面(多个连接点合成的地方) 进行动态代理生成对应 方法执行的编程技巧。
在这里插入图片描述

2.1 为什么用AOP编程

为什么使用AOP编程范式?
(1)分离功能需求和非功能需求;
(2)集中处理某一关注点;
(3)侵入性少,增强代码可读性及可维护性。

下边记录AOP切面在springboot中的使用。

AOP的应用场景
权限控制、缓存控制、事务控制、分布式追踪、异常处理等

2.2 AOP的基础术语

(1)Target:目标类,要被代理的类,例如,UserService;

(2)JoinPoint(连接点):所谓的连接点,是指那些被拦截到的方法;

(3)PointCut(切入点):被增强的连接点(所谓的增强其实就是添加的新功能);

(4)Advice(通知、增强),增强代码;

(5)Weaving(织入):是指把增强的advice应用到目标对象target来创建新的代理对象proxy的 过程。

(6)proxy:代理类;

(7)Aspect(切面):是切入点pointcut和通知advice的结合。

2.3 常见的AOP五种通知

(1)前置通知(@Before):在我们执行目标方法之前运行
(2)后置通知(@After):在我们执行目标方法结束之后,不管有没有异常

@After(value="execution(* com.example.aspectJ.demo1.ProductDao.findAll(..))")
public void after(){
   System.out.println("最终通知==================");
}

(3)返回通知(@AfterReturning):在我们的目标方法正常返回值后运行
(4)异常通知(AfterThrowing):在我们的目标方法出现异常后运行
(5)环绕通知(@Around):动态代理,需要手动执行jionPoint.process(),其实就是执行我们的目标方法执行之前,相当于前置通知,执行之后就相当于我们的后置通知

2.4 实际运用+环绕通知

@Around 环绕通知(等于前置通知 + 返回通知 + 异常通知 + 后置通知

环绕通知是通知类行中功能最强大的,它是JoinPoint的子接口,环绕通知需要携带 ProceedingJoinPoint 类型的参数。

环绕通知的几个重点:

(1)环绕通知类似于动态代理的全过程,ProceedingJoinPoint pjp 类型的参数可以决定是否执行目标方法(也就是说必须要手动调用 pjp.proceed() 方法,目标方法才能执行), 如果忘记这样做就会导致通知被执行了 , 但目标方法没有被执行 ,且让环绕通知必须要有返回值,返回值即目标方法的返回值。

(2)如果环绕通知没有返回值,会出现空指针异常的情况。

// ···· 此处省略引入包
@Aspect // 标注是一个切面
@Component
public class Aspact {
 
    // 定义一个切入点,关于切入点如何定义?
    @Pointcut("@annotation(com.lxc.springboot.annotation.MyAnnotation)")
    public void pointFn(){}
 
    @Around("pointFn()")
    public void aroundFn(ProceedingJoinPoint pjp) {
        String methodName = pjp.getSignature().getName();
        System.out.println("==========环绕通知执行了==========");
        Object result = null;
        try{
            // == 前置通知
            System.out.println("【目标方法】"+methodName);
            // 执行目标方法
            result = pjp.proceed();
            // == 结果通知
            System.out.println("目标方法返回结果为:"+result);
        }catch (Throwable e) {
            // == 异常通知
            System.out.println(e.getMessage());
        }
        // == 后置通知
        System.out.println("后置通知执行");
    }
}

在这里插入图片描述

除了环绕通知和异常通知,来看下 前置通知、结果通知和后置通知的执行顺序:前置–结果通知–后置通知。
在这里插入图片描述

AOP参考:

  1. AOP开发入门
  2. Spring AOP 所有切入点指示符详解(execution,within,this,target,args,@within,@target,@args,@annotation)
    3.官方AOP英文文档

三、【事务踩坑8+12场景】Spring声明事务&编程事务(最好)

JDBC实现事务伪代码:

1//Get database connection
2Connection connection = DriverManager.getConnection();
3//Set autoCommit is false
4connection.setAutoCommit(false);
5//use sql to operate database
6.........
7//Commit or rollback
8connection.commit()/connection.rollback
9
10connection.close();

需要在各个业务代码中编写代码如commit()、close()来控制事务。

但是Spring不乐意这么干了,这样对业务代码侵入性太大了,所有就用一个事务注解@Transactional来控制事务,底层实现是基于切面编程AOP实现的,而Spring中实现AOP机制采用的是动态代理,具体分为JDK动态代理和CGLIB动态代理两种模式。
在这里插入图片描述

  1. Spring的bean的初始化过程中,发现方法有Transactional注解,就需要对相应的Bean进行代理,生成代理对象。
  2. 然后在方法调用的时候,会执行切面的逻辑,而这里切面的逻辑中就包含了开启事务、提交事务或者回滚事务等逻辑。
    另外注意一点的是,Spring 本身不实现事务,底层还是依赖于数据库的事务。没有数据库事务的支持,Spring事务是不会生效的。

具体过程原理:

@Transactional实现原理三要素切面、切点、通知

  • InfrastructureAdvisorAutoProxyCreator后置处理器拦截所有Bean

  • 遍历所有类型为Advisor的切面

  • 返回满足切点条件的切面列表

  • 选择代理方法

  • 生成代理

  • 调用通知的invoke()方法

    • 开启事务
    • 调用其它通知的invoke()方法,如果没有执行目标方法
    • 执行异常,回滚事务
    • 执行成功,提交事务
  • 执行目标方法

了解@Transactional注解实现原理,不仅可以让我们对切面、切点、通知有一个清晰的认识,还可以让我们通过其思想实现类似功能,如@Cache注解实现应用缓存,@Async注解实现业务异步执行

接下来我们进入正题,看看哪些场景会导致Spring事务失败:
在这里插入图片描述

注意仅有图中红色的才生效如果不重写注解;@Transactional(rollbackFor=Exception.class),如果方法上加了这个注解,那么当这个方法抛出异常时(运行时和非运行时)就会回滚,数据库里面的数据也会回滚。如果不配置rollbackFor属性,那么事物只会在遇到运行时异常才会回滚。

默认情况下,SPRING只有在抛出的异常为运行时且为unchecked 异常才会回滚事务,也就是抛出的异常为RuntimeException 的子类(Errors也会)时才会回滚事务,而抛出 checked 异常则不会回滚事务 ,当然可以通过 @Transactional rollbackFor进行配置。
(6.1)checked异常: 一般是指程序不能直接控制的外界情况,是指在编译的时候就需要检查的一类异常,用户程序中必须采用try-catch机制处理或者通过throws交由调用者来处理。这类异常主要是指除了Error以及RuntimeException及其子类之外的异常。
(6.2)unchecked异常:一般是那些不在编译的时就要处理的一类异常。在JAVA体系里,所有的Error以及RuntimeException及其子类都是unchecked异常。
在这里插入图片描述
总结:

  1. 事务内部调用:@EnableAspectJAutoProxy(exposeProxy = true)在启动类中添加,会由Cglib代理实现。 或者:自身使用 proxy:
    1. ((ServiceA)AopContext.currentProxy()).doSave(user);
  2. 自己既想要保留事务tryCatch应对业务异常,又想要事务回滚使用:
    1. //假设这是一个service类的片段 try{ //出现异常 } catch (Exception e) { // 捕获异常,打印异常,或其他处理。但不抛出新的异常 e.printStackTrace(); // 手动回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } //此时return语句能够执行 return xxx;
  3. 少用@Transactional注解
  4. 将查询(select)方法放到事务外
  5. 事务中避免远程调用
  6. 事务中避免一次性处理太多数据
  7. 非事务执行
  8. 我们可以看到add方法的访问权限被定义成了private,这样会导致事务失效,spring要求被代理方法必须是public的。说白了,在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。
    1. 【注意第一种和self. 自注入方法——是指在方法内部调用另一个事务失效!】外部经过spring容器调用service的方法事务才生效,service类内部方法间相互调用事务不生效,也就是传说中的自调用失效问题。主要原因是 Spring数据库事务的约定,其实现原理是AOP,而AOP的原理是动态代理,在自调用的过程中,是类自身的调用,而不是代理对象去调用,那么就不会产生AOP,这样 Spring就不能把你的代码织入到约定的流程中,于是就产生了现在看到的失败场景。

Transaction参考

1.Transactional 事务注解实现原理
2.你必须懂也可以懂的@Transactional原理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/613213.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

46 udp网络程序

查询网络服务的命令 netstat -nlup n: 显示数字 a&#xff1a;显示所有 u&#xff1a;udp服务 p&#xff1a;显示pid Recv-Q收到的数量&#xff0c;本地ip和远端ip&#xff0c;00表示可以收到任何地址 网络聊天 服务端 定义一个server类&#xff0c;成员保存ip地址&#xff…

Required:(String) → String Found:String

报错 Function invocation summaryFu(...) expected No value passed for parameter msg Type mismatch. Required: (String) → String Found: String class FunTest {/*参数包含函数的调用者* */fun funAndFunPropTest() { // 调用下面的test()//创建一个函数&#xff0…

Colab/PyTorch - 002 Pre Trained Models for Image Classification

Colab/PyTorch - 002 Pre Trained Models for Image Classification 1. 源由2. 图像分类的预训练模型3. 示例 - AlexNet/ResNet1013.1 模型推断过程3.2 使用TorchVision加载预训练网络3.3 使用AlexNet进行图像分类3.3.1 Step1&#xff1a;加载预训练模型3.3.2 Step2&#xff1a…

Redis 基础之常用数据类型及命令

常用数据类型及命令 String&#xff08;字符串&#xff09;Hash&#xff08;哈希&#xff09;List&#xff08;列表&#xff09;Set&#xff08;集合&#xff09;zset ( sorted set&#xff1a;有序集合 )Redis setbit 命令HyperLogLogs ( 基数统计 ) Redis 比 Memcached 更优秀…

HTTP 1.1 与 HTTP 1.0

什么是HTTP HTTP 就是一个 超文本传输协议 协议 : 双方 约定 发送的 域名 数据长度 连接(长连接还是短连接) 格式(UTF-8那些) 传输 :数据虽然是在 A 和 B 之间传输&#xff0c;但允许中间有中转或接力。 超文本:图片、视频、压缩包,在HTTP里都是文本 HTTP 常见状态码 比如 20…

Python中的数据可视化:阶梯图matplotlib.pyplot.step()

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 Python中的数据可视化&#xff1a; 阶梯图 matplotlib.pyplot.step() [太阳]选择题 matplotlib.pyplot.step()的功能是&#xff1f; import matplotlib.pyplot as plt import numpy as…

linux - 搭建部署ftp服务器

ftp 服务: 实现ftp功能的一个服务,安装vsftpd软件搭建一台ftp服务器 ftp协议: 文件传输协议 (file transfer protocol),在不同的机器之间实现文件传输功能, 例如 视频文件下载,源代码文件下载 公司内部:弄一个专门的文件服务器,将公司里的文档资料和视频都存放…

企业网络需求及适合的解决方案

近年来&#xff0c;企业网络通信需求可谓五花八门&#xff0c;变幻莫测。它不仅为企业的生产、办公、研发、销售提供全面赋能&#xff0c;同时也让企业业务规模变大成为了可能。 在当前的技术格局下&#xff0c;中大型企业常见的技术方案有很多&#xff0c;而同时也有各自不可替…

武汉星起航:掌握亚马逊关键节日,抢占销售制高点

在电子商务的浪潮中&#xff0c;亚马逊平台以其卓越的服务和庞大的用户基础&#xff0c;成为全球卖家争相入驻的热门选择。对于卖家而言&#xff0c;了解并掌握亚马逊的各大促销节日&#xff0c;无疑是提升销售业绩、扩大品牌影响力的重要一环。武汉星起航在这里将详细解析亚马…

Leetcode—295. 数据流的中位数【困难】

2024每日刷题&#xff08;132&#xff09; Leetcode—295. 数据流的中位数 实现代码 class MedianFinder { public:MedianFinder() {}void addNum(int num) {if(maxHeap.empty() || num < maxHeap.top()) {maxHeap.push(num);} else {minHeap.push(num);}if(maxHeap.size(…

【Redis】用户登录校验

对于用 redis 对用户进行登录校验&#xff0c;大致可分为以下六步&#xff1a; 首先通过查询数据库来查找具有提供的用户名、密码和delFlag值为0的用户。如果未找到用户&#xff0c;则抛出一个带有消息"用户不存在"的ClientException&#xff08;用户不存在&#xf…

高效工作之软件系统——数据结构登记表

数据结构模板 开发完软件系统后&#xff0c;往往需要进行一些登记——《软件系统数据结构登记表》 然后软件项目有60个表左右&#xff0c;难道需要手动录入&#xff0c;那肯定不可能 工欲善其事必先利其器&#xff01;go。。。同事给的模板是下图 效果图 于是想到 之前使用…

RustDesk 自建服务器部署和使用教程

RustDesk 是一个强大的开源远程桌面软件&#xff0c;是中国开发者的作品&#xff0c;它使用 Rust 编程语言构建&#xff0c;提供安全、高效、跨平台的远程访问体验。可以说是目前全球最火的开源远程桌面软件了&#xff0c;GitHub 星星数量达到了惊人的 64k&#xff01; 与 Team…

2024数维杯B题详细思路代码数学建模高质量保姆级

2024年第九届数维杯大学生数学建模挑战赛题目 B 题 生物质和煤共热解问题的研究 &#xff08;1&#xff09;基于附件一&#xff0c;请分析正己烷不溶物(INS)对热解产率&#xff08;主要 考虑焦油产率、水产率、焦渣产率&#xff09;是否产生显著影响&#xff1f;并利用图像 加…

pygame学习--精灵组、碰撞检测、精灵更新

pygame学习--精灵组、碰撞检测、精灵更新 一.效果二.代码 通过pygame库,模拟种群的分化 1.X从左往右移动,表示年龄的增加;Y坐标表示阶层 2.随着X坐标不断增大,圆逐渐增大,颜色也加深 3.精灵越多,碰撞后死亡的概率越大,诞生新精灵的概率越小 4.每个精灵都有随机的运动速度及Y坐标…

OmniPlan Pro 4 for Mac中文激活版:项目管理的新选择

OmniPlan Pro 4 for Mac作为一款专为Mac用户设计的项目管理软件&#xff0c;为用户提供了全新的项目管理体验。其直观易用的界面和强大的功能特性&#xff0c;使用户能够轻松上手并快速掌握项目管理要点。 首先&#xff0c;OmniPlan Pro 4 for Mac支持自定义视图&#xff0c;用…

673. 最长递增子序列的个数(Leetcode)

文章目录 前言一、题目描述二、解题步骤1.小demo介绍2.动态规划1.状态表示2.状态转移方程3.初始化4.填表顺序5.返回值 三、代码编写总结 前言 在本篇文章中&#xff0c;我们将会讲到leetcode中673. 最长递增子序列的个数&#xff0c;我们将会用动态规划方式解决这道问题&#…

【JVM基础篇】JVM入门介绍

JVM入门介绍 为什么学习JVM 岗位要求 解决工作中遇到的问题 性能调优 真实案例 导出超大文件&#xff0c;系统崩溃从数据库中查询超大量数据出错消费者消费来不及导致系统崩溃Mq消息队列接受消息导致的内存泄漏业务高峰期系统失去响应 初识JVM 什么是JVM&#xff1f; JV…

Vue路由开启步骤

1.在控制台输入命令 //控制台下载安装npm add vue-router3.6.5 2.在main.js下导入并注册组件 import Vue from vue import App from ./App.vue//控制台下载安装npm add vue-router3.6.5 //导入 import VueRouter from "vue-router";//注册 Vue.use(VueRouter) con…

鸿蒙HarmonyOS开发:List列表组件的使用详解及案例演示(二)

文章目录 一、List组件简介1、List组件2、ListItem组件3、ListItemGroup组件 二、使用ForEach渲染列表三、设置列表分割线四、设置List排列方向五、索引值计算规则六、示例演示1、AlphabetIndexer组件2、代码3、效果 一、List组件简介 在我们常用的手机应用中&#xff0c;经常…