今天来整理新学到的Java中一种强大的机制——代理机制。先简单讲下我在网上看到的代理机制的描述,有一个目标类的对象,他不直接面对用户,而是在它的基础之上有另外一个对象,称为代理对象,用户用的是这个代理对象,真正的操作还是用的目标对象。接下来给例子讲下两种动态代理JDK和Cglib。
代理(Proxy)是一种设计模式,定义:为其他对象提供一个代理以控制对某个对象的访问,即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。
这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法。(开闭原则)
代理模式的关键点是:代理对象与目标对象。代理对象是对目标对象的扩展,并会调用目标对象。
静态代理
静态代理在使用时,需要定义接口或者父类,被代理对象与代理对象一起实现相同的接口或者是继承相同父类。
关键:在编译期确定代理对象,在程序运行前代理类的.class 文件就已经存在了。
public interface IUserDao {
void save();
}
public class UserDao implements IUserDao {
@Override
public void save() {
System.out.println("数据已经保存");
}
}
public class UserDaoProxy implements IUserDao {
private IUserDao target;
public UserDaoProxy(IUserDao target) {
this.target = target;
}
@Override
public void save() {
System.out.println("开始事务");
target.save();
System.out.println("结束事务");
}
}
public class Test {
public static void main(String[] args) {
UserDao userDao = new UserDao();//目标对象
UserDaoProxy userDaoProxy = new UserDaoProxy(userDao);//代理对象,把目标传递给代理对象,建立代理关系
userDaoProxy.save();//执行代理方法
}
}
静态代理优点:可以做到在不修改目标对象的功能前提下,对目标功能扩展。
静态代理缺点:代理类和目标类实现相同的接口,同时要实现相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。
一:JDK代理
直接简单粗暴点上例子,给出我们要代理的目标类SomeClass
和接口ISomeClass
public interface ISomeClass {
void fun1();
String fun2(int num);
}
public class SomeClass implements ISomeClass {
public SomeClass() {
}
@Override
public void fun1() {
System.out.println("执行fun1()");
}
@Override
public String fun2(int num) {
System.out.println("执行fun2()");
return "num为" + num;
}
public void classSpMethod() {
System.out.println("SomeClass没有接口限制,特有的方法");
}
public final void finalMethod() {
System.out.println("被final修饰的方法");
}
}
JDKProxy:JDK代理的获取,利用java.lang.reflect.Proxy类
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JDKProxy {
public JDKProxy() {
}
@SuppressWarnings("unchecked")
public static <T> T getJDKProxy(Object object) {
Class<?> klass = object.getClass();
ClassLoader classLoder = klass.getClassLoader();
Class<?>[] interfaces = klass.getInterfaces();
return (T) Proxy.newProxyInstance(classLoder, interfaces, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
System.out.println("前置拦截");
try {
result = method.invoke(object, args);
System.out.println("后置拦截");
} catch(Throwable e){
System.out.println("异常拦截");
throw e;
}
return result;
}
});
}
}
JDKProxy测试
public class JDKProxyDemo {
public static void main(String[] args) {
SomeClass someClass = new SomeClass();
ISomeClass jdkProxy = JDKProxy.getJDKProxy(someClass);//必须强制为接口类型,否则报错
jdkProxy.fun1();
String result = jdkProxy.fun2(100);
System.out.println(result);
// jdkProxy.finalMethod();报错,没定义
//验证其内部关系
System.out.println("jdkProxy instanceof SomeClass:" + (jdkProxy instanceof SomeClass));
System.out.println("jdkProxy instanceof ISomeClass:" + (jdkProxy instanceof ISomeClass));
System.out.println("someClass instanceof ISomeClass:" + (someClass instanceof ISomeClass));
}
}
/*输出结果
前置拦截
执行fun1()
后置拦截
前置拦截
执行fun2()
后置拦截
num为100
jdkProxy instanceof SomeClass:false
jdkProxy instanceof ISomeClass:true
someClass instanceof ISomeClass:true
*/
从上述结果可以知道我们成功的获取了相关类的JDK代理。
从验证内部关系我们可以得到如下结论:
JDK产生的代理对象和目标对象好像一个兄弟关系,共同的父亲是那个接口。
JDK代理要求
被代理的类必须是接口的实现类,往后这个代理对象必须强转为接口类型,因为产生的代理对象是接口类实现类类型,代理对象只能调用接口方法(帽子现在为接口)。
二:Cglib代理
需要cglib-nodep-x.x.x.jar的jar包。
CglibProxy:Cglib代理的获取
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class CglibProxy {
public CglibProxy() {
}
@SuppressWarnings("unchecked")
public static <T> T getCglibProxy(Object object) {
Class<?> klass = object.getClass();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(klass); //从这里就可以看出Cglib创建代理的端倪
enhancer.setCallback(new MethodInterceptor() { //上面全当巫师的咒语吧
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object result = null;
System.out.println("前置拦截");
try {
result = method.invoke(object, args);
System.out.println("后置拦截");
} catch(Throwable e) {
System.out.println("异常拦截");
throw e;
}
return result;
}
});
return (T) enhancer.create();
}
}
还是上面SomeClass
的相关测试
public class CglibProxyDemo {
public static void main(String[] args) {
SomeClass someClass = new SomeClass();
SomeClass cglibProxy = CglibProxy.getCglibProxy(someClass);
cglibProxy.fun1();
String result = cglibProxy.fun2(100);
System.out.println(result);
cglibProxy.classSpMethod();
cglibProxy.finalMethod();
System.out.println("cglibProxy instanceof SomeClass:" + (cglibProxy instanceof SomeClass));
}
}
/*输出结果
前置拦截
执行fun1()
后置拦截
前置拦截
执行fun2()
后置拦截
num为100
前置拦截
SomeClass没有接口限制,特有的方法
后置拦截
被final修饰的方法
cglibProxy instanceof SomeClass:true
*/
Cglib代理分析
从结果分析我们可以得知:
Cglib产生的代理对象和目标对象是个父子关系,父为目标对象的类。子类对象可以强转为父类型,所有目标类里面的方法(对外提供的)都可以调用,被代理的类不需要实现接口了。
但是也正是因为代理对象是被代理的类的派生类的对象。因为派生(继承的关系),父类final方法不能覆盖,不能进行代理了,但是还是可以调用!final修饰方法可以执行,但是不能被代理增加相应东西了。若代理类本身是final类,则根本就不可能被代理。
简述原理:实际上是动态生成了被代理类的子类字节码,因为字节码都是按照JVM编译后形成的class文件的规范编写的,所以它可以被JVM正常加载并且运行。
三:代理作用
学完代理,第一疑惑就是这有啥用?目标类的对象都已经new出来了,直接调用其方法不就行了,还要进行这复杂的操作,又是写接口又是导包的。
确实,现在我们没有需求只是简单接触代理当然感觉没用。就像刚学反射机制的时候我也这么想,后来用反射机制的时候那真是香的一X。
存在即合理。这种机制既然Java前辈为我们辛辛苦苦(代理和字节码有关)创造出来了,那么它后面一定会大放异彩的。
先简单说下目前我能想到的作用!
- 可以在参数那里偷梁换柱!
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object result = null;
System.out.println("前置拦截");
try {
args = new Object[] {Integer.valueOf(98)}; //可以在这里改参数
result = method.invoke(object, args);
System.out.println("后置拦截");
} catch(Throwable e) {
System.out.println("异常拦截");
throw e;
}
return result;
}
public class Demo {
public static void main(String[] args) {
SomeClass someClass = new SomeClass();
SomeClass cglibProxy = CglibProxy.getCglibProxy(someClass);
String result = cglibProxy.fun2(100);
System.out.println(result);
}
}
/*输出结果
前置拦截
执行fun2()
后置拦截
num为98
*/
可以看到我明明传进去的是100,但是可以在代理里改相关的参数。用户传递参数过来,我们做事需要用这些参数做一些封装和调整。
- 相关的拦截器
-
前置拦截:例如判断用户是否有权限进行这个方法的调用,没有权限就可以阻止该方法的执行。
-
后置拦截:记录哪个用户哪个时间做了什么的详细过程。具体可以写到外存做成日志。
-
异常拦截:方法执行出现异常,执行需要做的事。
四:代理使用场景
-
RMI短连接,就是用的JDKProxy,因为接口成为了客户端和服务器之间的必须遵守的规范!接口是规范,是约束!
-
AOP。非侵入式的功能扩充。不改变原来代码的任何内容,可以对功能进行增加(体现在前后置拦截)。满足开闭原则:对修改关闭对扩展开放!
这些都是后面博文所要涉及到的问题,等我写到了再把代理拿出来一起讲,理解就更深了。
五:后记(Q&A)
对于学习代理机制我有以下思考的问题。
-
问题一:既然Cglib代理能调用所有可以被继承的方法,那也包括接口所给的方法,这样看来Cglib代理比JDK代理好多了,只用Cglib“以偏概全”不行吗?
答:Cglib确实在使用的时候看起来能力更多点,但是能力越大责任越多,安全因素也就更多。Cglib代理这种能力大的代理在“工程控制”角度而言是有害的,会导致滥用的问题。而JDK因为使用接口正好补上了这个缺陷。接口是简单的,简单就是安全!再次强调接口是规范,接口是约束!规定了该做什么不该做什么,接口是面向未来的,为未来的工程考虑的!
-
问题二:在来解释下博文开始提到的:用户用的是这个代理对象,真正的操作还是用的目标对象。该如何理解?
答:从上面的所有例子我们就可以看到。调用方法的时候都是
method.invoke(object, args)
,这个object
就是我们传参传过来的需要代理的目标类的对象。所以再次强调,当我们使用代理对象执行方法的时候,其内部调用方法的对象其实还是原对象,并不是代理对象执行该方法! 因为你必须是原对象调用方法才会对原对象产生相应的改变啊,代理对象可以看成就是AOP(面向切面编程),切点就是执行的方法,可以切前扩展(前置拦截),切后扩展(后置拦截)!
静态代理和动态代理的区别。
静态代理的代码需要在编译前自己写好(例:UserDaoProxy),也正因为此造成了改一处改多出的情况,导致代码维护起来不容易。动态代理,在运行期默默帮你写了(例JDK的$Proxy0.class),不用明确定义代理类,动态生成代理对象给你。
JDK代理具体分析
要想弄明白JDK动态代理,将它动态生成的匿名类$Proxy0.class
文件反编译成源码分析一下就可以得到。与字节码有关的知识。JDK代理为什么必须强调接口。因为只有给定接口,JDK的Proxy经过一系列操作(asm操作)动态生成的匿名类$Proxy0.class文件才能把相关方法填进去。(实验得知。任何Java生成的class文件二进制开头cafe baby)
反射
所谓反射,不需要看到源码,通过分析通过classloader加载到内存的二进制字节码,可以知道这个类的一些属性和方法。
动态
Java是动态语言,动态意义就是在运行过程中可以对类的相关方法属性进行操作,添加或删除。
cglib问题
cg(codeGenerate)代码生成。从这单词可以得知依然与字节码有关。cglib不用接口,因为分析可得知它生成的是被代理类的子类,就是对被代理类的所有方法进行拦截。
cglib底层还是使用的asm。
AOP初识
例子:业务逻辑代码都写好了。 生产 ,库存,订单。 突然有需求,要求在所有的业务逻辑代码的所有操作上都加上权限检查,没有权限不能进行方法的操作。 怎么做?第一种方法是找到所有方法,对所有方法加操作。这种太麻烦。
第二种解决方法生成动态代理,每一个业务逻辑类生成动态代理,再写相关权限检查代码,代理做相关方法时将检查代码切入。
aop 写好可以切的类和代码。通过配置文件,要对哪个类和哪个方法往里切,顺序问题都在配置文件写好。