本篇文章从Java的代理机制讲到Spring的AOP
1、代理模式
代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能
举一个生活中的例子来简单理解一下:小马租房遇到黑心中介,实在忍受不了的小马就委托律师来为他处理租房手续并将中介告上法庭。此时,小马就是委托方,律师就是代理方,小马将自己的诉求告诉律师,律师会向法院传达小马的想法并引用法律条文。而律师也可以为不同的委托方进行代理。
2、静态代理
代理类在编译阶段生成,程序运行前就已经存在,那么这种代理方式被成为静态代理,这种情况下的代理类通常都是我们在Java代码中定义的。
我们先给出一个接口ISpeak:
public interface ISpeak {
public void speak();
}
小马类:需要实现这个接口
public class XiaoMa implements ISpeak {
@Override
public void speak() {
System.out.println("黑心中介太可恶了!");
}
}
小马的律师类:也需要实现这个接口,并添加律师所要执行的业务
public class XiaomaLawyer implements ISpeak {
private XiaoMa xiaoma =new XiaoMa();
@Override
public void speak() {
System.out.println("引用法律条文");
xiaoma.speak();
System.out.println("这样做是违反法律的!");
}
}
Test测试类:
public class Test {
public static void main(String[] args) {
ISpeak speaker = new XiaomaLawyer();
speaker.speak();
}
}
通过测试结果可以看出,我们完成了一次简单的静态代理。
那么我们来说说,静态代理的优缺点:
优点:业务类可以只关注自身逻辑,可以重用,通过代理类来增加通用的逻辑处理。
缺点:如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度
如果要按照上述的方法使用代理模式,那么真实角色(委托类例如小马)必须是事先已经存在的,并将其作为代理对象的内部属性。但是实际使用时,一个真实角色必须对应一个代理角色,如果大量使用会导致类的急剧膨胀。
此外,如果事先并不知道真实角色(委托类),该如何使用代理呢?这些问题可以通过Java的动态代理类来解决。
3、动态代理
动态代理类的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以不存在代理类的字节码文件。代理类和委托类的关系是在程序运行时确定。
3.1、JDK动态代理
JDK动态代理是利用反射机制生成代理接口的匿名类,即被代理的对象,必须实现了接口。
与上述静态代理一样,有一个ISpeak接口,与被代理类XiaoMa
接下来我们定义律师的JDKProxy类:
public class JDKProxy {
public JDKProxy() {
}
@SuppressWarnings("unchecked")
public static <T> T getProxy(T target) {//返回一个生成的代理对象
Class<?> klass = target.getClass();
return (T) Proxy.newProxyInstance(
/**
* 根据下面提供的信息,创建代理对象 在这个过程中,
* a.JDK会通过根据传入的参数信息动态地在内存中创建和.class文件等同的字节码
* b.然后根据相应的字节码转换成对应的class,
* c.然后调用newInstance()创建实例
*/
klass.getClassLoader(), //获取对应的ClassLoader
klass.getInterfaces(), //获取被代理对象实现的所有接口
new InvocationHandler() {//设置请求处理器,处理所有方法调用
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("前置拦截!引用法律条文");
Object object = method.invoke(target, args);
System.out.println("后置拦截!这样做是违反法律的");
return object;
}
});
}
}
将JDKProxy写成一个工具类后,在后续的测试及调用中会更加方便。
public class Test {
public static void main(String[] args) {
XiaoMa xiaoma = new XiaoMa();
ISpeak speaker = JDKProxy.getProxy(xiaoma);
speaker.speak();
}
}
我们来看一下JDK动态代理主要都做了什么:
Object object = Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
1、Proxy.newProxyInstance()获取XiaoMa类的所有接口列表(第二个参数:interfaces)
2、确定要生成的代理类的类名,默认为:com.sun.proxy.$ProxyXXXX
3、根据需要实现的接口信息,在代码中动态创建该Proxy类的字节码;
4、将对应的字节码转换为对应的class对象;
5、创建InvocationHandler实例handler,用来处理Proxy所有方法调用
6、Proxy的class对象以创建的handler对象为参数(第三个参数:invocationHandler),实例化一个Proxy对象
public Object invoke(Object proxy, Method method, Object[] args)
InvocationHandler,我们需要实现下列的invoke方法:在调用代理对象中的每一个方法时,在代码内部,都是直接调用了InvocationHandler的invoke方法,而invoke方法根据代理类传递给自己的method参数来区分是什么方法。可以看出,Proxy.newProxyInstance()方法生成的对象也是实现了ISpeak接口,所以可以在代码中将其强制转换为ShowService来使用,和静态代理到达了同样的效果。
3.2、CGLib动态代理
CGLib动态代理是针对类实现代理,主要是对指定的类生成一个子类,并且重写其中的方法。如果被代理类的方法被声明final类型,那么Cglib代理是无法正常工作的,因为final类型方法不能被重写。
cglib 创建某个类A的动态代理类的模式是:
1.查找A上的所有非final 的public类型的方法定义;
2.将这些方法的定义转换成字节码;
3.将组成的字节码转换成相应的代理的class对象;
4.实现 MethodInterceptor接口,用来处理对代理类上所有方法的请求(这个接口和JDK动态代理InvocationHandler的功能和角色是一样的)
我们给出一个不实现接口的XiaoYang类:
public class XiaoYang {
public void speak() {
System.out.println("租房套路深,一定要谨慎");
}
}
public class CGLIBProxy {
public CGLIBProxy() {
}
public static <T> T getProxy(T target) {
Enhancer enhancer = new Enhancer(); //cglib中加强器,用来创建动态代理
enhancer.setSuperclass(target.getClass());//设置要创建动态代理的类
enhancer.setCallback(new MethodInterceptor() {// // 设置回调,
//这里相当于是对于代理类上所有方法的调用,都会调用CallBack
//而Callback则需要实行intercept()方法进行拦截
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy arg3) throws Throwable {
System.out.println("前置拦截!引用法律条文");
Object object = method.invoke(target, args);
System.out.println("后置拦截!这样做是违反法律的");
return object;
}
});
@SuppressWarnings("unchecked")
T proxy = (T) enhancer.create();
return proxy;
}
}
通过以上实例可以看出,Cglib通过继承实现动态代理,具体类不需要实现特定的接口,而且代理类可以调用具体类的非接口方法,更加灵活。
4、Spring AOP
Spring框架的AOP机制可以让开发者把业务流程中的通用功能抽取出来,单独编写功能代码。在业务流程执行过程中,Spring框架会根据业务流程要求,自动把独立编写的功能代码切入到流程的合适位置。
例如,在一个业务系统中,用户登录是基础功能,凡是涉及到用户的业务流程都要求用户进行系统登录。如果把用户登录功能代码写入到每个业务流程中,会造成代码冗余,维护也非常麻烦,当需要修改用户登录功能时,就需要修改每个业务流程的用户登录代码,这种处理方式显然是不可取的。比较好的做法是把用户登录功能抽取出来,形成独立的模块,当业务流程需要用户登录时,系统自动把登录功能切入到业务流程中。
Spring AOP采用的是动态代理,在运行期间对业务方法进行增强,所以不会生成新类,对于动态代理技术,Spring AOP提供了对JDK动态代理的支持以及CGLib的支持,那么什么时候用哪种代理呢?
1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
2、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
实现AOP的方式有很多,下面我将用注解的形式来实现AOP:
切面类:
@Component("logAnnotation")//放入ioc容器
@Aspect //此类是个通知
public class LogAspectAnnotation {
@Before("execution(public * addStu(..))")
public void myBefore() {
System.out.println("注解形式的前置通知");
}
@After("execution(public * addStu(..))")
public void myAfter() {
System.out.println("注解形式的后置通知");
}
}
配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 自动开启注解 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<!-- 扫描器 -->
<context:component-scan base-package="stu.crayue.aop"></context:component-scan>
<bean id ="studentService" class="stu.crayue.service.StudentService"></bean>
</beans>
IStudentService接口:
public interface IStudentService {
void addStu(Student student);
}
被代理类:
public class StudentService implements IStudentService{
@Override
public void addStu(Student student) {
System.out.println("增加学生");
}
}
测试类:
public class Test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
IStudentService studentService=(IStudentService) context.getBean("studentService");
Student student =new Student();
student.setStuAge(23);
student.setStuName("dyp");
studentService.addStu(student);
}
}
测试结果:
4.1、Spring AOP到底使用哪个代理模式呢?
1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP
而要强制使用CGLIB代理,请将元素< aop:config >的属性proxy-target-class值设置为true
3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换