在理解Spring AOP
以及理清它与Aspect
和cglib
之间关系之前,有很多基础工作要做,比如,先对代理模式有个感性的认识。
动态代理与静态代理
代理模式
给某个对象提供一个代理对象,并由代理对象控制对于原对象的访问,即客户不直接操控原对象,而是通过代理对象间接地操控原对象。(代理模式的本质是控制对象访问,可以在具体的目标对象前后,附加很多操作,从而进行功能扩展)
代理模式的的实现方式有两种:
-
静态代理
代理类是在编译时就实现好的。也就是说Java
编译完成后代理类是一个实际的class
文件。 -
动态代理
代理类是在运行时生成的。也就是说Java
编译完之后并没有实际的class
文件,而是在运行时动态生成的类字节码,并加载到JVM
中。
静态代理代码示例
假如有接口UserService
和该接口的实现类UserServiceImpl
public interface UserService {
User getUser(String userId);
void updateUser(User user);
void deleteUser(String userId);
void addUser(User user);
}
public class UserServiceImpl implements UserService {
@Override
public User getUser(String userId) {
System.out.println("---getUser-----");
return null;
}
@Override
public void updateUser(User user) {
System.out.println("---updateUser-----");
}
@Override
public void deleteUser(String userId) {
System.out.println("---deleteUser-----");
}
@Override
public void addUser(User user) {
System.out.println("---addUser-----");
}
}
现在,假如要对UserService中的所有方法进行安全检查(即增加一个方法,假设为checkSecurity()
),则实现类UserServiceImpl的代码需要修该为这样:
public class UserServiceImpl implements UserService {
@Override
public User getUser(String userId) {
checkSecurity();
System.out.println("---getUser-----");
return null;
}
@Override
public void updateUser(User user) {
checkSecurity();
System.out.println("---updateUser-----");
}
@Override
public void deleteUser(String userId) {
checkSecurity();
System.out.println("---deleteUser-----");
}
@Override
public void addUser(User user) {
checkSecurity();
System.out.println("---addUser-----");
}
/**
* 校验安全性的方法
*/
private void checkSecurity(){
System.out.println("----checkSecurity-----");
}
}
假如以后还要对每个方法进行日志记录、缓存等,都需要对其中的每个方法添加相应的逻辑代码,给代码维护带来很大麻烦。
现在使用静态代理来解决该问题:为UserServiceImpl
添加一个代理类UserServiceImplProxy
;同时让这个代理类UserServiceImplProxy
实现UserService
的接口:
public class UserServiceImplProxy implements UserService {
private UserService userService;
//构造器
public UserServiceImplProxy(UserService userService){
this.userService = userService;
}
@Override
public User getUser(String userId) {
checkSecurity();
return userService.getUser(userId);
}
@Override
public void updateUser(User user) {
checkSecurity();
userService.updateUser(user);
}
@Override
public void deleteUser(String userId) {
checkSecurity();
userService.deleteUser(userId);
}
@Override
public void addUser(User user) {
checkSecurity();
userService.addUser(user);
}
/**
* 校验安全性的方法
*/
private void checkSecurity(){
System.out.println("----checkSecurity-----");
}
}
UserServiceImpl中还是原来的方法:
public class UserServiceImpl implements UserService {
@Override
public User getUser(String userId) {
System.out.println("---getUser-----");
return null;
}
@Override
public void updateUser(User user) {
System.out.println("---updateUser-----");
}
@Override
public void deleteUser(String userId) {
System.out.println("---deleteUser-----");
}
@Override
public void addUser(User user) {
System.out.println("---addUser-----");
}
}
从上面的代码可以看出,使用了静态代理模式之后,与User
紧密相关的增删改查方法都在UserServiceImpl
中,类似检查安全或日志记录等非User
相关操作都在代理类中实现。
当要实现User
相关的操作时,不使用UserServiceImpl
,而是通过代理UserServiceImpProxy
进行:
public static void main(String[] args) {
UserService userService = new UserServiceImplProxy(new UserServiceImpl());
userService.deleteUser("user的id");
}
静态代理虽然让User
相关操作在一个类中(UserServiceImpl
),非User
相关操作在另一个类中的(即一定程度提高代码内聚性),但需要为每个目标类(上面例子中的UserServiceImpl
)写一个代理类。假如我们有很多目标类都要实现同样的安全检查这类的操作,就要创建多个代理类,并且写很多重复的代码,显然我们需要更简便的方式。
动态代理示例(JDK
版本)
还是以上面的静态代理为基础,继续看动态代理是怎么解决上述问题的(动态代理的实现有多种:JDK
自带的动态处理、CGLIB
、Javassist
或者 ASM
库,下文中演示的是JDK
自带的动态代理实现)。
首先,接口UserService
和该接口的实现类UserServiceImpl
与演示静态代理时一致:
public interface UserService {
User getUser(String userId);
void updateUser(User user);
void deleteUser(String userId);
void addUser(User user);
}
public class UserServiceImpl implements UserService {
@Override
public User getUser(String userId) {
System.out.println("---getUser-----");
return null;
}
@Override
public void updateUser(User user) {
System.out.println("---updateUser-----");
}
@Override
public void deleteUser(String userId) {
System.out.println("---deleteUser-----");
}
@Override
public void addUser(User user) {
System.out.println("---addUser-----");
}
}
之后,创建一个代理类(UserServiceImpl
)的调用处理器,起个名字叫:SecurityHandler
,并让它实现InvocationHandler
接口(该接口是JDK1.3
之后自带的)
public class SecurityHandler implements InvocationHandler {
//使用一个通用的对象
private Object targetObject;
public Object createProxyInstance(Object object){
this.targetObject = targetObject;
//根据目标接口生成代理
//第一个参数是代理对象的classloader,第二个是代理对象实现的接口,第三个参数可以理解为回调。
return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),
targetObject.getClass().getInterfaces(),
this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//要增加的额外操作:本例中就是检查安全
checkSecurity();
//调用目标方法
Object ret = method.invoke(targetObject, args);
return ret;
}
/**
* 校验安全性的方法
*/
private void checkSecurity(){
System.out.println("----checkSecurity-----");
}
}
好了,现在就可以调用了,调用时与静态代理类似:
public static void main(String[] args) {
SecurityHandler handler = new SecurityHandler();
UserService userService = (UserService)handler.createProxyInstance(new UserServiceImpl());
userService.deleteUser("user的id");
}
AOP
有了上面对静态代理和动态代理的基本认识,我们再来看AOP这个概念。
AOP是英文Aspect Orient Programming
的缩写,翻译过来就是面向方面(切面)编程。为什么要有这个概念呢?我们不是面向对象编程(OOP)的吗?
AOP和OOP是面向不同领域的两种设计思想,AOP是OOP的延续和补充。
OOP(面向对象编程)针对问题领域中以及业务处理过程中存在的实体及其属性和操作进行抽象和封装,面向对象的核心概念是纵向结构的,其目的是获得更加清晰高效的逻辑单元划分;
AOP则是针对业务处理过程中的切面进行提取,例如,企业开发中经常会面临的种种非功能性需求(操作日志、权限控制、性能监测等等),用面向对象的思路,将业务操作对象的核心功能和对它的其他服务性功能代码分离,即某一个操作在各个模块中都有涉及,这个操作就可以看成“横切”存在于系统当中。在许多情况下,这些操作都是与业务逻辑相关性不强或者不属于逻辑操作的必须部分,而面向对象的方法很难对这种情况做出处理。 作为面向对象的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理、日志、缓存等等。
直白的说,OOP是对业务操作对象的核心功能的封装,而AOP则是业务操作对象的服务性功能的封装。
那AOP又和上面的代理模式有什么关系呢:AOP是一种思想,代理模式是在实现这种思想中用到的主要的设计模式。
Aspect与cglib
首先看Aspect
AspectJ 是 Java 语言的一个 AOP 实现,是 Eclipse 基金组织的开源项目。它主要包括两个部分:
第一个部分定义了如何表达、定义 AOP 编程中的语法规范,通过这套语言规范,我们可以方便地用 AOP 来解决 Java 语言中存在的交叉关注点问题;
比如下面的这些术语:
- 连接点Joinpoint:在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候(在实际应用中,一般是类中的方法)
- 切入点Pointcut:真正需要织入通知的目标方法,即切点来筛选连接点,从而找到真正要处理的方法。
-
通知(也叫增强)Advice:在切入点上要执行的具体动作(代码),如日志记录、权限验证等。通知可分为5种:
- before 目标方法执行前执行,前置通知
- after 目标方法执行后执行,后置通知
- after returning 目标方法返回时执行 ,后置返回通知
- after throwing 目标方法抛出异常时执行 异常通知
- around 在目标函数执行中执行,可控制目标函数是否执行,环绕通知
- 切面Aspect:切点和通知的结合体,就叫切面。比如说一个方法是一条顺序执行的一条线,现在一个面切向该线,在这个面上加上新的代码(通知)插入原方法,那这个面就可以理解为这个切点和新加的代码(通知)组成的。
- 织入Weaving:把切面的代码织入到目标方法的过程。
- 引入Introduction:允许我们向现有的类添加新方法属性。
- 目标Target:目标类,也就是要被通知的对象,也就是真正的业务逻辑对象
- 代理Proxy:就是上面代理模式中提到的代理对象(被增强后的对象)
另一个部分是工具部分,包括编译器、调试工具等。
Aspect属于静态代理,即它是在编译期将通知织入到源代码中,举个例子。
public class Hello {
// 定义一个简单方法,模拟应用中的业务逻辑方法
public void sayHello() {
System.out.println("-----sayHello()-----");
}
// 主方法,程序的入口
public static void main(String[] args) {
Hello h = new Hello();
h.sayHello();
}
}
public aspect TxAspect{
// 指定执行 Hello.sayHello() 方法时执行下面代码块
void around():call(void Hello.sayHello()){
System.out.println("开始事务 ...");
proceed();
System.out.println("事务结束 ...");}
}
上面类文件中不是使用 class、interface、enum
在定义 Java 类,而是使用了 aspect
。这个关键字Java自带的编译器是不识别的,只有Aspect自己的编译器才识别。经过编译后,我们的源代码会变成像下面这样(todo:仅做示意,并未实际测试):
public class Hello {
public void sayHello() {
try {
System.out.println("Hello AspectJ!");
} catch (Throwable localThrowable) {
}
private static final void sayHello_aroundBody1$advice(Hello target, TxAspect ajc$aspectInstance, AroundClosure ajc$aroundClosure) {
System.out.println("开始事务 ...");
AroundClosure localAroundClosure = ajc$aroundClosure;
sayHello_aroundBody0(target);
System.out.println("事务结束 ...");
}
}
这一过程可以用下图来理解:
Aspect编译时织入示意图
再来看cglib
CGLIB(Code Generation Library
)它是一个代码生成类库,属于动态代理的范畴。它的出现主要是弥补JDK自带的动态代理的不足:某个类必须有实现的接口,而生成的代理类也只能代理某个类接口定义的方法。如果一个类没有实现接口怎么办呢?这就有CGLIB的诞生了:JDK的代理类的实现方式是实现相关的接口成为接口的实现类,而CGLIB用继承的方式实现相关的代理类。
Spring AOP
终于到了主角Spring AOP。
Spring是集大成者,所以,它的AOP自然要博采众长:Spring AOP封装了JDK和CGLIB的动态代理实现,同时引入了AspectJ的编程方式和注解。SpringAOP规避了AspectJ依赖于特殊编译器(ajc编译器)的问题,在底层使用JDK和CGLIB的动态代理实现,但又使用了与AspectJ一样的注解。
实际开发
为了启用 Spring 对 @AspectJ 方面配置的支持,并保证 Spring 容器中的目标 Bean 被一个或多个方面自动增强,必须在 Spring 配置文件中添加如下配置:
<aop:aspectj-autoproxy/>
要想Spring AOP 通过CGLIB生成代理,只需要在Spring 的配置文件引入:
<aop:aspectj-autoproxy proxy-target-class="true"/>
这两点可以看文档说明:
aspectj-autoproxy的注释:
Enables the use of the @AspectJ style of Spring AOP.
proxy-target-class的注释:
Are class-based (CGLIB) proxies to be created?
By default, standard Java interface-based proxies are created.
开发步骤(基于注解的方式):
①在Spring配置文件中引入上述两个配置:(如果是有接口的代理对象,可以不引入proxy-target-class="true"
)
②让Spring扫描@Aspect注解所在的包
③写标注了@Aspect注解的类:主要是切入点指示符的配置,5种通知(或增强)方法。
示例1:为各个接口加入操作日志
//自定义注解@OperationLog
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog {
String title() default "";
}
------------------------------------------------------------------------------
//处理类,注意的是:上面提到的配置要写在spring-mvc的那个配置文件中
@Aspect
@Component
public class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
@Autowired
private LogService logService;
@Autowired
private UserService userService;
/**
* Controller层切点 使用到了我们定义的 SystemControllerLog 作为切点表达式。
* 而且我们可以看出此表达式是基于 annotation 的。
*/
@Pointcut("@annotation(com.maxwell.example.modules.sys.log.OperationLog)")
public void controllerAspect() {
}
/**
* 后置通知 用于记录Controller层记录用户的操作
*
* @param joinPoint 连接点
*/
@After("controllerAspect()")
public void doAfter(JoinPoint joinPoint) {
HttpServletRequest request = (HttpServletRequest) ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
try {
String method = (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()");
String title = getControllerMethodTitle(joinPoint);
//获取方法参数
StringBuilder params = new StringBuilder();
Object[] objects = joinPoint.getArgs();
Object first = null;
if(objects != null && objects.length > 0){
for (int i = 0; i < objects.length; i++){
if(i == 0){
first = objects[0];
}
if(! (objects[i] instanceof HttpServletRequest)){
params.append("第" + (i + 1) + "个参数为:" + JsonMapper.getInstance().toJson(objects[i]));
}
}
}
String userId = TokenUtil.getUserId(request);
String ip = request.getRemoteAddr();
//日志对象
final Log log = new Log();
log.setId(IdGen.uuid());
log.setMethod(method);
log.setTitle(title);
log.setRemoteAddr(ip);
log.setCreateTime(new Date());
log.setCreateUser(userId);
log.setUserAgent(request.getHeader("user-agent"));
log.setRequestUri(request.getRequestURI());
log.setParams(params.toString());
//保存到数据库
logService.add(log);
} catch (Exception e) {
logger.error("记录操作日志失败:" + e.getMessage());
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param joinPoint 连接点
* @return 方法描述
* @throws Exception
*/
private static String getControllerMethodTitle(JoinPoint joinPoint) throws Exception {
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] methods = targetClass.getMethods();
String title = "";
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length) {
title = method.getAnnotation(OperationLog.class).title();
break;
}
}
}
return title;
}
}
示例2:系统中满日志(输出到日志文件中)
@Component
@Aspect
public class SlowLogAspect {
private static Logger logger = LoggerFactory.getLogger("slowLog");
private static final int SLOW_TIME = 500;//单位:毫秒
@Pointcut("within(com.maxwell.example.common.service.CrudService+) || within(com.maxwell.example.modules.*.service..*)")
public void aroundPoint(){}
@Around("aroundPoint()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
Object[] args = joinPoint.getArgs();
Object result = null;
long start = System.currentTimeMillis();
try{
result = joinPoint.proceed(args);
} catch(Exception e){
throw e;
}
//慢日志
long end = System.currentTimeMillis();
if(end - start >= SLOW_TIME){
String method = (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()");
logger.warn(method + "执行时间为:" + (end-start)/(float)1000 + "秒");
}
return result;
}
}
遇到的一些问题
Shiro注解无效的问题:排查开启Shiro注解的配置是否在spring-mvc的配置中,配置在spring的配置文件中是无效的。
AOP无效的问题:是否在正确的位置配置了扫描要拦截的类所在的包。
todo:各种上下文的区别
参考
静态代理和动态代理的再学习
Java静态代理和动态代理
OOP与AOP的区别与联系
使用IntelliJ IEDA开发AspectJ项目——环境搭建
Spring AOP,AspectJ, CGLIB 有点晕
关于 Spring AOP (AspectJ) 你该知晓的一切:*****
Spring AOP 实现原理与 CGLIB 应用
代理模式原理及实例讲解
Java JDK代理、CGLIB、AspectJ代理分析比较
Spring AOP AspectJ Pointcut Expressions With Examples:切入点指示符的配置
自定义注解&Spring AOP实现日志组件(可重用)
《研磨设计模式》第11章:代理模式
2018-03-11更新:对JDK生成代理类的方法的三个参数进行说明
网友评论