众所周知,spring最核心的两个功能是aop和ioc,即面向切面,控制反转。这里我们探讨一下如何使用spring aop。
1.什么是aop
aop全称Aspect Oriented Programming,面向切面,AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。其与设计模式完成的任务差不多,是提供另一种角度来思考程序的结构,来弥补面向对象编程的不足。
通俗点讲就是提供一个为一个业务实现提供切面注入的机制,通过这种方式,在业务运行中将定义好的切面通过切入点绑定到业务中,以实现将一些特殊的逻辑绑定到此业务中。
比如,若是需要一个记录日志的功能,首先想到的是在方法中通过log4j或其他框架来进行记录日志,但写下来发现一个问题,在整个业务中其实核心的业务代码并没有多少,都是一些记录日志或其他辅助性的一些代码。而且很多业务有需要相同的功能,比如都需要记录日志,这时候又需要将这些记录日志的功能复制一遍,即使是封装成框架,也是需要调用之类的。在此处使用复杂的设计模式又得不偿失。
所以就需要面向切面出场了。
先介绍一些aop的名词,其实这些名词对使用aop没什么影响,但为了更好的理解最好知道一些
切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用基于模式或者基于@Aspect注解的方式来实现。
连接点(Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是表示一个方法的执行。
通知(Advice):在切面的某个特定的连接点上执行的动作。其中包括了“around”、“before”和“after”等不同类型的通知(通知的类型将在后面部分进行讨论)。许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。
切入点(Pointcut):匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。
引入(Introduction):用来给一个类型声明额外的方法或属性(也被称为连接类型声明(inter-type declaration))。Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用引入来使一个bean实现IsModified接口,以便简化缓存机制。
目标对象(Target Object):被一个或者多个切面所通知的对象。也被称做被通知(advised)对象。既然Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。
织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。
2.整合 aop
首先需要引入aop 的 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
接下来,我们进入正题。这里的涉及的通知类型有:前置通知、后置通知、异常通知、环绕通知,下面我们就具体的来看一下怎么在SpringBoot中添加这些通知。
首先我们先创建一个Aspect切面类:
@Component
@Aspect
public class WebControllerAop { }
指定切点:
//匹配com.wen.boot.controller包及其子包下的所有类的所有方法
@Pointcut("execution(* com.wen.boot.controller..*.*(..))")
public void executeService(){
}
接着我们再创建一个Controller请求处理类:
@RestController
@RequestMapping("/aop")
public class AopController{
}
1)前置通知
/**
* 前置通知,方法调用前被调用
* @param joinPoint
*/
@Before("executeService()")
public void doBeforeAdvice(JoinPoint joinPoint){
System.out.println("我是前置通知!!!");
//获取目标方法的参数信息
Object[] obj = joinPoint.getArgs();
//AOP代理类的信息
joinPoint.getThis();
//代理的目标对象
joinPoint.getTarget();
//用的最多 通知的签名
Signature signature = joinPoint.getSignature();
//代理的是哪一个方法
System.out.println(signature.getName());
//AOP代理类的名字
System.out.println(signature.getDeclaringTypeName());
//AOP代理类的类(class)信息
signature.getDeclaringType();
//获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
//如果要获取Session信息的话,可以这样写:
//HttpSession session = (HttpSession) requestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION);
Enumeration<String> enumeration = request.getParameterNames();
Map<String,String> parameterMap = Maps.newHashMap();
while (enumeration.hasMoreElements()){
String parameter = enumeration.nextElement();
parameterMap.put(parameter,request.getParameter(parameter));
}
String str = JSON.toJSONString(parameterMap);
if(obj.length > 0) {
System.out.println("请求的参数信息为:"+str);
}
}
接下来我们在Controller类里添加一个请求处理方法来测试一下前置通知:
@RequestMapping("/before/{id}/{value}")
public String before(@PathVariable String key,@PathVariable String value){
return "key="+key+" value="+value;
}
进行单元测试
@Test
public void beforeTest() {
try{
String result=mockMvc.perform(MockMvcRequestBuilders.get("/aop/before/key/value")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$").value(1))
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}catch( Exception e) {
e.printStackTrace();
}
}
得到如下的返回结果
2)后置通知 如下
/**
* 后置返回通知
* 这里需要注意的是:
* 如果参数中的第一个参数为JoinPoint,则第二个参数为返回值的信息
* 如果参数中的第一个参数不为JoinPoint,则第一个参数为returning中对应的参数
* returning 限定了只有目标方法返回值与通知方法相应参数类型时才能执行后置返回通知,否则不执行,对于returning对应的通知方法参数为Object类型将匹配任何目标返回值
* @param joinPoint
* @param keys
*/
@AfterReturning(value = "execution(* com.wen.boot.controller..*.*(..))",returning = "keys")
public void doAfterReturningAdvice1(JoinPoint joinPoint,Object keys){
System.out.println("第一个后置返回通知的返回值:"+keys);
}
@AfterReturning(value = "execution(* com.wen.boot.controller..*.*(..))",returning = "keys",argNames = "keys")
public void doAfterReturningAdvice2(String keys){
System.out.println("第二个后置返回通知的返回值:"+keys);
}
/**
* 后置通知(目标方法只要执行完了就会执行后置通知方法)
* @param joinPoint
*/
@After("executeService()")
public void doAfterAdvice(JoinPoint joinPoint){
System.out.println("后置通知执行了!!!!");
}
单元测试
@Test
public void beforeTest() {
try{
String result=mockMvc.perform(MockMvcRequestBuilders.get("/aop/before/key/value")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$").value(1))
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}catch( Exception e) {
e.printStackTrace();
}
}
结果如下
如图可以看到两个返回通知的返回值那么他们有什么不一样呢我们做一个测试将controller 中返回值类型改为void
@RequestMapping("/before/{key}/{value}")
public void before(@PathVariable String key,@PathVariable String value){
//return "key="+key+" value="+value;
}
在测试
发现第二个返回通知没有了
3)异常通知
@AfterThrowing(value = "executeService()",throwing = "exception")
public void ThrowingAdvice(JoinPoint joinPoint,Throwable exception){
//目标方法名:
System.out.println(joinPoint.getSignature().getName());
if(exception instanceof NullPointerException){
System.out.println("发生了空指针异常!!!!!");
}
}
这里定义一个空指针异常的捕获
controller
@RequestMapping("/throwing/{key}")
public String throwing(@PathVariable String key){
throw new NullPointerException();
}
单元测试
@Test
public void throwTest() {
try{
String result=mockMvc.perform(MockMvcRequestBuilders.get("/aop/throwing/key")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$").value(1))
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}catch( Exception e) {
e.printStackTrace();
}
}
结果
4) 环绕通知
代码
@Around("execution(* com.wen.boot.controller..*.around(..))")
public Object doAroundAdvice(ProceedingJoinPoint pjd){
System.out.println("环绕开始");
Object result = null;
String methodName = pjd.getSignature().getName();
//执行目标方法
try {
//前置通知
System.out.println("The method " + methodName + " begins with " + Arrays.asList(pjd.getArgs()));
result = pjd.proceed();
//返回通知
System.out.println("The method " + methodName + " ends with " + Arrays.asList(pjd.getArgs()));
} catch (Throwable e) {
//异常通知
System.out.println("The method " + methodName + " occurs expection : " + e);
throw new RuntimeException(e);
}
//后置通知
System.out.println("The method " + methodName + " ends");
System.out.println("环绕结束");
return result;
}
controller
@RequestMapping("/around/{key}")
public String around(@PathVariable String key){
return "环绕通知:"+key;
}
单元测试
@Test
public void aroundTest() {
try{
String result=mockMvc.perform(MockMvcRequestBuilders.get("/aop/around/key")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$").value(1))
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}catch( Exception e) {
e.printStackTrace();
}
}
结果:
文章地址:http://www.haha174.top/article/details/254142
推荐一篇文章:http://mp.weixin.qq.com/s/SLaIcVp2EOwz5996oJZrHw
源码地址:https://github.com/haha174/boot.git