Java的AOP修改参数、Mybatis拦截器实现自动填充

本文详细介绍了Java反射机制,包括反射的概念、Class对象的获取与API,以及如何通过反射获取运行时类的完整信息。通过反射可以实现AOP动态修改参数、Mybatis拦截器自动填充创建人和修改人信息,同时探讨了反射在框架中的重要性以及其优缺点。文中还提供了具体示例代码,帮助读者深入理解并运用反射。

前言​

作为Java开发者,你认为反射这个知识点重要程度,在你心里是什么样的呢? 以前我也只觉得反射非常重要,但总归是听这个文章说,听那个朋友说,学是学了,但却没怎么应用。 当我正式进入到社会当 cv 仔的时候,需要考虑的问题多了,慢慢思考问题了,就觉得反射是个力大无穷的东西,更会感觉反射是个无所不能的东西,如各种各样的框架的底层,各种各样的拦截器的实现,反射都是其中少不了的一部分~ 如果平时着重于开发业务的话,那么确实可能会较少使用到反射机制,但并非是说反射它不重要,反射它是Java 框架的基础勒,可以说木有反射,Java的动态性是会受限的~

大致思路:

编写的过程中,即使书写完已阅读过,但难免可能会出现遗漏,如有发现问题请及时联系修正,非常感谢你的阅读,希望我们都能成为技术道路上的朋友。

一、反射是什么?

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

不过要想解剖一个类,就要先获取到该类的字节码文件对应的Class类型的对象.

稍后就会讲到~

"反射之所以被称为框架的灵魂",主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力,这种能力也就是我们常说的动态性,利用这种性质可以使编写的程序更灵活更通用。

反射机制可以用来:

  • 在运行时分析类的能力,如可以构造任意一个类,可以获取任意一个类的全部信息,

  • 在运行时检查对象,如在运行时判断任意一个对象所属的类

  • 实现泛型数组操作代码,因为在运行时可以获取泛型信息

  • 利用Method对象,如我们经常使用的动态代理,就是使用Method.invoke()来实现方法的调用。

反射是一种功能强大且复杂的机制,在开发Java工具或框架方面,反射更是不可缺少的一部分。

二、Class 对象详解

之前说到了,如果要分析一个类,就必须要获取到该类的字节码文件对应 Class 类型对象。

另外如果有听到类模板对象,这个说的其实就是Class对象,大家不要误会了。

2.1、如何获取到Class对象呢?

得到Class的方式总共有四种:

  • 通过对象调用 getClass()方法来获取

  • 直接通过类名.class 的方式得到

  • 通过 Class对象的 forName() 静态方法来获取

  • Classloader,通过类加载器进行获取

