创建时间:2018年01月22日
缘起:由于公司对外的接口,需要加解密传输,现有方式复杂,代码重复,因此想使用一个注解搞定,期间试过aop来完成,但是发觉不能完全满足需求,但是在使用过程我忽然发现对aop的认识有点模糊了,虽然经常说经常用,所以决定重新梳理一遍。
通过本篇文章你将知道什么?
1. 什么是aop,发展史,优点,缺点?
2. spring aop的实现原理,及aop怎么生效、何时生的效?
一、what?
1.什么是aop
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,一种编程思想,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是Spring框架中的一个重要内容,它通过对既有程序定义一个切入点,然后在其前后切入不同的执行内容,比如常见的有:打开数据库连接/关闭数据库连接、打开事务/关闭事务、记录日志等。基于AOP不会破坏原来程序逻辑,因此它可以很好的对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
2.基础名词
- 切面(aspect):切入到指定类指定方法的代码片段称为切面。
- 增强/通知(advice):在特定连接点执行的动作。
- 切点(pointcut):切入到哪些类、哪些方法则叫切入点,用于指定某个增强应该在何时被调用。
- 连接点(join point):在应用执行过程中能够插入切面的一个点。
- 织入(weaving):把切面的代码织入到目标函数的过程。
3.通知类型介绍:
其实说的就是切入的代码在连接点的什么地方执行
- 前置通知[Before advice]:在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常。
- 正常返回通知[After returning advice]:在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行。
- 异常通知[After throwing advice]:如果一个方法抛出异常,通知将会被执行。Spring提供了健壮的类型以抛出通知,所以你可以按自己的喜好编写代码捕捉异常(和子类),而不需要通过Throwable或Exception。
- 后置通知[After advice]:在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容。
- 环绕通知[Around advice]:环绕通知围绕在连接点前后,比如一个方法调用的前后。这是最强大的通知类型,能在方法调用前后自定义一些操作。环绕通知还需要负责决定是继续处理join point(调用ProceedingJoinPoint的proceed方法)还是中断执行。
4.aop,spring aop,aspectj区别与联系
参考链接:
- http://blog.csdn.net/pingnanlee/article/details/38845955
- http://blog.csdn.net/qq_21050291/article/details/72523138
5.AOP发展历程
参考链接:
- http://www.baike.com/wiki/%E9%9D%A2%E5%90%91%E5%88%87%E9%9D%A2%E7%BC%96%E7%A8%8B
二、why?
1.为什么要有aop?
前提
(本段摘自知乎上的一段回答)
首先我们知道,java的特性就是封装、多态、继承。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。
但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。
也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程
为什么有aop,其实也是aop的好处
- 1)解耦(没有与业务代码硬编码)
- 2)重复利用代码(在切面内全部都有)
- 3)便于装卸(不想用把aop删掉即可)
那么缺点呢?
- 1)创建对象的流程麻烦了(如何创建spring已经封装好,一般不用自己搞,所以这都不是事)
- 2)由于spring大部分采用反射机制来实现,因此性能一定是个问题(jdk的升级,反射机制的性能不断提高)
2.常用的使用场景
- 日志管理(但事实上很难用AOP编写实用的程序日志)
- 事务管理(spring的事务控制)
- 权限验证
- 性能监测
三、how?
1.aop的实现原理
主要依赖jdk动态代理和cglib动态代理,如果被代理类有接口,则默认使用jdk,若无接口则使用cglib,来动态生产字节码,
spring强制使用cglib代理
<aop:aspectj-autoproxy proxy-target-class="true"/>
2.aop怎么起效的?
在spring容器初始化bean的时候,如果发现bean有注解(==此处注解应该是运行时注解==),就会创建代理类,如果判断无需代理,则生成原生对象.
四、常见的使用问题
1.场景1:实现Web层的日志切面
package com.bob.stu.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* http请求日志切面
*
* @author bob <bobyang_coder@163.com>
* @since 2017/5/14
*/
@Aspect() //切面注解
@Component
public class HttpRequestAspect {
private static final Logger logger = LoggerFactory.getLogger(HttpRequestAspect.class);
ThreadLocal<Long> startTime = new ThreadLocal<>();
/**
> * 定义切入点
> */
@Pointcut("execution(public * com.bob.stu.controller.*.*(..)))")
public void logging() {
}
/**
* 在方法之前打印日志
* 日志内容:1.访问的url;2.访问的method;3.访问者ip;4.请求参数
*/
@Before("logging()")
public void doBefore(JoinPoint joinPoint) {
logger.info("----------loggingBefore----------------");
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
logger.info("url={}", request.getRequestURL());
logger.info("http_method={}", request.getMethod());
logger.info("visitor_ip={}", request.getRemoteAddr());
logger.info("class_method={}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
logger.info("method_args={}", joinPoint.getArgs());
startTime.set(System.currentTimeMillis());//设置开始时间
}
/**
* 在方法之后打印日志
*/
@After("logging()")
public void doAfter() {
logger.info("----------loggingAfter----------------");
}
@AfterReturning(returning = "object", pointcut = "logging()")
public void doAfterReturning(Object object) {
logger.info("response={}", object.toString());
logger.info("spend time : {} ", System.currentTimeMillis() - startTime.get());//计算程序执行时间
}
}
可以看上面的例子,通过@Pointcut
定义的切入点为com.bob.stu.controller
包下的所有函数(对web层所有请求处理做切入点),然后通过@Before
实现,对请求内容的日志记录(本文只是说明过程,可以根据需要调整内容),最后通过@AfterReturning
记录请求返回的对象。
通过运行程序并访问:http://localhost:8080/girl/1
,可以获得下面的日志输出
2017-05-14 22:39:25.451 INFO 16734 --- [nio-8080-exec-3] com.bob.stu.aspect.HttpRequestAspect : ----------loggingBefore----------------
2017-05-14 22:39:25.452 INFO 16734 --- [nio-8080-exec-3] com.bob.stu.aspect.HttpRequestAspect : url=http://127.0.0.1:8080/girl2/1
2017-05-14 22:39:25.453 INFO 16734 --- [nio-8080-exec-3] com.bob.stu.aspect.HttpRequestAspect : http_method=GET
2017-05-14 22:39:25.453 INFO 16734 --- [nio-8080-exec-3] com.bob.stu.aspect.HttpRequestAspect : visitor_ip=127.0.0.1
2017-05-14 22:39:25.453 INFO 16734 --- [nio-8080-exec-3] com.bob.stu.aspect.HttpRequestAspect : class_method=com.bob.stu.controller.Girl2Controller.get
2017-05-14 22:39:25.453 INFO 16734 --- [nio-8080-exec-3] com.bob.stu.aspect.HttpRequestAspect : method_args=1
2017-05-14 22:39:25.453 INFO 16734 --- [nio-8080-exec-3] com.bob.stu.controller.Girl2Controller : ==========method:get========
Hibernate: select girl0_.id as id1_0_0_, girl0_.age as age2_0_0_, girl0_.name as name3_0_0_ from girl girl0_ where girl0_.id=?
2017-05-14 22:39:25.467 INFO 16734 --- [nio-8080-exec-3] com.bob.stu.aspect.HttpRequestAspect : ----------loggingAfter----------------
2017-05-14 22:39:25.468 INFO 16734 --- [nio-8080-exec-3] com.bob.stu.aspect.HttpRequestAspect : response=Girl{id=1, name='bob', age=12}
2017-05-14 22:39:25.468 INFO 16734 --- [nio-8080-exec-3] com.bob.stu.aspect.HttpRequestAspect : spend time : 15
优化:AOP切面中的同步问题
在HttpRequestAspect切面中,分别通过doBefore和doAfterReturning两个独立函数实现了切点头部和切点返回后执行的内容,若我们想统计请求的处理时间,就需要在doBefore处记录时间,并在doAfterReturning处通过当前时间与开始处记录的时间计算得到请求处理的消耗时间。
那么我们是否可以在HttpRequestAspect切面中定义一个成员变量来给doBefore和doAfterReturning一起访问呢?是否会有同步问题呢?的确,直接在这里定义基本类型会有同步问题,所以我们可以引入ThreadLocal对象,像下面这样进行记录:
ThreadLocal<Long> startTime = new ThreadLocal<>();
优化:AOP切面的优先级
由于通过AOP实现,程序得到了很好的解耦,但是也会带来一些问题,比如:我们可能会对Web层做多个切面,校验用户,校验头信息等等,这个时候经常会碰到切面的处理顺序问题。
所以,我们需要定义每个切面的优先级,我们需要@Order(i)
注解来标识切面的优先级。i的值越小,优先级越高。假设我们还有一个切面是CheckNameAspect
用来校验name必须为bob,我们为其设置@Order(10)
,而上文中WebLogAspect设置为@Order(5)
,所以HttpRequestAspect有更高的优先级,这个时候执行顺序是这样的:
- 在
@Before
中优先执行@Order(5)
的内容,再执行@Order(10)
的内容 - 在
@After
和@AfterReturning
中优先执行@Order(10)
的内容,再执行@Order(5)
的内容
所以我们可以这样子总结:
- 在切入点前的操作,按order的值由小到大执行
- 在切入点后的操作,按order的值由大到小执行
五、扩展参考链接
六、待研究问题
- 动态代理生成的class文件存在哪?
- 初始化bean怎么判断哪些bean需要被代理?