Spring之AOP(代理)
AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
一、为什么需要代理模式
假设需实现一个计算的类Math、完成加、减、乘、除功能,如下所示:
package com.zhangguo.Spring041.aop01;
public class Math {
//加
public int add(int n1,int n2){
int result=n1+n2;
System.out.println(n1+"+"+n2+"="+result);
return result;
}
//减
public int sub(int n1,int n2){
int result=n1-n2;
System.out.println(n1+"-"+n2+"="+result);
return result;
}
//乘
public int mut(int n1,int n2){
int result=n1*n2;
System.out.println(n1+"X"+n2+"="+result);
return result;
}
//除
public int div(int n1,int n2){
int result=n1/n2;
System.out.println(n1+"/"+n2+"="+result);
return result;
}
}
现在需求发生了变化,要求项目中所有的类在执行方法时输出执行耗时。最直接的办法是修改源代码,如下所示:
package com.zhangguo.Spring041.aop01;
import java.util.Random;
public class Math {
//加
public int add(int n1,int n2){
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=n1+n2;
System.out.println(n1+"+"+n2+"="+result);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//减
public int sub(int n1,int n2){
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=n1-n2;
System.out.println(n1+"-"+n2+"="+result);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//乘
public int mut(int n1,int n2){
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=n1*n2;
System.out.println(n1+"X"+n2+"="+result);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//除
public int div(int n1,int n2){
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=n1/n2;
System.out.println(n1+"/"+n2+"="+result);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//模拟延时
public void lazy()
{
try {
int n=(int)new Random().nextInt(500);
Thread.sleep(n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试运行:
package com.zhangguo.Spring041.aop01;
public class Test {
@org.junit.Test
public void test01()
{
Math math=new Math();
int n1=100,n2=5;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
math.div(n1, n2);
}
}
运行结果:
缺点:
1、工作量特别大,如果项目中有多个类,多个方法,则要修改多次。
2、违背了设计原则:开闭原则(OCP),对扩展开放,对修改关闭,而为了增加功能把每个方法都修改了,也不便于维护。
3、违背了设计原则:单一职责(SRP),每个方法除了要完成自己本身的功能,还要计算耗时、延时;每一个方法引起它变化的原因就有多种。
4、违背了设计原则:依赖倒转(DIP),抽象不应该依赖细节,两者都应该依赖抽象。而在Test类中,Test与Math都是细节。
使用静态代理可以解决部分问题。
二、静态代理
1、定义抽象主题接口。
package com.zhangguo.Spring041.aop02;
/**
* 接口
* 抽象主题
*/
public interface IMath {
//加
int add(int n1, int n2);
//减
int sub(int n1, int n2);
//乘
int mut(int n1, int n2);
//除
int div(int n1, int n2);
}
2、主题类,算术类,实现抽象接口。
package com.zhangguo.Spring041.aop02;
/**
* 被代理的目标对象
*真实主题
*/
public class Math implements IMath {
//加
public int add(int n1,int n2){
int result=n1+n2;
System.out.println(n1+"+"+n2+"="+result);
return result;
}
//减
public int sub(int n1,int n2){
int result=n1-n2;
System.out.println(n1+"-"+n2+"="+result);
return result;
}
//乘
public int mut(int n1,int n2){
int result=n1*n2;
System.out.println(n1+"X"+n2+"="+result);
return result;
}
//除
public int div(int n1,int n2){
int result=n1/n2;
System.out.println(n1+"/"+n2+"="+result);
return result;
}
}
3、代理类
package com.zhangguo.Spring041.aop02;
import java.util.Random;
/**
* 静态代理类
*/
public class MathProxy implements IMath {
//被代理的对象
IMath math=new Math();
//加
public int add(int n1, int n2) {
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=math.add(n1, n2);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//减法
public int sub(int n1, int n2) {
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=math.sub(n1, n2);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//乘
public int mut(int n1, int n2) {
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=math.mut(n1, n2);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//除
public int div(int n1, int n2) {
//开始时间
long start=System.currentTimeMillis();
lazy();
int result=math.div(n1, n2);
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//模拟延时
public void lazy()
{
try {
int n=(int)new Random().nextInt(500);
Thread.sleep(n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4、测试运行
package com.zhangguo.Spring041.aop02;
public class Test {
IMath math=new MathProxy();
@org.junit.Test
public void test01()
{
int n1=100,n2=5;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
math.div(n1, n2);
}
}
5、小结
通过静态代理,是否完全解决了上述的4个问题:
已解决:
5.1、解决了“开闭原则(OCP)”的问题,因为并没有修改Math类,而扩展出了MathProxy类。
5.2、解决了“依赖倒转(DIP)”的问题,通过引入接口。
5.3、解决了“单一职责(SRP)”的问题,Math类不再需要去计算耗时与延时操作,但从某些方面讲MathProxy还是存在该问题。
未解决:
5.4、如果项目中有多个类,则需要编写多个代理类,工作量大,不好修改,不好维护,不能应对变化。
如果要解决上面的问题,可以使用动态代理。
三、动态代理,使用JDK内置的Proxy实现
只需要一个代理类,而不是针对每个类编写代理类。
在上一个示例中修改代理类MathProxy如下:
package com.zhangguo.Spring041.aop03;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Random;
/**
* 动态代理类
*/
public class DynamicProxy implements InvocationHandler {
//被代理的对象
Object targetObject;
/**
* 获得被代理后的对象
* @param object 被代理的对象
* @return 代理后的对象
*/
public Object getProxyObject(Object object){
this.targetObject=object;
return Proxy.newProxyInstance(
targetObject.getClass().getClassLoader(), //类加载器
targetObject.getClass().getInterfaces(), //获得被代理对象的所有接口
this); //InvocationHandler对象
//loader:一个ClassLoader对象,定义了由哪个ClassLoader对象来生成代理对象进行加载
//interfaces:一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
//h:一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上,间接通过invoke来执行
}
/**
* 当用户调用对象中的每个方法时都通过下面的方法执行,方法必须在接口
* proxy 被代理后的对象
* method 将要被执行的方法信息(反射)
* args 执行方法时需要的参数
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//被织入的内容,开始时间
long start=System.currentTimeMillis();
lazy();
//使用反射在目标对象上调用方法并传入参数
Object result=method.invoke(targetObject, args);
//被织入的内容,结束时间
Long span= System.currentTimeMillis()-start;
System.out.println("共用时:"+span);
return result;
}
//模拟延时
public void lazy()
{
try {
int n=(int)new Random().nextInt(500);
Thread.sleep(n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试运行:
package com.zhangguo.Spring041.aop03;
public class Test {
//实例化一个MathProxy代理对象
//通过getProxyObject方法获得被代理后的对象
IMath math=(IMath)new DynamicProxy().getProxyObject(new Math());
@org.junit.Test
public void test01()
{
int n1=100,n2=5;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
math.div(n1, n2);
}
IMessage message=(IMessage) new DynamicProxy().getProxyObject(new Message());
@org.junit.Test
public void test02()
{
message.message();
}
}
小结:
JDK内置的Proxy动态代理可以在运行时动态生成字节码,而没必要针对每个类编写代理类。中间主要使用到了一个接口InvocationHandler与Proxy.newProxyInstance静态方法,参数说明如下:
使用内置的Proxy实现动态代理有一个问题:被代理的类必须实现接口,未实现接口则没办法完成动态代理。
如果项目中有些类没有实现接口,则不应该为了实现动态代理而刻意去抽出一些没有实例意义的接口,通过cglib可以解决该问题。
四、动态代理,使用cglib实现
CGLIB(Code Generation Library)是一个开源项目,是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口,通俗说cglib可以在运行时动态生成字节码。
4.1、引用cglib,通过maven
修改pom.xml文件,添加依赖
保存pom.xml配置文件,将自动从共享资源库下载cglib所依赖的jar包,主要有如下几个:
4.2、使用cglib完成动态代理,大概的原理是:cglib继承被代理的类,重写方法,织入通知,动态生成字节码并运行,因为是继承所以final类是没有办法动态代理的。具体实现如下:
package com.zhangguo.Spring041.aop04;
import java.lang.reflect.Method;
import java.util.Random;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/*
* 动态代理类
* 实现了一个方法***接口
*/
public class DynamicProxy implements MethodInterceptor {
// 被代理对象
Object targetObject;
//Generate a new class if necessary and uses the specified callbacks (if any) to create a new object instance.
//Uses the no-arg constructor of the superclass.
//动态生成一个新的类,使用父类的无参构造方法创建一个指定了特定回调的代理实例
public Object getProxyObject(Object object) {
this.targetObject = object;
//增强器,动态代码生成器
Enhancer enhancer=new Enhancer();
//回调方法
enhancer.setCallback(this);
//设置生成类的父类类型
enhancer.setSuperclass(targetObject.getClass());
//动态生成字节码并返回代理对象
return enhancer.create();
}
// 拦截方法
public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// 被织入的横切内容,开始时间 before
long start = System.currentTimeMillis();
lazy();
// 调用方法
Object result = methodProxy.invoke(targetObject, args);
// 被织入的横切内容,结束时间
Long span = System.currentTimeMillis() - start;
System.out.println("共用时:" + span);
return result;
}
// 模拟延时
public void lazy() {
try {
int n = (int) new Random().nextInt(500);
Thread.sleep(n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
参数:Object为由CGLib动态生成的代理类实例,Method为上文中实体类所调用的被代理的方法引用,Object[]为参数值列表,MethodProxy为生成的代理类对方法的代理引用。
返回:从代理实例的方法调用返回的值。
测试运行:
package com.zhangguo.Spring041.aop04;
public class Test {
//实例化一个DynamicProxy代理对象
//通过getProxyObject方法获得被代理后的对象
Math math=(Math)new DynamicProxy().getProxyObject(new Math());
@org.junit.Test
public void test01()
{
int n1=100,n2=5;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
math.div(n1, n2);
}
//另一个被代理的对象,不再需要重新编辑代理代码
Message message=(Message) new DynamicProxy().getProxyObject(new Message());
@org.junit.Test
public void test02()
{
message.message();
}
}
运行结果:
4.3、小结
使用cglib可以实现动态代理,即使被代理的类没有实现接口,但被代理的类必须不是final类。