毕业面试那会儿,被问最多的问题便是:请你解释一下什么是AOP
思想。当时最喜欢的回答方式是先将英文全称给呈现出来“威慑”下面试官,即——Aspect Oriented Programming
。然后把网上搜集的各种解释,使用场景理直气壮地背一遍。这样的回答能唬住一些对应届生要求不高的面试官,但真遇上爱刨根问底的大佬就该GG
了。最近在项目中用到了AOP
,想把几个思考点总结一下。
在讲AOP
之前,首先先回顾一下POP
——Process Oriented Programming
以及OOP
——Object Oriented Programming
。
-
POP与OOP
- 对于一个问题,给出解决问题的所需的步骤,POP是一种以功能实现为导向的编程思想,换句话说:功能性的目标实现了就行。然而OOP注重封装,强调实现过程中的模块化,对象化,将对象内部的属性和外部分开。用大家租房时可能遇到的户型来说,POP偏向于"开放式房型",布局中有床,灶台,浴缸,沙发等各类功能的事物。作为一个整体提供人们住房的实现。而OOP偏向于传统的户型,卫生间,厨房,卧室,客厅等具体事物之间有门隔开,作为整体起到“房子”该有的功能和效果,但彼此之间又相对独立,具有较低的耦合性。这样的房子也能正常居住。
- POP的设计,节约了空间成本(无需“门”的设计),这种设计在早期计算机配置低,内存小的情况下,是一种以时间换空间的良好设计。但这种设计的房子使得各个事物时间相互暴露互相串味,整理布局来看,也显得有些杂乱无章。OOP的户型则提供了更加优雅的设计,使得各个事物都有自己“该待”的地方(类的概念)。不同的类之间无需知道对方的细节,是需提供彼此之间自身的功能和属性(方法调用等)即可,各类各司其职,达到整体上服务租客的效果。
- 当然不能因为我是一名Java开发而肯定OOP思想的同时否认POP思想,两种思想都是不同的时期人类思考的产物,OOP和POP相对来说是整体和局部的概念,这也是从方法论的角度来看待两者。POP之间互相暴露的功能实现就像OOP实现中某个类,同类属性和方法是有相互了解的权利的。
- 由OOP编程思想主导的项目里,充斥着众多相互依赖同时相互隔离的对象,将一些数据(属性)和算法(方法)封装在类里面,使得系统更加安全,也更便于后期修改维护。但随着系统的复杂性的增强,应用会进行相应的升级。OOP设计思想也逐渐开始暴露弊端。
-
场景一
- 背景A:ClassA,ClassB以及ClassC,有各自需实现的业务方法。
/**
* @Author: Rebecca in Shanghai.
* @Date: 2019/3/30 22:40
*/
public class ClassA {
public void doSomethingInA(){
// Biz code omitted here.
}
}
/**
* @Author: Rebecca in Shanghai.
* @Date: 2019/3/30 22:41
*/
public class ClassB {
public void doSomethingInB(){
// Biz code omitted here.
}
private void checkIfLogIn() {
}
}
/**
* @Author: Rebecca in Shanghai.
* @Date: 2019/3/30 22:41
*/
public class ClassC {
public void doSomethingInC(){
// Biz code omitted here.
}
}
- 背景B: 各个类的各个方法执行前需增加“用户是否登录”的权限校验功能。
- A猿灵机一动。给每一个类的每一个方法原代码之前加入了如下的判断。
/**
* @Author: Rebecca in Shanghai.
* @Date: 2019/3/30 22:40
*/
public class ClassA {
private User user;
public void doSomethingInA(){
if (!user.isStatus()){
return;
}
// Biz code omitted here.
}
}
- 背景B: 各个类的各个方法执行前需增加“用户是否登录”的权限校验功能。
- B猿一阵嘲笑:呵呵,这不是侵入原代码了么?要是再来几个其他的功能入侵怎么办。下面是他的修改方案。将原“非法入侵”部分抽离成另一个私有方法进行调用。其他几个类的方法也进行了同样的处理。
/**
* @Author: Rebecca in Shanghai.
* @Date: 2019/3/30 22:40
*/
public class ClassA {
private User user;
public void doSomethingInA(){
checkIfLogIn();
// Biz code omitted here.
}
private void checkIfLogIn() {
if (user.isStatus()){
return;
}
}
}
- 背景B: 各个类的各个方法执行前需增加“用户是否登录”的权限校验功能。
- C猿看了摇摇头,心想:从整体来看,这三个类中被抽离出来的方法实现的功能不是一样的么?这对于整个系统来说不还是冗余代码么?于是他有了以下的改方案。利用AOP切面,在需进行用户登录校验的方法上加相应的注解,在不侵入原业务代码的同时也能实现较大程度的代码复用。这样的实现,代码的扩展性,可维护性也更强。
/**
* @Author: Rebecca in Shanghai.
* @Date: 2019/3/30 22:40
*/
public class ClassA {
private User user;
@CheckLogInListener
public void doSomethingInA(){
// Biz code omitted here.
}
}
/**
* @Author: Rebecca in Shanghai.
* @Date: 2019/3/30 23:04
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckLogInListener {
String name() default "";
}
/**
* @Author: Rebecca in Shanghai.
* @Date: 2019/3/30 23:09
*/
@Aspect
@Component
@Slf4j
public class CheckLogInHandler {
private static final String EXECUTION="@annotation(checkLogInListener)";
@Before(EXECUTION)
public void checkIfLogIn(CheckLogInListenercheckLogInListener) {
// checkIfLogIn code omitted here.
}
}
- 背景C: 各个类的各个方法执行前去掉“用户是否登录”的权限校验功能,并加上方法前后的日志打印以及业务异常失败重试功能。并要求日志打印的功能执行顺序在也是失败重试功能之前。
- 聪明的A猿听了背景B下C猿的解法后恍然大悟,刷刷刷给出了下面的解法。
/**
* @Author: Rebecca in Shanghai.
* @Date: 2019/3/30 16:30
*/
@Service
public class AopTestService {
@RetryListener(retryForException = Exception.class)
@LogListener(logLevel = "info")
public String testForAopOrder(MyInfo myInfo) {
return myInfo.toString();
}
}
/**
* @Author: Rebecca in Shanghai.
* @Date: 2019/3/29 22:11
*/
@Target(ElementType.METHOD)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface LogListener {
String logLevel() default "";
}
/**
* @Author: Rebecca in Shanghai.
* @Date: 2019/3/29 22:14
*/
@Slf4j
@Aspect
@Component
public class LogHandler {
private static final String EXECUTION = "@annotation(logListener)";
@Around(EXECUTION)
public void doAround(ProceedingJoinPoint point, LogListener logListener) {
log.info("LOG-begins. Args={}, logLevel={}", point.getArgs(), logListener.logLevel());
try {
Object returnObj = point.proceed();
log.info("LOG-ends. ReturnObj={}", returnObj);
} catch (Throwable throwable) {
log.error("Log-ends with error,error={}", throwable);
}
}
}
/**
* @Author: Rebecca in Shanghai.
* @Date: 2019/3/30 16:17
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RetryListener {
Class<?>[] retryForException();
Class<?>[] noRetryForException() default Exception.class;
}
/**
* @Author: Rebecca in Shanghai.
* @Date: 2019/3/30 16:28
*/
@Slf4j
@Aspect
@Component
public class RetryHandler {
private static final String EXECUTION = "@annotation(retryListener)";
@Around(EXECUTION)
public void doAround(ProceedingJoinPoint point, RetryListener retryListener) {
log.info("Retry-begins. Args={}, retryForExceptions={}", point.getArgs(), retryListener.retryForException());
try {
Object returnObj = point.proceed();
log.info("Retry-ends. ReturnObj={}", returnObj);
} catch (Throwable throwable) {
log.error("Retry-ends with error,error={}", throwable);
}
}
}
- 背景C: 各个类的各个方法执行前去掉“用户是否登录”的权限校验功能,并加上方法前后的日志打印以及业务失败重试功能。并要求日志打印的功能执行顺序在也是失败重试功能之前。
- 请求后的结果是:
- 请求后的结果是:
- 背景C: 各个类的各个方法执行前去掉“用户是否登录”的权限校验功能,并加上方法前后的日志打印以及业务失败重试功能。并要求日志打印的功能执行顺序在也是失败重试功能之前。
- 看到希望的执行结果,A猿沾沾自喜,但B猿却提出了A猿的解法有投机取巧的嫌疑:虽然此解法得到了想要的结果,但并没有指定具体的Aspect之间的执行顺序。B猿接过代码,在两个Aspect上分别加上了@Order注解。并给出了解释:@Order中的数字代表优先级,数字越小,优先级越高(越先执行)。
/**
* @Author: Rebecca in Shanghai.
* @Date: 2019/3/29 22:14
*/
@Order(0)
@Slf4j
@Aspect
@Component
public class LogHandler {
private static final String EXECUTION = "@annotation(logListener)";
@Around(EXECUTION)
public void doAround(ProceedingJoinPoint point, LogListener logListener) {
log.info("LOG-begins. Args={}, logLevel={}", point.getArgs(), logListener.logLevel());
try {
Object returnObj = point.proceed();
log.info("LOG-ends. ReturnObj={}", returnObj);
} catch (Throwable throwable) {
log.error("Log-ends with error,error={}", throwable);
}
}
}
/**
* @Author: Rebecca in Shanghai.
* @Date: 2019/3/30 16:28
*/
@Order(1)
@Slf4j
@Aspect
@Component
public class RetryHandler {
private static final String EXECUTION = "@annotation(retryListener)";
@Around(EXECUTION)
public void doAround(ProceedingJoinPoint point, RetryListener retryListener) {
log.info("Retry-begins. Args={}, retryForExceptions={}", point.getArgs(), retryListener.retryForException());
try {
Object returnObj = point.proceed();
log.info("Retry-ends. ReturnObj={}", returnObj);
} catch (Throwable throwable) {
log.error("Retry-ends with error,error={}", throwable);
}
}
}
- 背景C: 各个类的各个方法执行前去掉“用户是否登录”的权限校验功能,并加上方法前后的日志打印以及业务失败重试功能。并要求日志打印的功能执行顺序在也是失败重试功能之前。
- 那么为什么在不指定Order的情况下,A猿的实验结果也是“正确”的呢?C猿通过阅读源码给出了下面的解释:
- 首先,不指定Order的情况下,所有的Aspect的优先级都是最低的(lowest precedence)
- 在不指定Order的情况下,Aspect的执行顺序遵从目标对象在容器中的注册顺序有关。
- 这也就表明了面向切面编程的“不可控性”,当同一套代码部署PROD环境中,如果不指定Order,可能会出现与DEV环境不同的执行顺序,导致不可预料的效果。之前在Spring.doc文档中看过作者形容不指定Order时,不同的Aspect执行的顺序,印象很深的一个词是“Arbitrary”,即Aspect的执行顺序是随意的,不同的jvm对于执行顺序都有其随机算法。不知道这样理解对不对。但希望今后大家在使用AOP编程时能使用有效的方法控制切入代码和原有代码的执行顺序,否则后患无穷。
- 那么为什么在不指定Order的情况下,A猿的实验结果也是“正确”的呢?C猿通过阅读源码给出了下面的解释: