啰里吧嗦式讲解java静态代理动态代理模式
Posted tom-kang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了啰里吧嗦式讲解java静态代理动态代理模式相关的知识,希望对你有一定的参考价值。
一.为啥写这个
二.理解和弄懂代理的前期准备
2.1.什么是代理
2.2.如何利用代码生成一个java文件
<dependency> <groupId>com.squareup</groupId> <artifactId>javapoet</artifactId> <version>1.8.0</version> </dependency>
2.3.如何使用代码将生成的java文件编译成class文件
//编译桌面 proxy文件夹下的Proxy.java文件 会生成一个Proxy.class文件
JavaCompiler.compile(new File(sourcePath + "/proxy/Proxy.java"));
2.4.如何利用代码将class 使用 类加载器加载进内存,并创建对象
// 获得类加载器 使用反射load到内存
URLClassLoader classLoader = new URLClassLoader(new URL[] { new URL("file:C:\Users\保密\Desktop\") });
Class clazz1 = classLoader.loadClass("proxy.Proxy"); //通过Class的对象 clazz1 的getConstructor方法, 得到类Proxy的构造器对象, InvocationHandler.class是入参
Constructor constructor = clazz1.getConstructor(InvocationHandler.class);
//通过构造器对象的newInstance 方法, 去创建一个类的对象
Object obj = constructor.newInstance(handler);
2.5.方法的反射知识
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Person {
public void buy() {
System.out.println("买买买");
}
public static void main(String[] args) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
new Person().buy();
//拿到Person类下面 public void buy(){} 方法对象Method method = Person.class.getMethod("buy"){};
Method m = Person.class.getMethod("buy");
//通过method方法的invoke方法可以执行该方法, 入参是该方法属于的类的对象,和参数 method.invoke(person,new Object[] {}) 效果等同于new Person.buy()
m.invoke(new Person(), new Object[] {});
}
}
买买买
买买买
三.静态代理
public interface Servlet {
//假设一个java jar包下面的接口 有这样一个功能就是上传文件
void uploadFile();
}
public class HttpServlet implements Servlet{
@Override public void uploadFile() {
System.out.println("我要开始上传文件啦......");
System.out.println("真的很复杂啦......");
}
}
思路1 在每个该方法的调用前加入一段记录时间的代码
public static void main(String[] args) {
long t1 = System.currentTimeMillis();
new HttpServlet().uploadFile();
long t2 = System.currentTimeMillis;
system.out.println("花费的时间" + t2-t1);
}
我要开始上传文件啦...... 真的很复杂啦......
1 ------------------ 这种方式显而易见的弊端就是,只要调用uploadFile方法,就要加两行代码计算,
代码重复了N份,以后如果又不需要计算时间了 改起来会非常麻烦
思路2 通过继承的方式重写uploadFile()的方法
public class HttpServletForCalTime extends HttpServlet{
@Override
public void uploadFile() {
System.out.println("统计时间....");
super.uploadFile();
System.out.println("统计时间....");
}
public static void main(String[] args) {
HttpServletForCalTime a = new HttpServletForCalTime();
a.uploadFile();
}
}
统计时间....
我要开始上传文件啦......
真的很复杂啦......
统计时间....
--------------------- 这种写法的好处是统计文件上传时间用HttpServletForCalTime 类就行了,
但是弊端就是不好扩展,比如我想在统计时间前 先对文件的关键字进行屏蔽处理,把王八蛋换成空格,那么又得继承
如果我想先计算时间 在屏蔽关键字,在上传文件,那么又得创建一个新的继承类
类无限的扩展,因为继承是一种包裹关系,不够灵活思路3---静态代理 使用聚合,动态注入所要代理的对象, 原文中有句非常经典的话:其实设计模式,多多少少都跟多态有关
我理解的多态就是java的具体执行哪个方法不是在编译期绑定,而是在运行期动态绑定
//创建一个代理类, 有个成员变量, 执行uploadFile的内容 取决于你传入什么Servlet对象
public class ServletTimeProxy implements Servlet{
private Servlet servlet;
public ServletTimeProxy(Servlet servlet) {
this.servlet = servlet;
}
@Override
public void uploadFile() {
System.out.println("计算时间...");
servlet.uploadFile();
System.out.println("计算时间...");
}
}
public class ServletLogProxy implements Servlet{
private Servlet servlet;
public ServletLogProxy(Servlet servlet) {
this.servlet = servlet;
}
@Override
public void uploadFile() {
System.out.println("打印日志...");
servlet.uploadFile();
System.out.println("打印日志...");
}
}
执行什么内容取决于你传入什么对象,比较灵活,如果想先计算时间再打印日志
就调个个就行了 活还是HttpServlet代替你干,
public static void main(String[] args) {
HttpServlet impl = new HttpServlet();
ServletTimeProxy a = new ServletTimeProxy(impl);//统计时间
ServletLogProxy b = new ServletLogProxy(a);//打印日志
b.uploadFile();
}
-----------------------------------------
我要打印日志....
计算时间...
我要开始上传文件啦......
真的很复杂啦......
计算时间...
我要打印日志....
该方法也有弊端,就是扩展性还是不够好,如果jar包里我有N个类的xx方法都需要计算执行时间,打印日志
那么得实现N个jar包里的接口,注入接口,代理该接口的方法
java.lang.reflect.Proxy @since 1.3
四.动态代理的原理
import java.lang.Override;
import java.lang.System;
import staticProxy.Servlet;
class Proxy implements Servlet {
private Servlet impl;
public Proxy(Servlet impl) { this.impl = impl; }
@Override
public void uploadFile() {
long start = System.currentTimeMillis();
this.impl.uploadFile();
long end = System.currentTimeMillis();
System.out.println("Fly Time =" + (end - start));
}
}
那么利用javapoet可以这么写,我写Proxy1 是1.0版本
public class Proxy1 {
public static Object newProxyInstance() throws IOException {
//第一步 通过javapoet工具类 创建一个名为Proxy的java类 , 该类实现了Servlet接口
// == public class Proxy implement Servlet //这里之前少写一个public 导致后面获取构造器对象时报NoSuchException,
TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("Proxy") .addModifiers(Modifier.PUBLIC)//定义类的修饰符//构造一个类,类名
.addSuperinterface(Servlet.class);
//第二步 创建类里面的属性 我们要注入该接口
//我们希望的属性是 private Servlet impl //所以指定 属性的类型 和 属性的名字 和 属性的范围
//注意 Modifier包千万不要导错了,import javax.lang.model.element.Modifier; //之前报错找了好久
FieldSpec fieldSpec = FieldSpec.builder(Servlet.class, "impl", Modifier.PRIVATE).build();
//将属性 添加到类中
typeSpecBuilder.addField(fieldSpec);
//我们希望构造方法中 将该接口的实现类 传到构造器里 方便灵活调用要处理的方法 //Proxy(Servlet impl) { // this.impl = impl; // }
MethodSpec constructorMethodSpec = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC).addParameter(Servlet.class, "impl")
//构造器传入参数的类型 就是你要创建的java类实现接口的类型, 属性名字
.addStatement("this.impl = impl")//构造器里面的语句
.build();
typeSpecBuilder.addMethod(constructorMethodSpec);
//类 ,属性 ,构造器都写完了, 接下来开始写方法 , 你实现的接口有N个方法
Method[] methods = Servlet.class.getDeclaredMethods();
for (Method method : methods) {
//在文件中写入方法 和 方法内容 方法里有返回值 入参 方法名
MethodSpec methodSpec = MethodSpec.methodBuilder(method.getName())
//给方法添加范围
.addModifiers(Modifier.PUBLIC)
//给方法添加 注解 通常都是实现 x接口的覆盖注解
.addAnnotation(Override.class)
//给方法添加返回值通过method.getReturnType能拿到该方法return接收的对象
.returns(method.getReturnType())
//给方法添加语句 这里是给每个方法都加上了一个计算时间
.addStatement("long start = $T.currentTimeMillis()", System.class)
//添加换行符
.addCode(" ") //添加执行语句 我们希望的执行语句是 执行传过来的接口的某个方法 //比如 impl.upload()
.addStatement("this.impl." + method.getName() + "()") .addCode(" ") //$T可以理解为占位符,javapoet会去找对应的类
.addStatement("long end = $T.currentTimeMillis()", System.class)
.addStatement("$T.out.println("Fly Time =" + (end - start))", System.class)
.build();
typeSpecBuilder.addMethod(methodSpec);
}
//生成一个 TypeSpec 前面定义的java文件 第一个参数是包名 这样生成的xx.java文件再次包下
JavaFile javaFile = JavaFile.builder("proxy", typeSpecBuilder.build()).build();
String sourcePath = "C:/Users/自己登陆的电脑用户名/Desktop";
//在桌面上生成一个Proxy.java的文件
javaFile.writeTo(new File(sourcePath)); return null;
}
//代码只贴了改动点 public class Proxy2 { public static Object newProxyInstance(Class clazz) throws IOException { TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("Proxy") .addSuperinterface(clazz); //指定属性 是那种类型 ,代理,代理, 就是代理的你要实现的类的接口, 起码现在看来是这样 FieldSpec fieldSpec = FieldSpec.builder(clazz, "impl", Modifier.PRIVATE).build(); //方法的构造器 MethodSpec constructorMethodSpec = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .addParameter(clazz, "impl") //构造器传入参数的类型 就是你要创建的java类实现接口的类型, 属性名字 .addStatement("this.impl = impl")//构造器里面的语句 .build(); }
public interface InvocationHandler { void invoke(Object proxy, Method method, Object[] args); }
package animateProxy; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import staticProxy.Servlet; public class MyInvocationHandler implements InvocationHandler { private Servlet impl; //为了完成对被代理对象的方法拦截,我们需要在InvocationHandler对象中传入被代理对象实例。 public MyInvocationHandler(Servlet impl) { this.impl = impl; } //参数1 想要、 //参数2 这个参数表示 传入接口 中的所有Method对象 //参数3 这个参数 对应当前method方法 中的参数 @Override public void invoke(Object proxy, Method method, Object[] args) { // TODO Auto-generated method stub //1.自定义想要加在 动态代理类 的方法的处理 比如 //统计想要 代理类的 方法的时间 long start = System.currentTimeMillis(); System.out.println("proxy:" + proxy); //proxy: [email protected] try { //方法的反射操作 //方法的反射操作是method对象来进行方法调用 //和 执行 impl.upload() 效果完全相同 method.invoke(impl, new Object[] {}); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InvocationTargetException e) { // TODO Auto-generated catch block e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("Fly time = " + (end - start)); } }
然后我们希望动态代理工具proxy3.0 能生成一个这样的文件
package proxy; import animateProxy.InvocationHandler; import java.lang.Override; import java.lang.reflect.Method; import staticProxy.Servlet; public class Proxy implements Servlet { private InvocationHandler handler; public Proxy(InvocationHandler handler) { this.handler = handler; } @Override public void uploadFile() { try { Method method = staticProxy.Servlet.class.getMethod("uploadFile"); this.handler.invoke(this, method, null); } catch(Exception e) { e.printStackTrace(); } } }
package animateProxy; import java.io.File; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import javax.lang.model.element.Modifier; import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.TypeSpec; /** * * 现在在proxy2的基础上进化下, * * 现在想执行 我们自己 想执行的代码 而不是 * * 固定的写死的 计算每个方法时间的代码 * * 这点比较难想到 , 就是代理的逻辑抽取出来, 把打印方法执行时间的逻辑抽取出来 * 可以自定义个接口 用于处理自己想 加在代理对象上面的方法 * * 定义个接口 ,所有想写自己的代理的类的 处理逻辑的 都必须 实现该接口 * * 然后将实现了 该接口的对象 接口传进来 * * * 修改后是为了实现 TimProxy里面的内容 * * this.handler.invoke(this, method, null); * 代码理解起来比较吃力 看个图 * InvocationHandler * InterfaceT.java |· * | | 聚合了MyInvocationHandler.java * Proxy.newProxyInstance----->TimeProxy interfaceTimpl.java | * | | * ------------------------------------ * * * TimeProxy.upload()---------->MyInvocationHandler.invoke()---->interfaceTimpl.upload() * * <p>Title: Proxy1</p> * <p>Description: </p> * @author 18045153 * @date 2019年2月27日 */ public class Proxy3 { //只要你在newProxyInstance方法中指定代理需要实现的接口Class clazz //jdk1.3 Class<?>[]:第二个参数也和我们的实现版本不一致,这个其实很容易理解, //我们应该允许我们自己实现的代理类同时实现多个接口。前面设计只传入一个接口,只是为了简化实现,让你专注核心逻辑实现而已 public static Object newProxyInstance(Class clazz, InvocationHandler handler) throws IOException, ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { //第一步 通过javapoet工具类 创建一个名为TimeProxy的java类 , 该类实现了interfaceT接口 //InterfaceT.class === public class TimeProxy implement InterfaceT TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("Proxy") .addModifiers(Modifier.PUBLIC)//定义类的修饰符//构造一个类,类名 .addSuperinterface(clazz); //第二步 创建类里面的属性 我们要注入该接口 具体参考interfaJuhe.java 的写法 //我们希望的属性是 InterfaceT impl //所以指定 属性的类型 和 属性的名字 和 属性的范围 //在生成的代理类中增加成员变量handler 效果就是 private InvocationHandler handler FieldSpec fieldSpec = FieldSpec.builder(InvocationHandler.class, "handler", Modifier.PRIVATE).build(); //将属性 添加到类中 typeSpecBuilder.addField(fieldSpec); //我们希望构造方法中 将该接口的实现类 传到构造器里 方便灵活调用要处理的方法 //interfaJuhe(InterfaceT impl) { // this.impl = impl; // } //效果就是 public TimeProxy(InvocationHandler handler ) { // this.handler = handler; //} MethodSpec constructorMethodSpec = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .addParameter(InvocationHandler.class, "handler")//构造器传入参数的类型 就是你要创建的java类实现接口的类型, 属性名字 .addStatement("this.handler = handler")//构造器里面的语句 .build(); typeSpecBuilder.addMethod(constructorMethodSpec); //类 ,属性 ,构造器都写完了, 接下来开始写方法 , 你实现的接口有N个方法 //本来方法是写死的 , 现在该为 Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { //在文件中写入方法 和 方法内容 方法里有返回值 入参 方法名 MethodSpec methodSpec = MethodSpec.methodBuilder(method.getName()) //给方法添加范围 .addModifiers(Modifier.PUBLIC) //给方法添加 注解 通常都是实现 x接口的覆盖注解 .addAnnotation(Override.class) //给方法添加返回值 通过method.getReturnType能拿到该方法return接收的对象 .returns(method.getReturnType()) //给方法添加语句 这里是给每个方法都加上了一个计算时间 .addCode("try { ") //Method method = staticProxy.Servlet.class.getMethod("uploadFile"); //这样写是为了拿到 要代理的那个接口类的 xx 方法 .addStatement(" $T method = " + clazz.getName() + ".class.getMethod("" + method.getName() + "")", Method.class) // 为了简单起见,这里参数直接写死为空 //这样写诗为了执行 实现了InvocationHandler接口的 java类自己的处理方法 // this.handler.invoke(this, method, null); .addStatement(" this.handler.invoke(this, method, null)") .addCode("} catch(Exception e) { ") .addCode(" e.printStackTrace(); ") .addCode("} ") .build(); //给每个类加一个方法 typeSpecBuilder.addMethod(methodSpec); } //生成一个 TypeSpec 前面定义的java文件 第一个参数是包名 这样生成的xx.java文件再次包下 //package animateProxy; String sourcePath = "C:/Users/"+System.getProperties().getProperty("user.name")+"/Desktop"; JavaFile javaFile = JavaFile.builder("proxy", typeSpecBuilder.build()).build(); // 为了看的更清楚,我将源码文件生成到桌面 javaFile.writeTo(new File(sourcePath)); // 编译 JavaCompiler.compile(new File(sourcePath + "/proxy/Proxy.java")); // 使用反射load到内存 URLClassLoader classLoader = new URLClassLoader(new URL[] { new URL("file:C:\Users\"+System.getProperties().getProperty("user.name")+"\Desktop\") }); Object obj = null; //Classloader:类加载器,你可以使用自定义的类加载器,我们的实现版本为了简化,直接在代码中写死了Classloader。 Class clazz1 = classLoader.loadClass("proxy.Proxy"); System.out.println(clazz1); System.out.println(clazz1.getDeclaredConstructors().getClass()); //将生成的TimeProxy编译成class 使用类加载器加载进内存中 再通过反射或者该类的构造器 //再通过构造器将其代理类 TimeProxy 构造出来 //NoSuchException 打印classz信息 发现 刚开始创建类 没有使用public Constructor constructor = clazz1.getConstructor(InvocationHandler.class); System.out.println("constructor" + constructor); obj = constructor.newInstance(handler); return obj; } }
Servlet a = (Servlet) Proxy3.newProxyInstance(Servlet.class, handler); System.out.println("返回的代理对象" + a);//返回的代理对象[email protected] //handler.invoke(a, a.getClass().getDeclaredMethod("uploadFile", null), null); //Proxy.newProxyInstance(Flyable.class, new MyInvocationHandler(new Bird())); // 代理了interfaceTimpl类 并且 写了自己的实现 a.uploadFile();
如果报NoSuchException错误, 将自己桌面上生成的Proxy.java放到项目proxy包下面
五.JDK动态代理类的用法
上面的只是解释了动态代理的一种实现思路 , 童鞋们平时只要会用的话可以不用关注 ,需要关注有3点
1.通过jdk反射包 Proxy.newProxyInstance 方法可以完成 动态代理, 参数第一个是类加载器, 第二个是要代理的类的接口
第三个是实现了InvocationHandler接口的自定义类
2.实现InvocationHandler接口, 里面有3个参数 ,
第一个参数就是生成的动态代理类Proxy.java,
原文中说是 :
如果你的接口中有方法需要返回自身,如果在invoke中没有传入这个参数,将导致实例无法正常返回。
在这种场景中,proxy的用途就表现出来了。简单来说,这其实就是最近非常火的链式编程的一种应用实现。
这个暂时没看懂
第二个参数很简单,就是你要代理的 接口实现类 的方法对象, 通过method.invoke 可以执行原来的方法不受影响
第三个参数就是代理的方法的参数了
3.动态代理的设计思路,为什么要有动态代理, 因为上面的代码也展示了,在不动用原来代码逻辑的情况下, 可以加入自己的逻辑
这在java中是一种AOP思想, 这种思想的应用场景有 比如 日志管理, 权限管理, 事务管理
六.动态代理的实际运用
示例来自https://blog.csdn.net/yerenyuan_pku/article/details/52598220
李阿昀的博客
在动态代理技术里,由于不管用户调用代理对象的什么方法,
都是调用开发人员编写的处理器的invoke方法(这相当于invoke方法拦截到了代理对象的方法调用),
比如如果有个方法 c.create(HttpServlet server);需要传入一个实现了HttpServlet接口的参数
那么可以利用动态代理技术 完成对被代理对象方法的拦截
Proxy.newProxyInstance实际上就是生成了一个 Proxy implements HttpServlet 这样一个java类
相当于传入一个c.create(Proxy)
而Proxy实现了HttpServlet接口,自然实现了该接口里的所有方法, 在每个方法里实际上执行的是
Method method = staticProxy.Servlet.class.getMethod("xxxx");
this.handler.invoke(this, method, null);
这样两句话
真正执行的方法是 xx implements InvocationHandler 里的 invoke方法, 并且给你传入了你要 代理的对象的 方法对象
并且开发人员通过invoke方法的参数,还可以在拦截的同时,
知道用户调用的是什么方法,因此利用这两个特性,就可以实现一些特殊需求,
例如:拦截用户的访问请求,以检查用户是否有访问权限、动态为某个对象添加额外的功能。
package 动态代理的实际运用; import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 利用动态代理技术编写一个用于处理全站中文乱码的过滤器 * * 实现了对HttpServlet.getParamter方法的拦截 * * <p>Description: </p> * @author 18045153 * @date 2019年2月28日 */ public class CharacterEncodingFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { // TODO Auto-generated method stub } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; request.setCharacterEncoding("UTF-8"); // 解决post方式请求下的中文乱码问题 get:request.getParameter() // servlet-----> requestRroxy.getCookie() //传入自定义的 ServletRequest 对象 chain.doFilter((ServletRequest) Proxy.newProxyInstance( CharacterEncodingFilter.class.getClassLoader(), request.getClass().getInterfaces(), new InvocationHandler() { //类加载器,你可以使用自定义的类加载器 HttpServletRequest接口 , 这样动态生成的代理类 实现了该接口下的所有方法 //传入的method对象 实际就表示是 HttpServletRequest接口下的所有方法的对象 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { /* * 判断在Servlet那端调用的是不是getParameter方法, * 如不是则不需要拦截,直接放行,直接调用tomcat的request(真的request)帮你把事情干了, * 并且args参数还要接着往下传 */ if (!method.getName().equals("getParameter")) { return method.invoke(request, args); // 由于是内部类,所以request需要声明为final(但在Java8中没这个必要了) } // 判断客户端的请求方式是不是get if (!request.getMethod().equalsIgnoreCase("get")) { return method.invoke(request, args); } //执行完 HttpServletRequest 的public abstract String getParameter(String s);方法拿到的返回值 String value = (String) method.invoke(request, args); if (value == null) { return null; } // return new String(value.getBytes("ISO8859-1"), "UTF-8"); return new String(value.getBytes("UTF-8"), "UTF-8"); } }), response); } @Override public void destroy() { // TODO Auto-generated method stub } //在web.xml文件中配置CharacterEncodingFilter。 //<filter> // <filter-name>CharacterEncodingFilter</filter-name> // <filter-class>cn.itcast.web.filter.CharacterEncodingFilter</filter-class> //</filter> //<filter-mapping> // <filter-name>CharacterEncodingFilter</filter-name> // <url-pattern>/*</url-pattern> //</filter-mapping> }
以上是关于啰里吧嗦式讲解java静态代理动态代理模式的主要内容,如果未能解决你的问题,请参考以下文章