/** * @description: * @author: Ning Zaichun * @date: 2022年09月15日 22:49 */ public class ClassDemo01 { ​ @Test public void test1() throws Exception { //1、通过对象调用 getClass() 方法来获取 // 类型的对象,而我不知道你具体是什么类,用这种方法 Student student = new Student(); Class studentClass1 = student.getClass(); System.out.println(studentClass1); // out: class com.nzc.Student ​ //2、直接通过`类名.class` 的方式得到 // 任何一个类都有一个隐含的静态成员变量 class // Class studentClass2 = Student.class; Class<?> studentClass2 = Student.class; System.out.println(studentClass2); // out: class com.nzc.Student System.out.println("studentClass1和studentClass2 是否相等==>" + (studentClass1 == studentClass2)); // studentClass1和studentClass2 是否相等==>true ​ //3、通过 Class 对象的 forName() 静态方法来获取,使用的最多 // 但需要抛出或捕获 ClassNotFoundException 异常 Class<?> studentClass3 = Class.forName("com.nzc.Student"); System.out.println(studentClass3); // out: class com.nzc.Student System.out.println("studentClass1和studentClass3 是否相等==>" + (studentClass1 == studentClass3)); //studentClass1和studentClass3 是否相等==>true ​ //4、 使用类的加载器:ClassLoader 来获取Class对象 ClassLoader classLoader = ClassDemo01.class.getClassLoader(); Class studentClass4 = classLoader.loadClass("com.nzc.Student"); System.out.println(studentClass4); System.out.println("studentClass1和studentClass4 是否相等==>" + (studentClass1 == studentClass4)); //studentClass1和studentClass4 是否相等==>true } } 复制代码

在这四种方式中,最常使用的是第三种方式,第一种都直接new对象啦,完全没有必要再使用反射了;第二种方式也已经明确了类的名称,相当于已经固定下来,失去了一种动态选择加载类的效果;而第三种方式,只要传入一个字符串,这个字符串可以是自己传入的,也可以是写在配置文件中的。

像在学 JDBC 连接的时候,大家肯定都使用过 Class对象的 forName() 静态方法来获取Class对象,再加载数据库连接对象,但可能那时候只是匆匆而过罢了。

注意:不知道大家有没有观察,我把各种方式所获取到的class对象,都进行了一番比较,并且结果都为true,这是因为一个类在 JVM 中只会有一个 Class 实例,为了解释此点,我把类加载过程也简单的做了一个陈述。

2.2、类的加载过程

当我们需要使用某个类时,如果该类还未被加载到内存中,则会经历下面的过程对类进行初始化。

​即类的加载 ---> 链接 ---> 初始化三个阶段。

在这里我只着眼于类的加载过程了,想要了解更为详细的,就需要大家去找找资料看看啦~

加载过程:

1、在我们进行编译后(javac.exe命令),会生成一个或多个字节码文件(就是项目中编译完会出现的 target 目录下的以.class结尾的文件)

2、接着当我们使用 java.exe 命令对某个字节码文件进行解释运行时。

3、加载过程

  • 就相当于将 class 文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构;

  • 并生成一个代表这个类的 java.lang.Class 对象,这个加载到内存中的类,我们称为运行时类,此运行时类,就是 Class的一个实例,所有需要访问和使用类数据只能通过这个 Class 对象。

  • 所谓Class对象,也称为类模板对象,其实就是 Java 类在 JVM 内存中的一个快照,JVM 将从字节码文件中解析出的常量池、 类字段、类方法等信息存储到模板中,这样 JVM 在运行期便能通过类模板而获 取 Java 类中的任意信息,能够对 Java 类的成员变量进行遍历,也能进行 Java 方法的调用。

  • 反射的机制即基于这一基础。如果 JVM 没有将 Java 类的声明信息存储起来,则 JVM 在运行期也无法进行反射。

4、这个加载的过程还需要类加载器参与,关于类加载器的类型大家可以去了解了解,还有双亲委派机制等,此处我便不再多言

2.3、为了更便于记忆的图

​(图片说明:为更好的描述JVM中只有一个Class对象,而画下此图,希望通过这张简图,让你记忆更为深刻)

2.4、Class 常用的API

通过 Class 类获取成员变量、成员方法、接口、超类、构造方法等

  • getName():获得类的完整名字。

  • getFields():获得类的public类型的属性。

  • getDeclaredFields():获得类的所有属性。包括private声明的和继承类

  • getMethods():获得类的public类型的方法。

  • getDeclaredMethods():获得类的所有方法。包括private声明的和继承类

  • getMethod(String name, Class[] parameterTypes):获得类的特定方法,name参数指定方法的名字,parameterTypes 参数指定方法的参数类型。

  • getConstructors():获得类的public类型的构造方法。

  • getConstructor(Class[] parameterTypes):获得类的特定构造方法,parameterTypes参数指定构造方法的参数类型。

  • newInstance():通过类的不带参数的构造方法创建这个类的一个对象。

另外就还有反射包下的几个常用的对象Constructor、Filed、Method等,分别表示类的构造器、字段属性、方法等等

这些都会在下文慢慢陈述出来~

三、获取运行时类完整信息并使用

所谓运行时类,就是程序运行时所创建出来的类,你直接理解为通过反射获取到的类也可。大体意思是如此。

在讲述这一小节时,先要理解Java中一切皆对象这句话。

我们平常编写一个类,我们会将它称为一个Java对象,但是在反射这里将此概念再次向上抽象了。

类的基本信息:构造方法、成员变量,方法,类上的注解,方法注解,成员变量注解等等,这些都是Java对象,也是证明了Java中一切皆对象这句话。

其中Constructor就表示构造方法的对象,他包含了构造方法的一切信息,

Field、Method等等都是如此。

不要太过于麻烦和重复书写,我将案例中操作的所有相关代码,都放在此处了,案例中的依赖全部基于此。

public interface TestService { } 复制代码
@Target({ElementType.TYPE,ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MarkAnnotation { ​ String value() default ""; ​ } 复制代码
public class Generic<T> { } ​ 复制代码
public interface GenericInterface<T> { } ​ 复制代码
// TYPE 表示可以标记在类上 // PARAMETER 表示可以标记在方法形式参数 // METHOD 方法 // FIELD 成员属性上 @Target({ElementType.TYPE,ElementType.METHOD, ElementType.FIELD,ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) // 这里表明是运行时注解 @Documented public @interface LikeAnnotation { ​ String value() default ""; ​ ​ String[] params() default {}; } 复制代码
@Data @MarkAnnotation public class Person implements TestService{ private String sex; ​ public Integer age; ​ private void mySex(String sex){ System.out.println("我的性别"+sex); } ​ public void myAge(Integer age){ System.out.println("我的年龄"+age); } ​ ​ } 复制代码
@ToString(callSuper = true) // 增加这行是为了打印时将父类属性也打印出来,方便查看~ @Data @LikeAnnotation(value = "123") public class Student extends Person implements Serializable { @LikeAnnotation private String username; ​ @LikeAnnotation public String school; ​ private Double score; ​ public Student() { } ​ private Student(String username) { this.username = username; } ​ public Student(String username, String school) { this.username = username; this.school = school; } ​ @LikeAnnotation public void hello() { System.out.println("世界,你好"); } ​ public void say( String username) { System.out.println("你好,我叫" + username); } ​ private void myScore(Double score) { System.out.println("我的分数是一个私密东西," + score); } ​ public void annotationTest(@LikeAnnotation String username,@MarkAnnotation String str){ System.out.println( "测试获取方法参数中的注解信息"); } } 复制代码

3.1、反射获取运行时类构造方法并使用

class获取构造方法的相关API

// 获取所有的构造函数 Constructor<?>[] getConstructors() ​ // 获取 public或 private 修饰的狗赞函数,只要参数匹配即可 Constructor<?>[] getDeclaredConstructors() ​ // 获取所有 public 修饰的 构造函数 Constructor<T> getConstructor(Class<?>... parameterTypes) //调用此方法,创建对应的运行时类的对象。 public T newInstance(Object ... initargs) 复制代码

newInstance():调用此方法,创建对应的运行时类的对象。内部调用了运行时类的空参的构造器。

要想此方法正常的创建运行时类的对象,要求:

  • 运行时类必须提供空参的构造器;

  • 空参的构造器的访问权限得够。通常,设置为 public。

为什么要 javabean 中要求提供一个 public 的空参构造器?

原因: 1、便于通过反射,创建运行时类的对象;2、便于子类继承此运行时类时,默认调用 super() 时,保证父类有此构 造器。

想要更详细的了解,建议去看生成的字节码文件,在那里能够给出你答案。

测试:

/** * @description: * @author: Ning Zaichun * @date: 2022年09月17日 1:17 */ public class ConstructorTest { ​ /** * 获取公有、私有的构造方法 并调用创建对象 */ @Test public void test1() throws Exception { Class<?> aClass = Class.forName("com.nzc.Student"); ​ System.out.println("======获取全部public 修饰的构造方法========="); Constructor<?>[] constructors = aClass.getConstructors(); for (Constructor<?> constructor : constructors) { System.out.println(constructor); } System.out.println("======获取 public、private 修饰的构造方法,只要参数匹配即可========="); /** * 这里的参数 Class<?>... parameterTypes 填写的是参数的类型,而并非某个准确的值信息 */ Constructor<?> constructor = aClass.getDeclaredConstructor(String.class); System.out.println(constructor); // 因为此构造函数是 private 修饰,如果不设置暴力反射,则没有权限访问。 // 这里setAccessible(true) 是设置暴力反射,如果不设置,则会报错, constructor.setAccessible(true); // 这里调用的有参构造,所以在调用 newInstance 方法时,也需要填写参数 Object o1 = constructor.newInstance("宁在春"); constructor.setAccessible(false); System.out.println("o1===>"+o1); ​ /** * 如果需要获取有参构造,只需要填写对应的参数类型即可, * 获取无参构造,填null或不填都可。 */ Constructor<?> constructor1 = aClass.getConstructor(); Constructor<?> constructor2 = aClass.getConstructor(String.class,String.class); System.out.println("无参构造==>"+constructor1); System.out.println("有参构造==>"+constructor2); Object o2 = constructor1.newInstance(); Object o3 = constructor2.newInstance("宁在春2","xxxx社会"); System.out.println("o2===>"+o2); System.out.println("o3===>"+o3); } } 复制代码

既然能够获取到构造方法,那么也就是可以使用的,用Constructor.newInstance()方法来调用构造方法即可,在下列的打印信息中,也可以看出来确实如此,如果明确要获取为Student对象的话,进行强转即可。

======获取全部public 修饰的构造方法========= public com.nzc.Student(java.lang.String,java.lang.String) public com.nzc.Student() ======获取 public、private 修饰的构造方法,只要参数匹配即可========= private com.nzc.Student(java.lang.String) o1===>Student(username=宁在春, school=null, age=null) 无参构造==>public com.nzc.Student() 有参构造==>public com.nzc.Student(java.lang.String,java.lang.String) o2===>Student(username=null, school=null, age=null) o3===>Student(username=宁在春2, school=xxxx社会, age=null) 复制代码

3.2、反射获取运行时类成员变量信息

class对象中获取类成员信息使用到的API

Field[] getFields(); Field[] getDeclaredFields(); ​ Field getDeclaredField(String name); ​ public native Class<? super T> getSuperclass(); 复制代码

这里的 Field 类对象,其实就是表示类对象中的成员属性,不过还有多了很多其他在反射时需要用到的属性和API罢了~

3.2.1、获取私有公有类成员信息

为了有更好的对比,我先编写了一段不使用反射时的正常操作。

/** * 不使用反射的进行操作 */ @Test public void test1() { Student student = new Student(); student.setUsername("username"); student.setSchool("xxxx社会"); student.setScore(100.0); // 永远三岁的小伙子 哈哈 🤡 student.setAge(3); student.setSex("男"); ​ System.out.println("student信息===>" + student); // out:student信息===>Student(super=Person(sex=男, age=3), username=username, school=xxxx社会, score=100.0) } 复制代码

现在我再使用反射来实现上述操作~

在实现之前,还是先来看看如何获取公有、私有的成员变量吧

@Test public void test2() throws Exception { Class<?> stuClass = Class.forName("com.nzc.Student"); ​ System.out.println("========获取所有 public 修饰的成员属性(包括父类属性)====="); Field[] fields = stuClass.getFields(); for (Field field : fields) { System.out.println(field); } //public java.lang.String com.nzc.Student.school //public java.lang.Integer com.nzc.Person.age ​ System.out.println("========获取所有(public、private、protected等修饰的)属性成员(不包括父类成员属性)====="); Field[] fields1 = stuClass.getDeclaredFields(); for (Field field : fields1) { System.out.println(field); } //private java.lang.String com.nzc.Student.username //public java.lang.String com.nzc.Student.school //private java.lang.Double com.nzc.Student.score ​ System.out.println("========通过反射,获取对象指定的属性====="); Field username = stuClass.getDeclaredField("username"); System.out.println("username===>" + username); } 复制代码

但是你发现没有,这无法获取到父类的成员变量信息,父类的信息,我们该如何获取呢?

3.2.2、获取父类成员属性信息

其实一样的,我们也是要获取到父类的Class对象,在Class API中有一个getSuperClass()方法可以获取到父类的class对象,其他的操作都是一样的~

@Test public void test5() throws ClassNotFoundException { Class<?> stuClass = Class.forName("com.nzc.Student"); System.out.println("========获取所有属性成员(包括父类成员属性)====="); Class clazz = stuClass; List<Field> fieldList = new ArrayList<>(); while (clazz != null) { fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields()))); clazz = clazz.getSuperclass(); } Field[] fields2 = new Field[fieldList.size()]; fieldList.toArray(fields2); for (Field field : fields2) { System.out.println(field); } //private java.lang.String com.nzc.Student.username //public java.lang.String com.nzc.Student.school //private java.lang.Double com.nzc.Student.score //private java.lang.String com.nzc.Person.sex //public java.lang.Integer com.nzc.Person.age } 复制代码

到这里我们已经知道如何获取到类的成员变量信息了,就可以来看看如何给它们设置值,或者通过反射的方式来获取到值了。

3.2.3、设置或修改类成员变量值

@Test public void test3() throws Exception { Class<?> stuClass = Class.forName("com.nzc.Student"); System.out.println("获取到 filed 后,设置值或修改值"); Constructor<?> constructor = stuClass.getConstructor(); Object o = constructor.newInstance(); ​ //操作pubilc 属性 Field school = stuClass.getDeclaredField("school"); school.set(o,"xxxx社会"); System.out.println(o); //Student(super=Person(sex=null, age=null), username=null, school=xxxx社会, score=null) // 另外既然可以设置,那么自然也是可以获取的 System.out.println(school.get(o)); //xxxx社会 ​ // 操作 private 修饰的成员属性 Field name = stuClass.getDeclaredField("username"); // setAccessible(true) 因为是获取private 修饰的成员变量 // 不设置暴力反射 是无法获取到的,会直接报 IllegalAccessException 异常 name.setAccessible(true); name.set(o,"Ningzaichun"); name.setAccessible(false); System.out.println(o); //Student(super=Person(sex=null, age=null), username=Ningzaichun, school=xxxx社会, score=null) } 复制代码

其实看到这里对于反射已经有个大概的印象了,并且这都是比较平常且实用的一些方法。

3.2.4、获取成员变量注解信息

@Test public void test4() throws Exception { Class<?> stuClass = Class.forName("com.nzc.Student"); Field school = stuClass.getDeclaredField("school"); ​ LikeAnnotation annotation = school.getAnnotation(LikeAnnotation.class); System.out.println(annotation); //@com.nzc.LikeAnnotation(params=[], value=) ​ Annotation[] annotations = school.getAnnotations(); for (Annotation annotation1 : annotations) { System.out.println(annotation1); } //@com.nzc.LikeAnnotation(params=[], value=) ​ ​ LikeAnnotation declaredAnnotation = school.getDeclaredAnnotation(LikeAnnotation.class); System.out.println(declaredAnnotation); //@com.nzc.LikeAnnotation(params=[], value=) ​ Annotation[] declaredAnnotations = school.getDeclaredAnnotations(); for (Annotation declaredAnnotation1 : declaredAnnotations) { System.out.println(declaredAnnotation1); } //@com.nzc.LikeAnnotation(params=[], value=) } 复制代码

关于getAnnotation、getDeclaredAnnotation 的分析大家可以看看下面这篇文章@Repeatable详解-getAnnotation、getDeclaredAnnotation获取不到对象

对于想要更详细的了解Java注解的朋友,大家可以继续找找相关资料。

3.3、反射获取运行时类对对象方法信息并调用

Class对象中使用的相关API

Method[] getMethods(); ​ Method getMethod(String name, Class<?>... parameterTypes); ​ Method[] getDeclaredMethods(); ​ Method getDeclaredMethod(String name, Class<?>... parameterTypes); ​ // 获取方法返回值类型 Class<?> getReturnType(); ​ // obj – 调用底层方法的对象 // args – 用于方法调用的参数 // return 使用参数args在obj上调度此对象表示的方法的结果 Object invoke(Object obj, Object... args) 复制代码

3.3.1、获取对象方法

获取公有、私有方法

@Test public void test1() throws Exception { Class<?> stuClass = Class.forName("com.nzc.Student"); ​ System.out.println("========获取所有public修饰的方法======"); Method[] methods = stuClass.getMethods(); for (Method method : methods) { System.out.println(method); } //public boolean com.nzc.Student.equals(java.lang.Object) //public java.lang.String com.nzc.Student.toString() //public int com.nzc.Student.hashCode() //public void com.nzc.Student.hello() //public void com.nzc.Student.say(java.lang.String) //public java.lang.Double com.nzc.Student.getScore() //public void com.nzc.Student.setScore(java.lang.Double) //public void com.nzc.Student.setSchool(java.lang.String) .... 还有一些没写出来了 ​ System.out.println("========获取对象【指定的public修饰的方法】======"); // 第一个参数为方法名,此处是获取public修饰的无参方法 Method hello = stuClass.getMethod("hello"); System.out.println("hello===>"+hello); // 带参方法,第二个参数填写【参数类型】 Method say = stuClass.getMethod("say", String.class); System.out.println("say===>"+say); //hello===>public void com.nzc.Student.hello() //say===>public void com.nzc.Student.say(java.lang.String) ​ ​ // 参数为 方法名,此处是获取 private 修饰的无参方法 // stuClass.getDeclaredMethod(""); Method myScore = stuClass.getDeclaredMethod("myScore", Double.class); System.out.println("myScore==>"+myScore); //myScore==>private void com.nzc.Student.myScore(java.lang.Double) ​ System.out.println("=======获取所有的方法======="); Method[] declaredMethods = stuClass.getDeclaredMethods(); for (Method declaredMethod : declaredMethods) { System.out.println(declaredMethod); } //public void com.nzc.Student.say(java.lang.String) //private void com.nzc.Student.myScore(java.lang.Double) //public java.lang.Double com.nzc.Student.getScore() //public void com.nzc.Student.setScore(java.lang.Double) //protected boolean com.nzc.Student.canEqual(java.lang.Object) } 复制代码

既然能获取到,那么也是能够调用的啦~,

3.3.2、调用对象方法

还记得经常能看到的一个invoke()方法吗,这里就是调用method.invoke()方法来实现方法的调用。

@Test public void test2() throws Exception { Class<?> stuClass = Class.forName("com.nzc.Student"); Constructor<?> constructor = stuClass.getConstructor(); Object o = constructor.newInstance(); ​ Method myScore = stuClass.getDeclaredMethod("myScore", Double.class); myScore.setAccessible(true); // 调用方法 myScore.invoke(o, 99.0); // 相当于 Student student= new Student(); // student.myScore(99.0); myScore.setAccessible(false); } 复制代码

之前阐述了 Method它本身就记录了方法的一切信息,我们实现调用也就是第一步罢了,说它可以获取到当时定义方法的一切都不为过,接下来一步一步来说吧。

3.3.3、获取方法上的注解信息

@Test public void test2() throws Exception { Class<?> stuClass = Class.forName("com.nzc.Student"); System.out.println("==== 获取成员变量上指定的注解信息==="); ​ Field username = stuClass.getDeclaredField("username"); System.out.println(username); //private java.lang.String com.nzc.Student.username Annotation annotation = username.getAnnotation(LikeAnnotation.class); System.out.println(annotation); //@com.nzc.LikeAnnotation(params=[], value=) ​ Method hello = stuClass.getDeclaredMethod("hello"); LikeAnnotation annotation1 = hello.getAnnotation(LikeAnnotation.class); System.out.println(hello+"===="+annotation1); // public void com.nzc.Student.hello()====@com.nzc.LikeAnnotation(params=[], value=) } 复制代码

3.3.4、获取方法参数及参数注解信息

不过在写项目时,有可能还会要获取方法参数上的注解,那该如何获取方法参数呢?又该如何获取方法参数的注解信息呢?

@Test public void test3() throws Exception { Class<?> stuClass = Class.forName("com.nzc.Student"); System.out.println("==== 获取方法参数中的注解信息==="); ​ Method annotationTest = stuClass.getDeclaredMethod("annotationTest",String.class,String.class); ​ // 获取方法的返回值类型 Class<?> returnType = annotationTest.getReturnType(); // 获取权限修饰符 System.out.println(Modifier.toString(annotationTest.getModifiers()) ); // 获取到全部的方法参数 Parameter[] parameters = annotationTest.getParameters(); for (Parameter parameter : parameters) { Annotation annotation = parameter.getAnnotation(LikeAnnotation.class); if(annotation!=null){ // 参数类型 Class<?> type = parameter.getType(); // 参数名称 String name = parameter.getName(); System.out.println("参数类型"+type+" 参数名称==>"+name+" 参数上的注解信息"+annotation); //参数类型class java.lang.String 参数名称==>arg0 参数上的注解信息@com.nzc.LikeAnnotation(params=[], value=) } } // 获取参数上全部的注解信息 Annotation[][] parameterAnnotations = annotationTest.getParameterAnnotations(); for (int i = 0; i < parameterAnnotations.length; i++) { for (int i1 = 0; i1 < parameterAnnotations[i].length; i1++) { System.out.println(parameterAnnotations[i][i1]); } } // @com.nzc.LikeAnnotation(params=[], value=) //@com.nzc.MarkAnnotation(value=) int parameterCount = annotationTest.getParameterCount(); System.out.println("获取参数个数==>"+parameterCount); } ​ ​ 复制代码

3.3.5、获取方法返回参数和方法权限修饰符

@Test public void test33() throws Exception { Class<?> stuClass = Class.forName("com.nzc.Student"); ​ Method annotationTest = stuClass.getDeclaredMethod("annotationTest",String.class,String.class); // 获取方法的返回值类型 Class<?> returnType = annotationTest.getReturnType(); System.out.println(returnType); //void // 获取权限修饰符 System.out.println(Modifier.toString(annotationTest.getModifiers()) ); //public } 复制代码

3.4、反射获取运行时类信息、接口信息、包信息

3.4.1、获取运行时类的接口信息

/** * 获取运行时类实现的接口 */ @Test public void test5() { Class clazz = Student.class; Class[] interfaces = clazz.getInterfaces(); for (Class c : interfaces) { System.out.println(c); } System.out.println("===================="); // 获取运行时类的父类实现的接口 Class[] interfaces1 = clazz.getSuperclass().getInterfaces(); for (Class c : interfaces1) { System.out.println(c); } } 复制代码

3.4.2、获取类所在包的信息

/** * 获取运行时类所在的包 */ @Test public void test6() { Class clazz = Person.class; Package pack = clazz.getPackage(); System.out.println(pack); // out:package com.nzc } 复制代码

3.4.3、获取类上注解信息

@Test public void test1() throws Exception { Class<?> stuClass = Class.forName("com.nzc.Student"); System.out.println("==== 获取类上指定的注解信息==="); ​ LikeAnnotation annotation = stuClass.getAnnotation(LikeAnnotation.class); System.out.println(annotation); //@com.nzc.LikeAnnotation(params=[], value=123) System.out.println("获取注解上的值信息==>"+annotation.value()); //获取注解上的值信息==>123 //annotation.params(); 注解有几个属性,就可以获取几个属性 ​ System.out.println("==== 获取类上全部注解信息==="); ​ Annotation[] annotations = stuClass.getAnnotations(); for (Annotation annotation1 : annotations) { System.out.println(annotation1); //@com.nzc.LikeAnnotation(params=[], value=123) } ​ LikeAnnotation declaredAnnotation = stuClass.getDeclaredAnnotation(LikeAnnotation.class); System.out.println("declaredAnnotation==>"+declaredAnnotation); //declaredAnnotation==>@com.nzc.LikeAnnotation(params=[], value=123) Annotation[] declaredAnnotations = stuClass.getDeclaredAnnotations(); ​ } 复制代码

注意:lombok 相关的注解是无法获取到的,因为 lombok 注解为编译时注解,并非是运行时注解,在编译完成后,lombok 注解并不会保留于class文件中,因此是无法通过反射获取到的。

@Data 也标明了它的存在级别为源码级别,而运行时存在注解@Retention为@Retention(RetentionPolicy.RUNTIME) 。

3.5、反射获取运行时类的父类的泛型信息、接口泛型信息

@Test public void test8(){ Class clazz = Person.class; ​ // 获取泛型父类信息 Type genericSuperclass = clazz.getGenericSuperclass(); System.out.println(genericSuperclass); // com.nzc.Generic<java.lang.String> // 获取泛型接口信息 Type[] genericInterfaces = clazz.getGenericInterfaces(); ​ for (Type genericInterface : genericInterfaces) { System.out.println(genericInterface); //interface com.nzc.TestService //com.nzc.GenericInterface<java.lang.String> } } 复制代码

四、反射应用场景及实战案例

5.1、那到底什么时候会使用反射

一句话说它的应用场景就是:确定不下来到底使用哪个类的时候,比如你要开发一个通用工具类,为了达到通用性,传入的参数对象,一般都是无法限制的,这个时候就是要用到反射啦~。

反射的特征:动态性

5.2、AOP + 自定义注解 修改前端传递的参数信息

需求如下:我现在引入了一个第三方 jar 包,里面有一个 MyBatis-Plus 查询构造器,其中构造 LIKE条件查询的条件是当前端传过来的参数带有逗号时,拼接为LIKE查询条件。

关键代码:

标识在对象的某个成员属性上

@Target({ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface LikeAnnotation { ​ String value() default ""; } ​ 复制代码

标识在Controller层上,以此来判断那些请求是需要被切入的。

@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MarkAnnotation { ​ String value() default ""; ​ } 复制代码

一个非常简单的Javabean

@Data public class Login { ​ @LikeAnnotation(value = "username") private String username; ​ private String password; ​ public Login() { } ​ public Login(String username, String password) { this.username = username; this.password = password; } } ​ 复制代码

Controller 类

@Slf4j @RestController public class HelloController { ​ @MarkAnnotation @GetMapping("/search") public Login getLikeLogin(Login login){ System.out.println(login); return login; } ​ } 复制代码

重点重点,切面类 LikeAnnotationAspect,处理逻辑全部在此处

@Aspect @Component @Slf4j public class LikeAnnotationAspect { ​ // 标记了 @MarkAnnotation 注解的才切入 降低性能消耗 @Pointcut("@annotation(com.nzc.annotation.MarkAnnotation)") public void pointCut() { ​ } ​ // 获取当前类及父类所有的成员属性 private static Field[] getAllFields(Object object) { Class<?> clazz = object.getClass(); List<Field> fieldList = new ArrayList<>(); while (clazz != null) { fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields()))); clazz = clazz.getSuperclass(); } Field[] fields = new Field[fieldList.size()]; fieldList.toArray(fields); return fields; } ​ @Around("pointCut()") public Object doAround(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); Object[] args = pjp.getArgs(); // 获取到第一个参数 Object parameter = args[0]; ​ log.info("parameter==>{}", parameter); if (parameter == null) { return pjp.proceed(); } ​ Field[] fields = getAllFields(parameter); ​ for (Field field : fields) { log.debug("------field.name------" + field.getName()); if (field.getAnnotation(LikeAnnotation.class) != null) { try { field.setAccessible(true); Object username = field.get(parameter); field.setAccessible(false); if (username != null && !username.equals("")) { field.setAccessible(true); field.set(parameter, "," + username + ","); field.setAccessible(false); } } catch (Exception e) { } } } // 调用方法 Object result = pjp.proceed(); long end = System.currentTimeMillis(); log.debug("修改耗时==>" + (end - start) + "ms"); return result; } ​ } 复制代码

实现效果:

5.3、Mybatis 拦截器实现自动填充创建人、修改人信息

看似好像写业务的我们,没有怎么接触Java反射,但实际上可能处处都隐含着反射。

使用过 Mybatis-Plus 的朋友,应该知道,可以设置自动填充数据(创建时间、更新时间、创建人、更新人等),不过那个是实现MetaObjectHandler接口进行处理的。

但是今天的话,我用Mybatis 原生的拦截器来进行一番实现,实现每次更新、添加时自动填充创建人、更新人等,表里没时间字段,就没演示时间了,但实现原理都一致。

import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.binding.MapperMethod.ParamMap; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.plugin.*; import org.springframework.stereotype.Component; ​ import java.lang.reflect.Field; import java.util.*; ​ /** * mybatis拦截器,自动注入创建人、创建时间、修改人、修改时间 * @author Ning Zaichun */ @Slf4j @Component @Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }) }) public class MybatisInterceptor implements Interceptor { public static Field[] getAllFields(Object object) { Class<?> clazz = object.getClass(); List<Field> fieldList = new ArrayList<>(); while (clazz != null) { fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields()))); clazz = clazz.getSuperclass(); } Field[] fields = new Field[fieldList.size()]; fieldList.toArray(fields); return fields; } ​ @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; String sqlId = mappedStatement.getId(); log.info("------sqlId------" + sqlId); //2022-09-17 17:16:50.714 INFO 14592 --- [nio-8080-exec-1] com.nzc.tree.commons.MybatisInterceptor : ------sqlId------com.nzc.tree.mapper.CategoryMapper.updateById SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); Object parameter = invocation.getArgs()[1]; log.info("------sqlCommandType------" + sqlCommandType); //2022-09-17 17:16:50.714 INFO 14592 --- [nio-8080-exec-1] com.nzc.tree.commons.MybatisInterceptor : ------sqlCommandType------UPDATE ​ if (parameter == null) { return invocation.proceed(); } if (SqlCommandType.INSERT == sqlCommandType) { Field[] fields = getAllFields(parameter); for (Field field : fields) { log.info("------field.name------" + field.getName()); try { if ("createBy".equals(field.getName())) { field.setAccessible(true); Object local_createBy = field.get(parameter); field.setAccessible(false); if (local_createBy == null || local_createBy.equals("")) { field.setAccessible(true); field.set(parameter, "nzc-create"); field.setAccessible(false); } } } catch (Exception e) { e.printStackTrace(); } } } if (SqlCommandType.UPDATE == sqlCommandType) { Field[] fields = null; if (parameter instanceof ParamMap) { ParamMap<?> p = (ParamMap<?>) parameter; if (p.containsKey("et")) { parameter = p.get("et"); } else { parameter = p.get("param1"); } if (parameter == null) { return invocation.proceed(); } ​ fields = getAllFields(parameter); } else { fields = getAllFields(parameter); } ​ for (Field field : fields) { log.info("------field.name------" + field.getName()); try { if ("updateBy".equals(field.getName())) { field.setAccessible(true); field.set(parameter,"nzc-update"); field.setAccessible(false); } } catch (Exception e) { e.printStackTrace(); } } } return invocation.proceed(); } ​ @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } ​ @Override public void setProperties(Properties properties) { // TODO Auto-generated method stub } } 复制代码

里面牵扯到的一些 Mybatis 中的一些对象,我没细说了,大家打印出来的时候都可以看到的。

测试:

执行的SQL语句打印信息

结果:

​反射的特性,看似和我们天天写业务没啥关系,但是它其实一直伴随着我们,这也是 Java 开发者的基础知识,基础不牢,地动山摇~

五、反射的优缺点

优点: 反射提高了程序的灵活性和扩展性,降低耦合性,提高自适应能力。 它允许程序创建和控制任何类的对象,无需提前硬编码目标类;对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;

缺点 :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。

以下文字来自于 反射是否真的会让你的程序性能降低吗?

1.反射大概比直接调用慢50~100倍,但是需要你在执行100万遍的时候才会有所感觉

2.判断一个函数的性能,你需要把这个函数执行100万遍甚至1000万遍

3.如果你只是偶尔调用一下反射,请忘记反射带来的性能影响

4.如果你需要大量调用反射,请考虑缓存。

5.你的编程的思想才是限制你程序性能的最主要的因素

小结

仔细阅读下来你会发现,

正如文中所说,所谓Class对象,也称为类模板对象,其实就是 Java 类在 JVM 内存中的一个快照,JVM 将从字节码文件中解析出的常量池、 类字段、类方法等信息存储到模板中,这样 JVM 在运行期便能通过类模板而获取 Java 类中的任意信息,能够对 Java 类的成员变量进行遍历,也能进行 Java 方法的调用,获取到类的任意信息。

反射也就是这样啦,不知道你会使用啦吗,如果你还没有的话,我觉得可以再读上一遍,顺带自己验证一遍,希望你能有所收获。

<think>好的,我现在需要帮用户解决如何使用AOPMyBatis实现公共字段的自动插入的问题。首先,我得理清楚用户的需求。他们希望在进行数据库操作时,自动填充像创建时间、创建人这些公共字段,而不需要每次手动设置。这样可以减少重复代码,提高效率。 接下来,我得回忆一下MyBatisAOP的相关知识。MyBatis本身没有提供这种自动填充的功能,所以需要通过其他方式实现AOP(面向切面编程)可以在方法执行前后插入自定义逻辑,这似乎是一个可行的方案。用户提到的引用[2]中提到了Spring内置的切面类,用于事务管理,这说明AOPSpring中可以很好地与应用结合,处理横切关注点。 首先,我需要确定在哪里切入。公共字段通常在插入和更新操作时需要处理,所以切入点应该是MyBatis的Mapper接口中对应的方法,比如insert和update方法。然后,定义通知(Advice),在方法执行前修改参数自动设置create_time、update_time等字段。 然后,考虑如何获取当前用户信息。通常,用户信息可能存储在Session或SecurityContext中,所以需要在AOP通知中获取这些信息,并设置到实体对象中。此外,时间字段可以使用new Date()来获取当前时间。 接下来,代码实现方面,需要创建一个切面类,使用@Aspect和@Component注解,定义切入点表达式,匹配Mapper中的插入和更新方法。然后编写@Before通知,在方法执行前处理参数。这里需要判断操作类型是插入还是更新,分别设置不同的字段。例如,插入时需要设置create_time和create_user,而更新时设置update_time和update_user。 还需要注意实体类中是否有这些字段,并且有对应的setter方法。如果实体类没有这些字段,AOP代码会报错,所以需要确保所有需要自动填充的实体都包含这些公共字段。 另外,用户提到的引用[3]中给出了公共字段的示例,包括create_time、create_user等,这可以帮助确认字段名称和数据类型,确保代码中的字段名与实际一致。 可能的问题包括切入点表达式是否正确,如何区分插入和更新操作,以及如何高效地获取当前用户。对于切入点,可以使用@annotation来匹配自定义注解,或者在方法名上进行匹配,比如方法名包含insert或update。不过方法名可能不稳定,更可靠的方式是使用自定义注解,比如@AutoFill,然后在需要的方法上添加该注解。 总结步骤: 1. 创建自定义注解@AutoFill,用于标记需要自动填充的方法。 2. 创建切面类,定义切入点为@AutoFill注解的方法。 3. 在通知中判断操作类型(插入或更新),设置相应的公共字段。 4. 获取当前用户信息并填充到实体对象中。 5. 确保实体类有对应的字段和setter方法。 这样,当Mapper中的方法被调用时,AOP自动处理公共字段的填充,无需手动设置,提高开发效率和代码可维护性。</think>### 分步说明:使用AOPMyBatis实现公共字段自动插入 #### 1. 定义公共字段注解(标记需要填充的方法) ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface AutoFill { OperationType value(); // 定义操作类型:INSERT/UPDATE } public enum OperationType { INSERT, UPDATE } ``` #### 2. 创建切面类(核心逻辑实现) ```java @Aspect @Component public class AutoFillAspect { // 获取当前用户ID(需结合具体认证框架实现) private Long getCurrentUserId() { // 示例:从Spring Security获取 return SecurityContextHolder.getContext().getAuthentication().getPrincipal().getId(); } // 定义切入点:所有被@AutoFill注解的方法 @Pointcut("@annotation(com.example.annotation.AutoFill)") public void autoFillPointCut() {} @Before("autoFillPointCut() && @annotation(autoFill)") public void autoFill(JoinPoint joinPoint, AutoFill autoFill) { Object[] args = joinPoint.getArgs(); if (args == null || args.length == 0) return; Object entity = args[0]; // 约定第一个参数为实体对象 Long userId = getCurrentUserId(); Date now = new Date(); try { if (autoFill.value() == OperationType.INSERT) { Method setCreateTime = entity.getClass().getMethod("setCreateTime", Date.class); Method setCreateUser = entity.getClass().getMethod("setCreateUser", Long.class); setCreateTime.invoke(entity, now); setCreateUser.invoke(entity, userId); } if (autoFill.value() == OperationType.UPDATE || autoFill.value() == OperationType.INSERT) { Method setUpdateTime = entity.getClass().getMethod("setUpdateTime", Date.class); Method setUpdateUser = entity.getClass().getMethod("setUpdateUser", Long.class); setUpdateTime.invoke(entity, now); setUpdateUser.invoke(entity, userId); } } catch (Exception e) { throw new RuntimeException("自动填充公共字段失败", e); } } } ``` #### 3. 在Mapper接口应用注解 ```java public interface UserMapper { @AutoFill(OperationType.INSERT) void insert(User user); @AutoFill(OperationType.UPDATE) void update(User user); } ``` #### 4. 实体类字段示例 ```java public class User { private Date createTime; private Long createUser; private Date updateTime; private Long updateUser; // getters/setters... } ``` ### 关键实现原理 1. **切入点定位**:通过自定义`@AutoFill`注解精准定位需要增强的DAO方法[^2] 2. **反射机制**:动态调用实体类的setter方法填充字段,避免硬编码 3. **上下文集成**:结合安全框架获取当前用户信息,保持数据一致性 4. **事务安全**:AOP增强在事务管理切面之前执行,保证字段写入有效性[^2] ### 优化建议 1. **缓存反射Method对象**:提升反射调用性能 2. **增加字段检查**:执行前验证实体类是否包含目标字段 3. **支持自定义策略**:通过接口扩展不同字段填充规则 4. **结合MyBatis拦截器**:对于复杂场景可配合`Interceptor`实现更底层控制[^1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值