前言
最近正在学习Spring,和以往一样,学习资料的来源依然是万能的3W。作为一个非专业IT人,以往的我并没有整理笔记的习惯,但最近的记忆力给我带来了些许困扰。于是本人的博客之旅就这样开始了…
作为第一篇博文,为了不打击自己的积极性,内容上主要参考了知乎大大bravo1988,若有侵权望告知。
言归正传,Spring的核心是控制反转(IOC)和面向切面(AOP),而AOP的底层实现便是本文的核心–动态代理。
1.代理模式
百度百科中代理模式是这样定义的:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
代理模式的UML图如下:
代理模式一般分为静态代理和动态代理,为了直观的描述静态代理和动态代理,下面以一个例子来说明。
假设我们现在有个类Student如下:
public class Student {
public String getSex(int stu_id){
String sex = stu_id==1?"Girl":"ToDo";
return sex;
}
public int getAge(int stu_id){
int age = stu_id==1?15:-1;
return age;
}
}
现在要完成如下需求:在对象的每个方法前后加上方法开始及结束说明。
1.1静态代理
静态代理指在程序运行前已经存在代理类的字节码文件,静态代理的原理可以用下图表示。
为了完成上述需求,计划定义如下三个类
1.Student接口Interface Student
2.Student接口实现类StudentImpl(真实对象类)
3.Student接口代理类StudentProxy(代理对象类)
public interface Student {
public String getSex(int stu_id);
public int getAge(int stu_id);
}
public class StudentImpl implements Student{
public String getSex(int stu_id){
String sex = stu_id==1?"Girl":"ToDo";
return sex;
}
public int getAge(int stu_id){
int age = stu_id==1?15:-1;
return age;
}
}
public class StudentProxy implements Student{
public Student realSubject;
public StudentProxy(Student realSubject){
this.realSubject = realSubject;
}
public String getSex(int stu_id){
System.out.println("getSex方法开始");
String sex = realSubject.getSex(stu_id);
System.out.println("getSex方法结束");
return sex;
}
public int getAge(int stu_id){
System.out.println("getAge方法开始");
int age = realSubject.getAge(stu_id);
System.out.println("getAge方法开始");
return age;
}
}
下面开始测试
public class StudentStaticProxyTest {
public static void main(String args[]){
Student student = new StudentProxy(new StudentImpl());
int stu_id=1;
String sex = student.getSex(stu_id);
System.out.println(stu_id+"号学生性别"+sex);
int age = student.getAge(stu_id);
System.out.println(stu_id+"号学生年龄为"+age);
}
}
测试结果如下
通过静态代理完成了需求,但从实现的方法看静态代理存在着如下缺陷:
1.每个方法都需要添加说明,若方法有很多个,代理类编码效率低
2.当需求改变时,代理类每个方法都要改变,不易修改维护
3.一个代理类只能实现一种接口的代理,如果有许多接口需要代理,工作量较大
通过上述分析,代理类只是用于生成代理对象,代理对象才是最终需求,那么能不能直接通过对象接口动态生成代理对象呢?
其实这些问题前辈们早已做了探索,JDK中便存在着这样的方法,接下来将详细介绍JDK动态代理。
1.2 JDK动态代理
同样还是以上文提及的需求为例开始介绍。在介绍静态代理的时候,为了实现Student类的代理,编写了StudentProxy类,这正是静态代理存在着的缺陷,需要为每一个代理对象编写代理类。那么能不能不通过定义代理类直接生成代理对象呢?
这里先回顾一下java对象是如何生成的。
由此可见对象是由class字节码生成的,这刚好解决了上述问题,可以不通过定义代理类直接由class加载到内存生成的字节码生成对象,找啊找,终于在jdk中找到了直接生成Class的方法。
但仔细查看,又遇到了新问题,Class的构造方法是Private类型,外部无法调用,看来只能找其他的方法了。
又开始了jdk探索之旅,看看JDK中有没有方法可以通过代理接口类得到class,不负众望,在Proxy类中果然存在着gteProxyClass这样一个静态方法。
和当初期望的一样,只需传入代理对象接口的类加载器,代理对象的接口无需代理类就可以返回一个代理对象。有了这个方法,下面可以测试了。
首先做一个准备工作,看一下真实对象的接口及实现类的Class
public class ClassTest {
public static void main(String[] args) {
/*Student接口的Class对象 得到Class对象的三种方式:
* 1.Class.forName(xxx)
* 2.xxx.class
* 3.xxx.getClass()
* 注意,这并不是我们new了一个Class对象,而是让虚拟机加载并创建Class对象
*/
Class<Student> studentClass = Student.class;
//Student接口的构造器信息
Constructor[] studentClassConstructors = studentClass.getConstructors();
//Student接口的方法信息
Method[] studentClassMethods = studentClass.getMethods();
//打印
System.out.println("------接口Class的构造器信息------");
printClassInfo(studentClassConstructors);
System.out.println("------接口Class的方法信息------");
printClassInfo(studentClassMethods);
//Student实现类的Class对象
Class<StudentImpl> studentImplClass = StudentImpl.class;
//Student实现类的构造器信息
Constructor<?>[] studentImplClassConstructors = studentImplClass.getConstructors();
//Student实现类的方法信息
Method[] studentImplClassMethods = studentImplClass.getMethods();
//打印
System.out.println("------实现类Class的构造器信息------");
printClassInfo(studentImplClassConstructors);
System.out.println("------实现类Class的方法信息------");
printClassInfo(studentImplClassMethods);
}
public static void printClassInfo(Executable[] targets){
for (Executable target : targets) {
// 构造器/方法名称
String name = target.getName();
StringBuilder sBuilder = new StringBuilder(name);
// 拼接左括号
sBuilder.append('(');
Class[] clazzParams = target.getParameterTypes();
// 拼接参数
for(Class clazzParam : clazzParams){
sBuilder.append(clazzParam.getName()).append(','); }
//删除最后一个参数的逗号
if(clazzParams!=null && clazzParams.length != 0)
{ sBuilder.deleteCharAt(sBuilder.length()-1); }
//拼接右括号
sBuilder.append(')');
//打印 构造器/方法
System.out.println(sBuilder.toString());
}
}
}
从上述结果可以看出接口Class没有构造方法,实现类Class除了有接口的方法还有从Object类继承的方法
接着用getProxyClass进行测试
public class StudentDynamicProxyTest {
public static void main(String[] args) {
Class studentProxyClass = Proxy.getProxyClass(
Student.class.getClassLoader(), Student.class);
//以Student实现类的Class对象作对比,看看代理Class是什么类型
System.out.println(StudentImpl.class.getName());
System.out.println(studentProxyClass.getName());
//打印代理Class对象的构造器
Constructor[] constructors = studentProxyClass.getConstructors();
System.out.println("----构造器----");
printClassInfo(constructors);
//打印代理Class对象的方法
Method[] methods = studentProxyClass.getMethods();
System.out.println("----方法----");
printClassInfo(methods); }
public static void printClassInfo(Executable[] targets) {
for (Executable target : targets) {
// 构造器/方法名称
String name = target.getName();
StringBuilder sBuilder = new StringBuilder(name);
// 拼接左括号
sBuilder.append('(');
Class[] clazzParams = target.getParameterTypes();
// 拼接参数
for (Class clazzParam : clazzParams) {
sBuilder.append(clazzParam.getName()).append(','); }
//删除最后一个参数的逗号
if (clazzParams != null && clazzParams.length != 0)
{ sBuilder.deleteCharAt(sBuilder.length() - 1); }
//拼接右括号
sBuilder.append(')');
//打印 构造器/方法
System.out.println(sBuilder.toString());
}
}
}
从上述结果可以看出通过给Proxy.getProxyClass()传入对象接口加载器和接口类,得到了一个强化版的Class:即包含接口的方法信息getAge()、getSex(),又包含了构造器$Proxy0(InvocationHandler),还有一些自己特有的方法以及从Object继承的方法。
既然这个Class既有方法信息,又有构造器,下一步通过它生成一个代理对象
public class StudentDynamicProxyTest {
public static void main(String[] args) throws Throwable{
Class studentProxyClass = Proxy.getProxyClass(
Student.class.getClassLoader(), Student.class);
Constructor constructor = studentProxyClass.getConstructor(InvocationHandler.class);
Student studentProxyImpl = (Student)constructor.newInstance(new InvocationHandler(){
@Override
public Object invoke(Object proxy ,Method method,Object[] args){
return null;
}
});
System.out.println(studentProxyImpl.getAge(1));
System.out.println(studentProxyImpl.getSex(1));
}
}
居然报了空指针错误,联想到前面,只有在nvocationHandler的invoke()返回null,难道是因为这个?带着疑问,编写如下测试类
public class StudentDynamicProxyTest {
public static void main(String[] args) throws Throwable{
Class studentProxyClass = Proxy.getProxyClass(
Student.class.getClassLoader(), Student.class);
Constructor constructor = studentProxyClass.getConstructor(InvocationHandler.class);
Student studentProxyImpl = (Student)constructor.newInstance(new InvocationHandler(){
@Override
public Object invoke(Object proxy ,Method method,Object[] args){
return 1;
}
});
System.out.println(studentProxyImpl.getAge(1));
System.out.println(studentProxyImpl.getSex(1));
}
}
将返回值改为1后,getAge方法不报错,getSex方法报错,并报出Integer无法转为String的错误,据此推测每次调用代理对象的方法都会调用invoke,而且invoke的返回值就是代理方法的返回值,正是因为getAge返回值为整型,而invoke返回值被设为1所以此方法不报错,而返回值为String的getSex方法报错。
通过推理动态代理的Class大致如下
这样设计有一个明显的有点,方法与方法体分离耦合性降低。
但是最终的需求是调用真实对象的方法,那么如何调用呢?注意到invoke函数的后面两个参数,方法及方法参数,由于代理对象和真实对象继承于同一个接口,方法是同名的,可以通过方法的反射调用执行真实对象,具体测试如下。
public class Test3 {
public static void main(String[] args) throws Throwable {
StudentImpl target = new StudentImpl();
//传入目标对象
//目的:1.根据它实现的接口生成代理对象 2.代理对象调用目标对象方法
Student studentProxy = (Student) getProxy(target);
studentProxy.getAge(1);
studentProxy.getSex(1);
}
private static Object getProxy(final Object target) throws Exception {
//参数1:随便找个类加载器给它, 参数2:目标对象实现的接口,让代理对象实现相同接口
Class proxyClass = Proxy.getProxyClass(
target.getClass().getClassLoader(), target.getClass().getInterfaces());
Constructor constructor = proxyClass.getConstructor(InvocationHandler.class);
Object proxy = constructor.newInstance(new InvocationHandler() {
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "方法开始");
Object result = method.invoke(target, args);
System.out.println(result); System.out.println(method.getName() + "方法结束");
return result; } });
return proxy; }
}
到这jdk动态代理的原理基本了解了,当然前辈们早已准备了更简单的方法
此时是不是顿时感觉神清气爽,但是方法的提示依旧在invoke函数体中,耦合性大,不利于后期扩展,那么有没有好的办法能将其抽离呢?答案是肯定的
定义一个Log接口及实现类
public interface Log {
public void beforeMethod(Method method);
public void afterMethod(Method method);
}
public class LogImpl implements Log{
public void beforeMethod(Method method){
System.out.println(method.getName() + "方法开始");
};
public void afterMethod(Method method){
System.out.println(method.getName() + "方法结束");
};
}
测试类如下
public class Test4 {
public static void main(String[] args) throws Throwable {
StudentImpl target = new StudentImpl();
//传入目标对象
//目的:1.根据它实现的接口生成代理对象 2.代理对象调用目标对象方法
Student studentProxy = (Student) getProxy(target,new LogImpl());
studentProxy.getAge(1);
studentProxy.getSex(1);
}
private static Object getProxy(final Object target ,Log log) throws Exception {
/*代理对象的方法最终都会被JVM导向它的invoke方法*/
Object proxy = Proxy.newProxyInstance(
target.getClass().getClassLoader(),/*类加载器*/
target.getClass().getInterfaces(),/*让代理对象和目标对象实现相同接口*/
(proxy1, method, args) -> {
log.beforeMethod(method);
Object result = method.invoke(target, args);
System.out.println(result);
log.afterMethod(method);
return result;
}
);
return proxy;
}
}
其实Spring的AOP实现原理大概也就是这样了,只是细节更复杂一点。