什么是AOP?
AOP (Aspect Orient Programming)是面向切面变成,用通俗的话说,就是面向多个对象的编程。把需要的公共方法提取出来,组成一个切面,在指定的切点上进行方法拦截。spring会利用动态代理的方式生成代理东西,执行加入公共方法和目标方法。
相关概念
Aspect(切面):是通知和切入点的结合,通知和切入点共同定义了关于切面的全部内容—它的功能、在何时和何地完成其功能
joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点.
Pointcut(切入点):所谓切入点是指我们要对哪些joinpoint进行拦截的定义.
通知定义了切面的”什么”和”何时”,切入点就定义了”何地”.
Advice(通知):所谓通知是指拦截到joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)
Target(目标对象):代理的目标对象
Weaving(织入):是指把切面应用到目标对象来创建新的代理对象的过程.切面在指定的连接点织入到目标对象
定义service
package com.demo.test.aop;
import com.demo.test.entity.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestAopController {
private static Logger logger = LoggerFactory.getLogger(TestAopController.class);
@RequestMapping("/testAop")
public String getuser(@RequestParam String id) {
logger.info("【执行实际方法】获取id: {}",id );
int age = Integer.valueOf(id); // 可能出现异常的地方,出发异常通知
rturn "age=>"+age;
}
}
定义切面
package com.demo.test.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
@Aspect
@Component
public class LogAspects {
private static Logger log = LoggerFactory.getLogger(LogAspects.class);
/**
* 定义切入点,切入点为com.demo.test.aop下的所有函数
*/
@Pointcut("execution(* com.demo.test.aop..*.*(..))")
private void webLog(){
}
/**
* 前置通知
* 在目标方法执行之前执行执行的通知。
*
* 前置通知方法,可以没有参数,也可以额外接收一个JoinPoint,Spring会自动将该对象传入,代表当前的连接点,通过该对象可以获取目标对象 和 目标方法相关的信息。
*
* 注意,如果接收JoinPoint,必须保证其为方法的第一个参数,否则报错。
* @param joinPoint
*/
@Before(value = "webLog()")
public void methodBefore(JoinPoint joinPoint) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
//打印请求内容
log.info("========================【前置通知】请求内容开始======================");
log.info("请求地址:" + request.getRequestURI().toString());
log.info("请求方式" + request.getMethod());
log.info("请求类方法" + joinPoint.getSignature());
log.info("请求类方法参数" + Arrays.toString(joinPoint.getArgs()));
log.info("========================【前置通知】请求内容结束======================");
}
/**
* 后置通知
* 在目标方法执行之后执行的通知。
* 在后置通知中也可以选择性的接收一个JoinPoint来获取连接点的额外信息,但是这个参数必须处在参数列表的第一个。
* 后置通知,方法执行报错,执行不下去了,就不会执行了
* @param o
*/
@AfterReturning(returning = "o", pointcut = "webLog()")
//这个注解的作用是:在切入点,return后执行,如果想对某些方法的返回参数进行处理,可以在这操作
public void methodAfterReturing(Object o) {
log.info("--------------【AfterReturning - 后置通知】返回内容----------------");
log.info("Response内容:" + o);
log.info("--------------【AfterReturning - 后置通知】返回内容----------------");
}
/**
* 是在目标方法执行之后执行的通知,实际是代理方法执行完毕后执行。
* 最终通知,无论被代理的方法是否被执行,都会执行这个方法,这个方法就是在新生成的代理类执行完后执行
* 和后置通知不同之处在于,后置通知是在方法正常返回后执行的通知,如果方法没有正常返-例如抛出异常,则后置通知不会执行。
* 而最终通知无论如何都会在目标方法调用过后执行,即使目标方法没有正常的执行完成。
*
* 另外,后置通知可以通过配置得到返回值,而最终通知无法得到。
*
* 最终通知也可以额外接收一个JoinPoint参数,来获取目标对象和目标方法相关信息,但一定要保证必须是第一个参数。
* @param joinpoint
*/
@After(value = "webLog()")
public void logEnd(JoinPoint joinpoint) {
log.info("--------------【After - 最终通知】----------------");
}
/**
* 在目标方法抛出异常时执行的通知
*
* 可以配置传入JoinPoint获取目标对象和目标方法相关信息,但必须处在参数列表第一位
*
* 另外,还可以配置参数,让异常通知可以接收到目标方法抛出的异常对象。
* @param object
*/
@AfterThrowing(value = "webLog()",throwing = "object")
public void logEnd(Exception object) {
log.info("--------------【异常通知】---------------==》" + object);
}
/**
* 2.环绕通知
* 在目标方法执行之前和之后都可以执行额外代码的通知。
*
* 在环绕通知中必须显式的调用目标方法,目标方法才会执行,这个显式调用时通过ProceedingJoinPoint来实现的,可以在环绕通知中接收一个此类型的形参,spring容器会自动将该对象传入,注意这个参数必须处在环绕通知的第一个形参位置。
*
* **要注意,只有环绕通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint。
*
* 环绕通知需要返回返回值,否则真正调用者将拿不到返回值,只能得到一个null。
*
* 环绕通知有控制目标方法是否执行、有控制是否返回值、有改变返回值的能力。
*
* 环绕通知虽然有这样的能力,但一定要慎用,不是技术上不可行,而是要小心不要破坏了软件分层的“高内聚 低耦合”的目标。
* @param pjp
* @return
*/
@Around("webLog()")
public Object arroundFunc(ProceedingJoinPoint pjp) {
try {
Object result = null;
Object[] args = pjp.getArgs();
beforeFunc();//前置通知
result = pjp.proceed(args);
afterReturnFunc();//后置通知
return result;
} catch (Throwable t) {
afterThrowFunc();//异常通知
throw new RuntimeException(t);
} finally {
afterFunc();//最终通知
}
}
void beforeFunc(){
log.info("--------------【环绕通知-前置通知】---------------==》");
}
void afterReturnFunc(){
log.info("--------------【环绕通知-后置通知】---------------==》");
}
public void afterThrowFunc() {
log.info("--------------【环绕通知-异常通知】---------------==》");
}
public void afterFunc() {
log.info("--------------【环绕通知-最终通知】---------------==》");
}
}
模拟正常访问
浏览器中访问: http://localhost:8082/testAop?id=11
此时没有异常,正常结果
执行结果如下:
模拟异常访问
http://localhost:8082/testAop?id=ooo
参数id=ooo,不能转为整形,报异常
执行结果如下
执行顺序
Spring版本不一样,通知执行顺序可能也会存在差异
下面以Spring4.0版本、Spring5.28版本进行测试
Spring4.0
正常情况:环绕前置=====@Before目标方法执行=环绕返回=环绕最终===@After=====@AfterReturning
异常情况:环绕前置=====@Before目标方法执行=环绕异常=环绕最终===@After=====@AfterThrowing
Spring5.28
正常情况:环绕前置=====@Before=目标方法执行=@AfterReturning=====@After=环绕返回=环绕最终
异常情况:环绕前置=====@Before=目标方法执行=@AfterThrowing=====@After=环绕异常=环绕最终
这里没有继承接口,aop是如何利用动态代理的?
1.默认使用 JDK 动态代理,这样便可以代理所有的接口类型(interface)
2.Spring AOP也支持CGLIB的代理方式。如果我们被代理对象没有实现任何接口或者实现的接口都是空接口,则是CGLIB
3.我们可以强制使用CGLIB,指定proxy-target-class = “true” 或者 基于注解@EnableAspectJAutoProxy(proxyTargetClass = true)
切入点表达式
定义切入点的时候需要一个包含名字和任意参数的签名,还有一个切入点表达式,如execution(public * com.example.aop…(…))
切入点表达式的格式:execution([可见性]返回类型[声明类型].方法名(参数)[异常])
其中[]内的是可选的,其它的还支持通配符的使用:
- *:匹配所有字符
- …:一般用于匹配多个包,多个参数
- +:表示类及其子类
4)运算符有:&&,||,!
切入点表达式关键词用例:
1)execution:用于匹配子表达式。
//匹配com.cjm.model包及其子包中所有类中的所有方法,返回类型任意,方法参数任意
@Pointcut(“execution(* com.cjm.model…(…))”)
public void before(){}
execution(* com.test.aop…*.getuser(…)) 表示指定方法getuser