Java反射详解,学以致用,实战案例(AOP修改参数、Mybatis拦截器实现自动填充)2:https://developer.aliyun.com/article/1394601
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 方法的调用,获取到类的任意信息。
反射也就是这样啦,不知道你会使用啦吗,如果你还没有的话,我觉得可以再读上一遍,顺带自己验证一遍,希望你能有所收获。
后记
其实想写这篇文章时间已经不短了,但懒
偶尔发作(我又经常偶尔),所以总是一拖再拖,终于把它完成了,我也不知道你会不会读到此处,看到我发的牢骚。
只是非常简单的希望每一位阅读者能够有所收获,这应该是我持续写文的快乐吧~
今天又是好值的一天啊~
各位下次见!
明人不说暗话,我还是想要一键三连的,哈哈,走过路过还是可以点点赞的啦,要是喜欢,也可以给个关注啦,有收获可以再点点收藏的😊