Spring AOP的使用
不用AOP会有什么问题
常规:OOP(Object Oriented Programming,面向对象编程)中,是按业务流程进行程序的设计,这样,不同的业务之间是相互独立的。
需求:在业务的每个方法执行时,需要将日志打印输出到指定地方、要进行权限认证、有事务的要求。
方案:在每个方法的首尾都加上相关代码,这样导致每个方法中除了要实现正常的业务逻辑外,还得加上日志、权限、事务的代码,代码非常冗余。这不是一个好的实现。
缺点:
- 工作量特别大,如果项目中有多个类,多个方法,则要修改多次。
- 违背了设计原则:开闭原则(OCP),对扩展开放,对修改关闭,而为了增加功能把每个方法都修改了,也不便于维护。
- 违背了设计原则:单一职责(SRP),每个方法除了要完成自己本身的功能,还要计算耗时、延时;每一个方法引起它变化的原因就有多种。
“一个方法只做一件事情”,方法除了包含业务逻辑代码外还需要加例如日志、事务等相关操作的代码或代码引用。这样我们一个方法就不是做一件事情,而是做了业务逻辑、日志、事务三件事情。因为日志、事务等属于通用型的功能,所以可以定义成一个切面,这样可以在代码需要日志和事务的时候切入程序。来达到一个方法只做一件事情的目的。 - 违背了设计原则:依赖倒转(DIP),抽象不应该依赖细节,两者都应该依赖抽象。而在Test类中,Test与Math都是细节。
因此就需要用到Spring中的AOP!
AOP是什么
AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP的补充和完善。
OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合,是一种纵向的关系。
AOP则是对纵向关系进行了横切,也就是将纵向关系中的对象、方法,通过切面的形式切了一刀,这样在执行业务方法的时候,就会通过这个切面,完成日志、权限、事务等通用功能。
AOP的底层实现为代理模式。
AOP核心概念
- 横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点
- 切面(aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象
- 连接点(joinpoint):被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
- 切入点(pointcut):对连接点进行拦截的定义
- 通知(advice):所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置(before)、后置(after)、异常(afterThrowing)、最终(afterReturn)、环绕(around)通知五类
- 目标对象:代理的目标对象
- 织入(weave):将切面应用到目标对象并导致代理对象创建的过程
- 引入(introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
Spring AOP的最原始实现为引介增强,即DelegatingIntroductionInterceptor,这个将在后续文章里写。
AOP的实现
如果要使用aop功能,不论哪种方法,都需要在XML中添加以下内容来支持AOP
<!-- 支持aop,proxy-target-class="true"指默认用Cglib,否则用JDK -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
用到的POM,Spring的AOP包,一般在引用Spring-webmvc后,就都包含了
<!-- AspectJ的相关包 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
1. 通过XML和Java代码
- 要被切的方法(其实也就是普通的方法,我以controller为例)
@RestController
@RequestMapping(value = "/my/second")
public class MySecondController {
private static final Logger LOGGER = LogManager.getLogger(MySecondController.class);
@RequestMapping(value = "/{pathValue}", method = RequestMethod.GET)
public String getFirst(@PathVariable String pathValue, HttpServletRequest request, HttpServletResponse response) throws Exception {
LOGGER.info("开始getFirst方法,path={}", pathValue);
if (pathValue.startsWith("s")) {
throw new Exception();
}
return "Hello world";
}
}
- 新加一个切面类,这个类中的方法为在执行普通方法的时候,需要做的事,也就是日志、权限、事务等
public class LoggerAspectByXml {
private static final Logger LOGGER = LogManager.getLogger(LoggerAspectByXml.class);
/**
* @Title: myBeforeMethod
* @Description: 前置,业务方法执行前
* @param: joinPoint
*/
public void myBeforeMethod(JoinPoint joinPoint) throws Exception {
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
LOGGER.info("myBeforeMethod 访问的类名:{},方法名:{},参数为:{}", simpleName, methodName, args);
LOGGER.info("前置完成,准备访问业务方法:{}", methodName);
}
/**
* @Title: myAfterMethod
* @Description: 后置,无论业务方法是否发生异常都会执行, 所以访问不到方法的返回值
* @param: joinPoint
*/
public void myAfterMethod(JoinPoint joinPoint) throws Exception {
LOGGER.info("业务方法完成,获取不到返回值");
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
LOGGER.info("myAfterMethod 访问的类名:{},方法名:{},参数为:{}", simpleName, methodName, args);
}
/**
* @Title: myAfterReturnMethod
* @Description: 只有当业务方法正常返回时,才执行
* @param: joinPoint
* @param: result,这个参数的名字必须与XML中的一致
*/
public void myAfterReturnMethod(JoinPoint joinPoint, Object result) throws Exception {
LOGGER.info("业务方法完成,返回值是:{}", result);
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
LOGGER.info("myAfterReturnMethod 访问的类名:{},方法名:{},参数为:{}", simpleName, methodName, args);
}
/**
* @Title: myAfterThrowingMethod
* @Description: 只有当业务方法抛出异常时,才执行
* @param: joinPoint
* @param: ex,这个参数的名字必须与XML中的一致
*/
public void myAfterThrowingMethod(JoinPoint joinPoint, Exception ex) throws Exception {
LOGGER.info("异常访问完成,异常:{}", ex);
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
LOGGER.info("myAfterThrowingMethod 访问的类名:{},方法名:{},参数为:{}", simpleName, methodName, args);
}
/**
* @Title: myAroundMethod
* @Description: 切点为around方法,ProceedingJoinPoint只能用于aroundMethod
* @param: point
* @return: java.lang.Object
*/
public Object myAroundMethod(ProceedingJoinPoint point) {
String simpleName = point.getTarget().getClass().getSimpleName();
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
LOGGER.info("myAroundMethod 访问的类名:{},方法名:{},参数为:{}", simpleName, methodName, args);
Object result;
try {
LOGGER.info("开始访问的业务方法名:{}", methodName);
result = point.proceed();
LOGGER.info("结束访问,返回值:{}", result);
} catch (Throwable throwable) {
throwable.printStackTrace();
LOGGER.error("出错:", throwable);
throw new RuntimeException(throwable);
}
LOGGER.info("全部完成:{}.{}", simpleName, methodName);
return result;
}
}
- XML中的配置
在Spring的xml中添加如下内容:
<!-- 支持aop -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!-- 实现AOP的方式:编写类和XML中的配置 -->
<bean id="logAspect" class="com.my.springdemo.filter.LoggerAspectByXml"/>
<aop:config>
<aop:pointcut id="pointcut" expression="execution(* com.my.springdemo.controller.*Controller.*(..))"/>
<aop:aspect order="1" ref="logAspect">
<!-- 前置标签 -->
<aop:before method="myBeforeMethod" pointcut-ref="pointcut"/>
<!-- 后置标签 -->
<aop:after method="myAfterMethod" pointcut-ref="pointcut"/>
<!-- 最终标签,returning属性只有after-returning这个标签有,且值必须与方法中的形参名一致 -->
<aop:after-returning method="myAfterReturnMethod" pointcut-ref="pointcut" returning="result"/>
<!-- 异常标签,throwing属性只有after-throwing这个标签有,且值必须与方法中的形参名一致 -->
<aop:after-throwing method="myAfterThrowingMethod" pointcut-ref="pointcut" throwing="ex"/>
<!-- 环绕标签 -->
<aop:around method="myAroundMethod" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
从功能上看,用aroundMethod这个就可以实现其他四个的功能。
2. 通过Java代码
@Aspect //该标签把LoggerAspectByAnnotation类声明为一个切面
@Order(1) //设置切面的优先级:如果有多个切面,可通过设置优先级控制切面的执行顺序(数值越小,优先级越高)
@Component //该标签把LoggerAspectByAnnotation类放到IOC容器中
public class LoggerAspectByAnnotation {
private static final Logger LOGGER = LogManager.getLogger(LoggerAspectByAnnotation.class);
@Pointcut("execution(* com.my.springdemo.controller.*Controller.*(..))")
public void declearJoinPointExpression() {
}
/**
* @Title: myBeforeMethod
* @Description: 前置,业务方法执行前
* @param: joinPoint
*/
// @Before("declearJoinPointExpression()")
public void myBeforeMethod(JoinPoint joinPoint) throws Exception {
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
LOGGER.info("myBeforeMethod 访问的类名:{},方法名:{},参数为:{}", simpleName, methodName, args);
LOGGER.info("前置完成,准备访问业务方法:{}", methodName);
}
/**
* @Title: myAfterMethod
* @Description: 后置,无论业务方法是否发生异常都会执行, 所以访问不到方法的返回值
* @param: joinPoint
*/
// @After("declearJoinPointExpression()")
public void myAfterMethod(JoinPoint joinPoint) throws Exception {
LOGGER.info("业务方法完成,获取不到返回值");
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
LOGGER.info("myAfterMethod 访问的类名:{},方法名:{},参数为:{}", simpleName, methodName, args);
}
/**
* @Title: myAfterReturnMethod
* @Description: 只有当业务方法正常返回时,才执行
* @param: joinPoint
* @param: result
*/
// @AfterReturning(value = "declearJoinPointExpression()", returning = "result")
public void myAfterReturnMethod(JoinPoint joinPoint, Object result) throws Exception {
LOGGER.info("业务方法完成,返回值是:{}", result);
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
LOGGER.info("myAfterReturnMethod 访问的类名:{},方法名:{},参数为:{}", simpleName, methodName, args);
}
/**
* @Title: myAfterThrowingMethod
* @Description: 只有当业务方法抛出异常时,才执行
* @param: joinPoint
* @param: ex
*/
// @AfterThrowing(value = "declearJoinPointExpression()", throwing = "ex")
public void myAfterThrowingMethod(JoinPoint joinPoint, Exception ex) throws Exception {
LOGGER.info("异常访问完成,异常:{}", ex);
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
LOGGER.info("myAfterThrowingMethod 访问的类名:{},方法名:{},参数为:{}", simpleName, methodName, args);
}
/**
* @Title: myAroundMethod
* @Description: 切点为around方法,ProceedingJoinPoint只能用于aroundMethod
* @param: point
* @return: java.lang.Object
*/
@Around("declearJoinPointExpression()")
public Object myAroundMethod(ProceedingJoinPoint point) {
String simpleName = point.getTarget().getClass().getSimpleName();
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
LOGGER.info("myAroundMethod 访问的类名:{},方法名:{},参数为:{}", simpleName, methodName, args);
//获取请求的URI,可以根据这个URI来进一步确定要不要切,面试可能会问哦!
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String uri = request.getRequestURI();
LOGGER.info("请求的URI为:{}", uri);
Object result;
try {
LOGGER.info("开始访问的方法名:{}", methodName);
result = point.proceed();
LOGGER.info("结束访问,返回值:{}", result);
} catch (Throwable throwable) {
throwable.printStackTrace();
LOGGER.error("出错:", throwable);
throw new RuntimeException(throwable);
}
LOGGER.info("完成:{}.{}", simpleName, methodName);
return result;
}
}