通过上一篇《浅析Java反射系列相关基础知识(上)之类的加载以及反射的基本应用》文章我们知道每当要加载一个 class 的时候,JVM(Java Virtual Machine,Java虚拟机)就为其创建一个 Class 实例并关联起来,通过 Class 实例获取 class 信息的方法我们叫做「反射」。那么反射的目的也就是当我们获得某个 Object 实例的时候,我们可以利用反射这种方式获取该 Object 的 class 信息。 那么利用 JVM 动态加载 class 的特性,我们就可以做很多事情,我们可以在运行期间根据条件加载不同的实现类,可以通过反射生成 JDK(Java Development Kit,Java 语言的软件开发工具包) 动态代理等,这也是本文的重点内容。
动态代理的概念以及Java中动态代理的实现
所谓的「动态代理」指的就是在程序运行的时候,我们通过反射机制动态的创建代理类,通过代理类实现原有类的程序执行的同时,我们可以在这个基础上扩展我们自己业务,从而在不影响原有业务基础之上增加新的特性和功能,实现主副业务的解耦,代码的解耦等。
在 Java 中,java.lang.reflect 包下面有一个 Proxy 类和 InvocationHandler 接口,我们就是使用他俩来生成 JDK 动态代理类或者动态代理对象的。两个类部分重要的源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
/** * Proxy类主要提供创建动态代理类和代理对象的静态方法,是所有动态代理类的父类 */ public class Proxy implements java.io.Serializable { /** * 创建一个动态代理类所对应的Class对象 * 该代理类会实现interfaces所指定的接口 */ @CallerSensitive public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) throws IllegalArgumentException { final Class<?>[] intfs = interfaces.clone(); final SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkProxyAccess(Reflection.getCallerClass(), loader, intfs); } return getProxyClass0(loader, intfs); } /** * 检查代理类权限,这也是为什么要传入类加载器的原因 */ private static void checkProxyAccess(Class<?> caller, ClassLoader loader, Class<?>... interfaces) { SecurityManager sm = System.getSecurityManager(); if (sm != null) { ClassLoader ccl = caller.getClassLoader(); if (VM.isSystemDomainLoader(loader) && !VM.isSystemDomainLoader(ccl)) { sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION); } ReflectUtil.checkProxyPackageAccess(ccl, interfaces); } } /** * 直接创建动态代理对象,通过该代理对象实现interfaces指定的接口 * 执行代理对象的每个方法的时候都会被InvocationHandler对象的invoke方法来替代执行 */ @CallerSensitive public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException { Objects.requireNonNull(h); final Class<?>[] intfs = interfaces.clone(); final SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkProxyAccess(Reflection.getCallerClass(), loader, intfs); } /* * Look up or generate the designated proxy class. */ Class<?> cl = getProxyClass0(loader, intfs); /* * Invoke its constructor with the designated invocation handler. */ try { if (sm != null) { checkNewProxyPermission(Reflection.getCallerClass(), cl); } final Constructor<?> cons = cl.getConstructor(constructorParams); final InvocationHandler ih = h; if (!Modifier.isPublic(cl.getModifiers())) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { cons.setAccessible(true); return null; } }); } return cons.newInstance(new Object[]{h}); } catch (IllegalAccessException|InstantiationException e) { throw new InternalError(e.toString(), e); } catch (InvocationTargetException e) { Throwable t = e.getCause(); if (t instanceof RuntimeException) { throw (RuntimeException) t; } else { throw new InternalError(t.toString(), t); } } catch (NoSuchMethodException e) { throw new InternalError(e.toString(), e); } } } public interface InvocationHandler { /** * 代理的对象执行的所有方法都会被此方法替换执行 */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; } |
举一个简单的日志业务场景实例说一下动态代理的作用。比如老四做了一款12306刷票辅助软件,帮助用户刷到票提交订单的前后都需要在软件 GUI(Graphical User Interface,又称图形用户接口)界面日志控制台打印乔朴提交订单的相关信息。比如,占票成功前后需要打印「开始为您锁定有座座位」、「占票成功,准备开始提交订单」;提交订单之后需要提示用户「下单成功,请尽快前往12306官网付款」。这样的信息在实际的抢票流程中是不需要的,也就是说抢票模块只需要负责老老实实的给我刷票就好,至于这些日志提醒信息如果我们生硬的嵌套在抢票业务代码中不仅是业务耦合,维护起来也是相当麻烦,接下来我们以此为例看看如何通过反射生成 JDK 动态代理解决这样的问题。
首先,有一个非常简单的 Ticket 接口,声明了一个提交订单方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package com.glorze.reflect; /** * 12306刷票接口 * @ClassName Ticket * @author: 高老四 * @since: 2019年1月18日 下午3:40:17 */ public interface Ticket { /** * 提交订单 * @Title: submitOrder * @param idCard 身份证号码 * @return void * @author: glorze.com * @since: 2019年1月18日 下午3:46:49 */ public void submitOrder(String idCard); } |
然后 TicketImpl 类实现 Ticket 接口,这里只输出了一句话。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.glorze.reflect; /** * 抢票接口实现类 * @ClassName TicketImpl * @author: 高老四 * @since: 2019年1月18日 下午4:09:26 */ public class TicketImpl implements Ticket { @Override public void submitOrder(String idCard) { System.out.println("身份证号码: " + idCard + "的用户提交订单成功,等待付款"); } } |
创建 TicketLogger 业务类,这里面的两个方法要分前后切入到 submitOrder() 方法中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package com.glorze.reflect; /** * 12306刷票 * @ClassName TicketLogger * @author: 高老四 * @since: 2019年1月18日 下午4:10:17 */ public class TicketLogger { public void holdLogger() { System.out.println("您预订的车票存有余座,可以订票,准备开始提交订单"); } public void submitLogger() { System.out.println("订单提交成功,请尽快在订单时间内前往12306官网付款"); } } |
写一个主方法类,通过动态代理进行日志切入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package com.glorze.reflect; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * 实现InvocationHandler通过invoke方法实现反射类方法的主调 * @ClassName TicketInvocationHandler * @author: 高老四 * @since: 2019年1月18日 下午3:55:49 */ public class TicketInvocationHandler implements InvocationHandler { private Object target; public void setTarget(Object target) { this.target = target; } /** * proxy: 动态代理对象, 区别于目标对象 * method: 正在执行的方法 * args: 调用目标方法时传入的参数 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("切入抢票方法: " + method + "开始..."); TicketLogger ticketLogger = new TicketLogger(); ticketLogger.holdLogger(); Object result = method.invoke(target, args); ticketLogger.submitLogger(); System.out.println("切入抢票方法结束"); return result; } public static void main(String[] args) { Ticket glorzeTicket = new TicketImpl(); // 创建invocationHandler对象 TicketInvocationHandler invocationHandler = new TicketInvocationHandler(); // 为invocationHandler设置target对象 invocationHandler.setTarget(glorzeTicket); // 创建并返回动态代理 Ticket t = (Ticket) Proxy.newProxyInstance(Ticket.class.getClassLoader(), new Class[] {Ticket.class}, invocationHandler); // 动态代理对象调用目标对象方法实现invoke方法的调用 t.submitOrder("220173199809071435"); } } |
从以上代码我们看到,说白点就是通过动态代理的对象来代替 target 对象,然后代理对象通过 invoke 方法实现了我们想要的日志切入功能。不难发现当我们采用动态代理这种方式可以灵活地实现解耦,这其实就是我们经常在 Spring 中经常提及到的 AOP(Aspect Oriented Programming,面向切面编程),Spring 中的 AOP 代理包含了目标对象的全部方法,从而实现在目标方法前后可以定义一些通用方法帮助我们实现业务解耦,业务无痕切入等功能。
反射与泛型
关于泛型,老四之前在文章《浅析Java中的泛型以及泛型中的PECS(Producer Extends,Consumer Super)原则》中有所浅析,当然,在反射中也是支持泛型的,Class 这个类就是支持泛型的,形如 Class<String> ,如果类型未知需要使用 Class<?> 代替。在没说泛型反射之前,你可能经常看到形如 String str = (String)Class.forname(clazzName).newInstance(); 这样的代码,这样的代码是经常会在不小心的情况下抛出 ClassCastException 异常的,所以我们应该经常使用反射泛型以此来约束我们获取到的反射类型,甚至是参数类型,这样在实例化的反射类的时候不需要进行类型强制转换。
在说反射泛型之前,先简单说一下 Java8 新增的方法参数反射,这里面包含了可以获取带泛型的形参参数的特性。
Java8 中新增了一个 Executable 抽象类和一个 Parameter 类,Constructor 和 Method 都是 Executable 的子类,顾名思义,Constructor 就是提供了关于反射类的构造器形参相关信息获取的一些方法,Method 提供了获取方法形参的一些信息的方法。我们主要介绍一下获取形参信息的一些主要方法:
- int getParameterCount():获取构造器或者方法的形参个数
- Parameter[] getParameters():获取构造器或者方法的所有形参
- String getName():获取形参名称
- Type getparameterizedType():获取带泛型的形参类型,比如 Map<String, Integer>
- Class<?> getType(): 获取形参类型
- boolean isVarArgs():判断形参是否是可变形参
简单的代码示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
package com.glorze.reflect; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.List; /** * java8新增的方法参数反射 * @ClassName Reflect * @author: 高老四 * @since: 2019年1月16日 下午5:36:14 */ public class Reflect { public void testParameter(List<String> map, String num) { System.out.println("简单声明一个带参数的方法"); } public static void main(String[] args) throws NoSuchMethodException, SecurityException { Class<Reflect> clazz = Reflect.class; Method method = clazz.getMethod("testParameter", List.class, String.class); int parameterCount = method.getParameterCount(); Parameter[] parameters = method.getParameters(); for(Parameter parameter : parameters) { // 是否返回形参名称 if(parameter.isNamePresent()) { } System.out.println("testParameter方法泛型参数类型: " + parameter.getParameterizedType()); System.out.println("testParameter方法形参名称: " + parameter.getName()); System.out.println("testParameter方法参数类型: " + parameter.getType()); } System.out.println("============="); String name = method.getName(); boolean isVarArgs = method.isVarArgs(); System.out.println("testParameter方法参数个数: " + parameterCount); System.out.println("testParameter方法名称: " + name); System.out.println("testParameter方法是否存在可变形参: " + isVarArgs); } } |
上面代码有一个需要注意的地方,如图所示,形参名称打印出来不正确,这是因为这个方法要求运行的时候需要我们制定 javac 指定 -parameters 选项,在程序中可以通过 parameter.isNamePresent() 方法来判断是否返回了方法的形参信息。
通过上面的我们知道 java8 新增了可以获取方法中带有泛型形参的反射,那么在此之前我们主要是使用反射来获取成员变量的泛型信息,其实代码相通,直接看一下简单的示例代码好了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
package com.glorze.reflect; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Map; /** * 使用反射获取泛型信息 * @ClassName GenericReflect * @author: 高老四 * @since: 2019年1月21日 下午3:16:59 */ public class GenericReflect { /** * 泛型成员变量 */ private Map<String, Integer> map; public static void main(String[] args) throws NoSuchFieldException, SecurityException { Class<GenericReflect> clazz = GenericReflect.class; Field field = clazz.getDeclaredField("map"); Class<?> commonType = field.getType(); Type genericType = field.getGenericType(); if(genericType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) genericType; Type rawType = parameterizedType.getRawType(); System.out.println("成员变量的原始类型: " + rawType); Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); for(Type t : actualTypeArguments) { System.out.println("成员变量的泛型类型是: " + t); } } System.out.println("成员变量普通类型: " + commonType); System.out.println("成员变量的泛型类型: " + genericType); } } |
可以看到,getType() 方法只对普通类型的成员变量有效,如果成员变量是带有泛型的类型,并且想获取准确的泛型类型,我们需要使用 getGenericType() 方法。转换为 ParameterizedType 对象之后,可以通过其提供的 getRawType() 和 getActualTypeArguments() 方法获取相应的参数类型,前者返回没有泛型信息的原始类型,后者返回泛型参数的类型。
更博不易,如果觉得文章对你有帮助并且有能力的老铁烦请捐赠盒烟钱,点我去赞助。或者扫描文章下面的微信/支付宝二维码打赏任意金额(点击「给你买杜蕾斯」),也可扫描小站放的支付宝领红包二维码,线下支付享受优惠的同时老四也可以获得对应赏金,老四这里抱拳谢谢诸位了。捐赠时请备注姓名或者昵称,因为您的署名会出现在赞赏列表页面,您的捐赠钱财也会被用于小站的服务器运维上面,再次抱拳感谢。