AOP(Aspect Oriented Programming),即面向切面编程,可以处理很多事情,常见的功能比如日志记录,性能统计,安全控制,事务处理,异常处理等。
AOP可以认为是一种更高级的“复用”技术,它是OOP(Object Oriented Programming,面向对象编程)的补充和完善。AOP的理念,就是将分散在各个业务逻辑代码中相同的代码通过横向切割的方式抽取到一个独立的模块中。将相同逻辑的重复代码横向抽取出来,使用动态代理技术将这些重复代码织入到目标对象方法中,实现和原来一样的功能。这样一来,我们在写业务逻辑时就只关心业务代码。
OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。
所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。
业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,它们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
下面参考自网上图片,可以比较直观地理解上述这几个AOP术语和流转过程。
(1)动态代理使用动态代理可以为一个或多个接口在运行期动态生成实现对象,生成的对象中实现接口的方法时可以添加增强代码,从而实现AOP:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 动态代理类
*/
public class DynamicProxy implements InvocationHandler {
/**
* 需要代理的目标类
*/
private Object target;
/**
* 写法固定,aop专用:绑定委托对象并返回一个代理类
*
* @param target
* @return
*/
public Object bind(Object target) {
this.target = target;
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
/**
* 调用 InvocationHandler接口定义方法
*
* @param proxy 指被代理的对象。
* @param method 要调用的方法
* @param args 方法调用时所需要的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
// 切面之前执行
System.out.println("[动态代理]切面之前执行");
// 执行业务
result = method.invoke(target, args);
// 切面之后执行
System.out.println("[动态代理]切面之后执行");
return result;
}
}
缺点是只能针对接口进行代理,同时由于动态代理是通过反射实现的,有时可能要考虑反射调用的开销,否则很容易引发性能问题。
(2)字节码生成
动态字节码生成技术是指在运行时动态生成指定类的一个子类对象(注意是针对类),并覆盖其中特定方法,覆盖方法时可以添加增强代码,从而实现AOP。
最常用的工具是CGLib:
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 使用cglib动态代理
*
* JDK中的动态代理使用时,必须有业务接口,而cglib是针对类的
*/
public class CglibProxy implements MethodInterceptor {
private Object target;
/**
* 创建代理对象
*
* @param target
* @return
*/
public Object getInstance(Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(this.target.getClass());
// 回调方法
enhancer.setCallback(this);
// 创建代理对象
return enhancer.create();
}
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object result = null;
System.out.println("[cglib]切面之前执行");
result = methodProxy.invokeSuper(proxy, args);
System.out.println("[cglib]切面之后执行");
return result;
}
}
(3)定制的类加载器
当需要对类的所有对象都添加增强,动态代理和字节码生成本质上都需要动态构造代理对象,即最终被增强的对象是由AOP框架生成,不是开发者new出来的。
解决的办法就是实现自定义的类加载器,在一个类被加载时对其进行增强。
JBoss就是采用这种方式实现AOP功能。
这种方式目前只是道听途说,本人没有在实际项目中实践过。
(4)代码生成
利用工具在已有代码基础上生成新的代码,其中可以添加任何横切代码来实现AOP。
(5)语言扩展
可以对构造方法和属性的赋值操作进行增强,AspectJ是采用这种方式实现AOP的一个常见的Java语言扩展。
比较:根据日志,上述流程的执行顺序依次为:过滤器、拦截器、AOP方法认证、AOP类认证。
附:记录API日志
最后通过记录API日志,记录日志时加入API耗时统计(其实我们在开发.NET应用的过程中通过AOP这种记录日志的方式也已经是标配),加深上述AOP的几个核心概念的理解:
package com.power.demo.controller.tool;
import com.power.demo.apientity.BaseApiRequest;
import com.power.demo.apientity.BaseApiResponse;
import com.power.demo.util.DateTimeUtil;
import com.power.demo.util.SerializeUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
/**
* 服务日志切面,主要记录接口日志及耗时
**/
@Aspect
@Component
public class SvcLogAspect {
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void requestMapping() {
}
@Pointcut("execution(* com.power.demo.controller.*Controller.*(..))")
public void methodPointCut() {
}
@Around("requestMapping() && methodPointCut()")
public Object around(ProceedingJoinPoint pjd) throws Throwable {
System.out.println("Spring AOP方式记录服务日志");
Object response = null;//定义返回信息
BaseApiRequest baseApiRequest = null;//请求基类
int index = 0;
Signature curSignature = pjd.getSignature();
String className = curSignature.getClass().getName();//类名
String methodName = curSignature.getName(); //方法名
Logger logger = LoggerFactory.getLogger(className);//日志
StopWatch watch = DateTimeUtil.StartNew();//用于统计调用耗时
// 获取方法参数
Object[] reqParamArr = pjd.getArgs();
StringBuffer sb = new StringBuffer();
//获取请求参数集合并进行遍历拼接
for (Object reqParam : reqParamArr) {
if (reqParam == null) {
index++;
continue;
}
try {
sb.append(SerializeUtil.Serialize(reqParam));
//获取继承自BaseApiRequest的请求实体
if (baseApiRequest == null && reqParam instanceof BaseApiRequest) {
index++;
baseApiRequest = (BaseApiRequest) reqParam;
}
} catch (Exception e) {
sb.append(reqParam.toString());
}
sb.append(",");
}
String strParam = sb.toString();
if (strParam.length() > 0) {
strParam = strParam.substring(0, strParam.length() - 1);
}
//记录请求
logger.info(String.format("【%s】类的【%s】方法,请求参数:%s", className, methodName, strParam));
response = pjd.proceed(); // 执行服务方法
watch.stop();
//记录应答
logger.info(String.format("【%s】类的【%s】方法,应答参数:%s", className, methodName, SerializeUtil.Serialize(response)));
// 获取执行完的时间
logger.info(String.format("接口【%s】总耗时(毫秒):%s", methodName, watch.getTotalTimeMillis()));
//标准请求-应答模型
if (baseApiRequest == null) {
return response;
}
if ((response != null && response instanceof BaseApiResponse) == false) {
return response;
}
System.out.println("Spring AOP方式记录标准请求-应答模型服务日志");
Object request = reqParamArr[index];
BaseApiResponse bizResp = (BaseApiResponse) response;
//记录日志
String msg = String.format("请求:%s======应答:%s======总耗时(毫秒):%s", SerializeUtil.Serialize(request),
SerializeUtil.Serialize(response), watch.getTotalTimeMillis());
if (bizResp.getIsOK() == true) {
logger.info(msg);
} else {
logger.error(msg);//记录错误日志
}
return response;
}
}
标准的请求-应答模型,我们都会定义请求基类和应答基类,本文示例给到的是BaseApiRequest和BaseApiResponse,搜集日志时,可以对错误日志加以区分特殊处理。
注意上述代码中的@Around环绕通知,参数类型是ProceedingJoinPoint,而前面第一个示例的@Before前置通知,参数类型是JoinPoint。
下面是AspectJ通知和增强的5种模式:
@Before前置通知,在目标方法执行前实施增强,请求参数JoinPoint,用来连接当前连接点的连接细节,一般包括方法名和参数值。在方法执行前进行执行方法体,不能改变方法参数,也不能改变方法执行结果。
@After 后置通知,请求参数JoinPoint,在目标方法执行之后,无论是否发生异常,都进行执行的通知。在后置通知中,不能访问目标方法的执行结果(因为有可能发生异常),不能改变方法执行结果。
@AfterReturning 返回通知,在目标方法执行后实施增强,请求参数JoinPoint,其能访问方法执行结果(因为正常执行)和方法的连接细节,但是不能改变方法执行结果。(注意和后置通知的区别)
@AfterThrowing 异常通知,在方法抛出异常后实施增强,请求参数JoinPoint,throwing属性代表方法体执行时候抛出的异常,其值一定与方法中Exception的值需要一致。
@Around 环绕通知,请求参数ProceedingJoinPoint,环绕通知类似于动态代理的全过程,ProceedingJoinPoint类型的参数可以决定是否执行目标方法,而且环绕通知必须有返回值,返回值即为目标方法的返回值。