背景
今天師兄和我說,“之葉,你設(shè)計一個方案,把目前業(yè)務(wù)方法中和業(yè)務(wù)無關(guān)的邏輯都抽離出來,讓每個方法只關(guān)心自己的業(yè)務(wù)邏輯”。我會心一笑 ??(因為我們早應(yīng)該做這件事情了)

現(xiàn)有的業(yè)務(wù)方法
之前代碼里每個業(yè)務(wù)方法幾乎都是長這樣:
public class XxxServiceImpl implements XxxService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public XxxResponse<...> queryXxx(XxxRequest request) {
// 記錄方法開始時間
long startTime = System.currentTimeMillis();
// 構(gòu)造響應(yīng)
XxxResponse<PagedData> response = new XxxResponse();
// 設(shè)置調(diào)用機器
response.setHost(ServiceUtils.getHost());
// 設(shè)置方法開始執(zhí)行時間
response.setSysTime(startTime);
try {
// 業(yè)務(wù)邏輯代碼
......
response.setData(pagedData);
} catch(Throwable e) {
// 拋出異常時候執(zhí)行
logger.error(...);
response.failBizInfo(ServiceBizError.UNKNOWN_ERROR);
} finally {
// 設(shè)置方法耗時
long costTime = System.currentTimeMillis() - startTime;
response.setCostTime(costTime);
// 記錄調(diào)用信息
logger.info(...);
}
// 返回響應(yīng)
return response;
}
// 后面還有若干個類似的業(yè)務(wù)方法
......
}
很容易可以看出,記錄方法開始時間、捕獲異常并處理、打印錯誤日志、記錄方法耗時 這些都是和業(yè)務(wù)沒有關(guān)系的,業(yè)務(wù)方法關(guān)心的,只應(yīng)該是 業(yè)務(wù)邏輯代碼 才對。一兩個方法這個樣子看起來也還好,但是目前項目里面已經(jīng)有十幾個這種樣子的方法了,而且以后還會更多,重復(fù)代碼對我們簡直不能忍 —— 是的,我也早就看這些業(yè)務(wù)方法不順眼了,安排!

設(shè)計方案
AOP 登場
大家都聽過 Spring 有兩大神器 —— IoC 和 AOP —— 了解 AOP 的人,都知道 AOP 是 Aspect Oriented Programming,即面向切面編程:通過預(yù)編譯方式(CGLib)或者運行期動態(tài)代理(JDK Proxy)來實現(xiàn)程序功能代理的技術(shù)。此時的情況,就完美匹配 AOP 的應(yīng)用場景。我們可以定義一個切點(PointCut,也叫連接點),然后對和 切點匹配的方法,織入(Weaving)切面(Aspect),進行增強(Advice)處理:即在方法 調(diào)用前、調(diào)用后 或者 拋出異常時,進行額外的處理。
實現(xiàn)方案
搭建示例項目
為了方便示例,首先我們建立一個簡單的 SpringBoot 項目,并添加示例的 Service 和 Controller:

加入一個 DemoService:
public interface DemoService {
/**
* 除法運算
*
* @param request 除法運算請求
* @return 除法運算結(jié)果
*/
DivisionResponse divide(DivisionRequest request);
}
DemoService 的實現(xiàn):
@Service
public class DemoServiceImpl implements DemoService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public DivisionResponse divide(DivisionRequest request) {
long startTime = System.currentTimeMillis();
DivisionResponse response = new DivisionResponse();
// 設(shè)置方法調(diào)用的時間
response.setSysTime(startTime);
// 設(shè)置方法調(diào)用的機器
response.setHost(getHost());
// 請求參數(shù)
int dividend = request.getDividend();
int divisor = request.getDivisor();
try {
// 模擬檢查業(yè)務(wù)參數(shù)
// ...檢查業(yè)務(wù)參數(shù)...
TimeUnit.MILLISECONDS.sleep(300);
// 模擬執(zhí)行業(yè)務(wù)
int result = dividend / divisor;
// 設(shè)置業(yè)務(wù)執(zhí)行結(jié)果
response.setData(result);
// 調(diào)用正常
response.setSuccess(true);
} catch (Throwable e) {
// 調(diào)用出錯
response.setSuccess(false);
// 記錄執(zhí)行錯誤
logger.error("DemoServiceImpl.divide 執(zhí)行出錯", e);
response.setPrompt(e.getMessage());
} finally {
// 設(shè)置方法調(diào)用耗時
response.setCostTime(System.currentTimeMillis() - startTime);
// 記錄方法調(diào)用信息
logger.info("DemoServiceImpl.divide request={}, response={}", request, response);
}
return response;
}
/**
* 模擬獲得服務(wù)器名稱
*/
private String getHost() {
return UUID.randomUUID().toString().substring(0, 8);
}
}
再加入一個 DemoController:
@RestController
public class DemoController {
@Resource
private DemoService demoService;
@GetMapping("division.do")
public DivisionResponse doDivision(@RequestParam int a,
@RequestParam int b) {
// 構(gòu)建請求
DivisionRequest request = new DivisionRequest();
request.setDividend(a);
request.setDivisor(b);
// 執(zhí)行
return demoService.divide(request);
}
}
啟動應(yīng)用,看一下目前調(diào)用業(yè)務(wù)方法時的情況:
-
調(diào)用正常情況(a=2,b=1)
正常情況 -
調(diào)用出錯情況(a=2,b=0)
錯誤情況
編寫切面
現(xiàn)在的 Java Web 應(yīng)用,使用注解來進行配置和做 AOP 已經(jīng)是主流 —— 因為相比 XML,注解更簡單而且更好用。所以我們先定義一個 @ServiceMethodAspectAnno:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceMethodAspectAnno {
}
這個注解的目標(biāo)類型是 方法,并且在 運行期 保留。然后我們就可以來定義切面了,這個切面會攔截所有被 @ServiceMethodAspectAnno 注解的方法,并做織入處理:
@Component
@Aspect // @Aspect 告訴 Spring 這是一個切面
public class ServiceMethodAspect {
/**
* 方法連接點(處理被 @ServiceMethodAspectAnno 注解的方法)
*/
@Pointcut("@annotation(org.mizhou.aop.aspect.anno.ServiceMethodAspectAnno)")
public void methodPointcut() { }
/**
* 切入被 @ServiceMethodAspectAnno 注解的方法
*
* @param point 連接點
*
* @return 方法返回值
* @throws Throwable 可能拋出的異常
*/
@Around("methodPointcut()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
// 方法不匹配,即不是要處理的業(yè)務(wù)方法
if (!isMatched(point)) {
// 方法不匹配時的執(zhí)行動作
onMismatch(point);
// 直接執(zhí)行該方法并返回結(jié)果
return point.proceed();
}
// 方法返回值
Object result;
// 是否拋出異常
boolean thrown = false;
// 記下開始執(zhí)行的時間
long startTime = System.currentTimeMillis();
try {
// 執(zhí)行目標(biāo)方法
result = point.proceed();
} catch (Throwable e) {
// 記錄拋出了異常
thrown = true;
// 處理異常
onThrow(point, e);
// 拋出異常的情況下,則構(gòu)造一個返回值的實例,用于業(yè)務(wù)服務(wù)方法的返回
result = getOnThrown(point, e);
}
// 切面結(jié)束
onComplete(point, startTime, thrown, result);
return result;
}
/**
* 是否是匹配的方法<br/>
* 限定方法類型入?yún)⑵ヅ?BaseRequest,返回值匹配 BaseResponse
*
* @param point 方法的連接點
* @return 是可以處理的方法返回 true,否則返回 false
*/
private boolean isMatched(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Class returnType = signature.getReturnType();
// returnType 是 BaseResponse 或其子類型
if (BaseResponse.class.isAssignableFrom(returnType)) {
Class[] parameterTypes = signature.getParameterTypes();
// 參數(shù)必須是 BaseRequest 或其子類型
return parameterTypes.length == 1
&& BaseRequest.class.isAssignableFrom(parameterTypes[0]);
}
return false;
}
/**
* 如果是不要處理的方法,執(zhí)行的動作
*
* @param point 方法的連接點
*/
private void onMismatch(ProceedingJoinPoint point) {
Logger logger = getLogger(point);
String logTag = getLogTag(point);
logger.warn("{} 不是 @{} 可以處理的方法", logTag, ServiceMethodAspectAnno.class.getSimpleName());
}
/**
* 拋出異常時,執(zhí)行的動作
*
* @param point 方法的連接點
* @param e 拋出的異常
*/
private void onThrow(ProceedingJoinPoint point, Throwable e) {
Logger logger = getLogger(point);
String logTag = getLogTag(point);
logger.error("{} 調(diào)用出錯", logTag, e);
}
/**
* 構(gòu)建拋出異常時的返回值
*
* @param point 方法的連接點
* @param e 拋出的異常
* @return 拋出異常時的返回值
*/
@SuppressWarnings("unchecked")
private BaseResponse getOnThrown(ProceedingJoinPoint point, Throwable e) throws Exception {
MethodSignature signature = (MethodSignature) point.getSignature();
Class<? extends BaseResponse> returnType = signature.getReturnType();
BaseResponse response = returnType.newInstance();
response.setPrompt(e.getMessage());
response.setSuccess(false);
return response;
}
/**
* 切面完成時,執(zhí)行的動作
*
* @param point 方法的連接點
* @param startTime 執(zhí)行的開始時間
* @param thrown 是否拋出異常
* @param result 執(zhí)行獲得的結(jié)果
*/
private void onComplete(ProceedingJoinPoint point, long startTime, boolean thrown, Object result) {
BaseResponse response = (BaseResponse) result;
// 設(shè)置方法調(diào)用的時間
response.setSysTime(startTime);
// 設(shè)置方法調(diào)用的機器
response.setHost(getHost());
// 設(shè)置方法調(diào)用耗時
response.setCostTime(System.currentTimeMillis() - startTime);
Logger logger = getLogger(point);
// point.getArgs() 獲得方法調(diào)用入?yún)? Object request = point.getArgs()[0];
// 記錄方法調(diào)用信息
logger.info("{}, request={}, response={}", getLogTag(point), request, response);
}
/**
* 模擬獲得服務(wù)器名稱
*/
private String getHost() {
return UUID.randomUUID().toString().substring(0, 8);
}
/**
* 獲得被代理對象的 Logger
*
* @param point 連接點
* @return 被代理對象的 Logger
*/
private Logger getLogger(ProceedingJoinPoint point) {
// 獲得被代理對象
Object target = point.getTarget();
return LoggerFactory.getLogger(target.getClass());
}
/**
* LogTag = 類名.方法名
*
* @param point 連接點
* @return 目標(biāo)類名.執(zhí)行方法名
*/
private String getLogTag(ProceedingJoinPoint point) {
Object target = point.getTarget();
String className = target.getClass().getSimpleName();
MethodSignature signature = (MethodSignature) point.getSignature();
String methodName = signature.getName();
return className + "." + methodName;
}
}
最后我們就可以簡化我們的業(yè)務(wù)方法了:
@ServiceMethodAspectAnno
public DivisionResponse divide(DivisionRequest request) throws Exception {
DivisionResponse response = new DivisionResponse();
// 請求參數(shù)
int dividend = request.getDividend();
int divisor = request.getDivisor();
// 模擬檢查業(yè)務(wù)參數(shù)
// ...檢查業(yè)務(wù)參數(shù)...
TimeUnit.MILLISECONDS.sleep(300);
// 模擬執(zhí)行業(yè)務(wù)
int result = dividend / divisor;
// 設(shè)置業(yè)務(wù)執(zhí)行結(jié)果
response.setData(result);
return response;
}
可以看到,目前業(yè)務(wù)方法只保留了業(yè)務(wù)相關(guān)的邏輯,并且方法上使用了 @ServiceMethodAspectAnno 進行注解。原來的 記錄方法開始時間、捕獲異常并處理、打印錯誤日志、記錄方法耗時 等功能,都被放到了切面當(dāng)中。
驗證切面
現(xiàn)在來驗證下此時切面是否可以按預(yù)期工作。先加入一個新的 Service 以及其實現(xiàn),用于驗證切面ServiceMethodAspect 是否能夠正確篩選出要處理的方法。
NumberService.java
public interface NumberService {
/**
* 除法運算
*
* @param dividend 被除數(shù)
* @param divisor 除數(shù)
* @return 商
* @throws Exception 可能產(chǎn)生的異常(切面會捕獲)
*/
int divide(int dividend, int divisor) throws Exception;
}
NumberServiceImpl.java
@Service
public class NumberServiceImpl implements NumberService {
@Override
@ServiceMethodAspectAnno // 測試切面能夠篩選方法
public int divide(int dividend, int divisor) throws Exception {
// 模擬檢查業(yè)務(wù)參數(shù)
// ...檢查業(yè)務(wù)參數(shù)...
TimeUnit.MILLISECONDS.sleep(300);
// 模擬執(zhí)行業(yè)務(wù)
int result = dividend / divisor;
return result;
}
}
因為我們限定了可以被織入的方法必須參數(shù)為 BaseRequest,且返回值為 BaseResponse —— 顯然 NumberService.divide 因為返回的是 int 不滿足這一點。
在 DemoController 中再增加一個處理請求的方法:
@RestController
public class DemoController {
......
@Resource
private NumberService numberService;
@GetMapping("another.do")
public Integer doAnotherDivision(@RequestParam int a,
@RequestParam int b) throws Exception {
return numberService.divide(a, b);
}
}
重啟 SpringBoot 應(yīng)用:
調(diào)用正常時(http://localhost:8080/division.do?a=2&b=1):

調(diào)用出錯時(http://localhost:8080/division.do?a=2&b=0):

測試與注解不匹配的方法(http://localhost:8080/another.do?a=2&b=1):

滿意~ 這下再加入新的業(yè)務(wù)方法,就不用再在每個方法中寫那些與業(yè)務(wù)無關(guān)的功能代碼了,直接一個注解搞定~

擴展方案
問題
本來開開心心可以收工了,也不知道是誰突然在我腦子里發(fā)出了一個聲音:如果下次其他方面的業(yè)務(wù),入?yún)⒉皇?BaseRequest,返回值不是 BaseResponse,或者要在 onThrow 時記錄不同的日志 —— 那么使用上面的方案,是不是要編寫一個新的切面?
也是, isMatched、onMismatch、onThrow、onComplete 這些方法,是每個切面都會有的。并且對于不同的業(yè)務(wù),可能會有不同的實現(xiàn),所以應(yīng)該由一個更加通用的方案,方便將來進行擴展。
思考
我們一般用的注解,像下面這樣子的:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
都是可以指定參數(shù)的。那么我們不也可以在 @ServiceMethodAspectAnno 中,指定一個 處理類,專門用來處理一種類型的業(yè)務(wù)方法嗎?靈感突現(xiàn):
- 可以將 isMatched、onMismatch、onThrow、getOnThrow,onComplete 這些方法,放到一個方法切面處理器接口中
- 然后不同業(yè)務(wù)方法的切面處理器,都去實現(xiàn)這個接口,針對自己的業(yè)務(wù)場景實現(xiàn)處理器的每個方法
- 提供一些方法的默認(rèn)實現(xiàn),例如 onMismatch 和 onThrow,這兩個方法一般都是記錄下相應(yīng)的日志
實現(xiàn)
首先我們定義方法切面處理器的接口 MethodAspectProcessor<R>:
/**
* 方法切面處理器
*/
public interface MethodAspectProcessor<R> {
/**
* 是否是要處理的方法
*
* @param point 方法的連接點
* @return 是要處理的方法返回 true,否則返回 false
*/
boolean isMatched(ProceedingJoinPoint point);
/**
* 如果是不要處理的方法,執(zhí)行的動作
*
* @param point 方法的連接點
*/
default void onMismatch(ProceedingJoinPoint point) {
}
// 下面的方法,只在 isMatched 返回 true 時有效
/**
* 執(zhí)行之前的動作<br>
*
* @param point 方法的連接點
* @return 返回 true 則表示繼續(xù)向下執(zhí)行;返回 false 則表示禁止調(diào)用目標(biāo)方法,
* 方法切面處理會此時會先調(diào)用 getOnForbid 方法獲得被禁止執(zhí)行時的返回值,然后調(diào)用 onComplete 方法結(jié)束切面
*/
default boolean onBefore(ProceedingJoinPoint point) {
return true;
}
/**
* 禁止調(diào)用目標(biāo)方法時(onBefore 返回 false 時),執(zhí)行該方法構(gòu)建返回值
*
* @param point 方法的連接點
* @return 禁止調(diào)用目標(biāo)方法時的返回值
*/
default R getOnForbid(ProceedingJoinPoint point) {
return null;
}
/**
* 拋出異常時,執(zhí)行的動作
*
* @param point 方法的連接點
* @param e 拋出的異常
*/
void onThrow(ProceedingJoinPoint point, Throwable e);
/**
* 構(gòu)建拋出異常時的返回值
*
* @param point 方法的連接點
* @param e 拋出的異常
* @return 拋出異常時的返回值
*/
R getOnThrow(ProceedingJoinPoint point, Throwable e);
/**
* 切面完成時,執(zhí)行的動作
*
* @param point 方法的連接點
* @param startTime 執(zhí)行的開始時間
* @param forbidden 目標(biāo)方法是否被禁止執(zhí)行
* @param thrown 目標(biāo)方法執(zhí)行時是否拋出異常
* @param result 執(zhí)行獲得的結(jié)果
*/
default void onComplete(ProceedingJoinPoint point, long startTime, boolean forbidden, boolean thrown, R result) {
}
}
接著我們改造下 @ServiceMethodAspectAnno,因為我們現(xiàn)在應(yīng)該是在做一個通用的方法處理器了,所以先給它改名叫 @MethodAspectAnno,然后加入表示方法切面處理器的字段:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAspectAnno {
Class<? extends MethodAspectProcessor> value();
}
然后提供一個 MethodAspectProcessor 抽象類 AbstractMethodAspectProcessor<R>,包括了 onMismatch 和 onThrow 的默認(rèn)實現(xiàn):
/**
* 提供默認(rèn)的兩個功能:<br/>
* (1)方法不匹配時記錄日志<br/>
* (2)目標(biāo)方法拋出異常時記錄日志
*/
public abstract class AbstractMethodAspectProcessor<R> implements MethodAspectProcessor<R> {
@Override
public void onMismatch(ProceedingJoinPoint point) {
Logger logger = getLogger(point);
String logTag = getLogTag(point);
// 獲得方法簽名
MethodSignature signature = (MethodSignature) point.getSignature();
// 獲得方法
Method method = signature.getMethod();
// 獲得方法的 @MethodAspectAnno 注解
MethodAspectAnno anno = method.getAnnotation(MethodAspectAnno.class);
// 獲得方法切面處理器的 Class
Class<? extends MethodAspectProcessor> processorType = anno.value();
String processorName = processorType.getSimpleName();
// 如果是接口或者抽象類
if (processorType.isInterface() || Modifier.isAbstract(processorType.getModifiers())) {
logger.warn("{} 需要指定具體的切面處理器,因為 {} 是接口或者抽象類", logTag, processorName);
return;
}
logger.warn("{} 不是 {} 可以處理的方法,或者 {} 在 Spring 容器中不存在", logTag, processorName, processorName);
}
@Override
public void onThrow(ProceedingJoinPoint point, Throwable e) {
Logger logger = getLogger(point);
String logTag = getLogTag(point);
logger.error("{} 執(zhí)行時出錯", logTag, e);
}
/**
* 獲得被代理類的 Logger
*
* @param point 連接點
* @return 被代理類的 Logger
*/
protected Logger getLogger(ProceedingJoinPoint point) {
Object target = point.getTarget();
return LoggerFactory.getLogger(target.getClass());
}
/**
* LogTag = 類名.方法名
*
* @param point 連接點
* @return 目標(biāo)類名.執(zhí)行方法名
*/
protected String getLogTag(ProceedingJoinPoint point) {
Object target = point.getTarget();
String className = target.getClass().getSimpleName();
MethodSignature signature = (MethodSignature) point.getSignature();
String methodName = signature.getName();
return className + "." + methodName;
}
}
再提供一個方法不匹配時的實現(xiàn) MismatchMethodAspectProcessor<R>,作為接口的默認(rèn)實現(xiàn):
/**
* 方法不匹配時的方法切面處理器<br/>
* isMatched 方法返回 false,即不會對任何方法做處理<br/>
* 方法執(zhí)行之前,會調(diào)用 onMismatch 方法,該方法在 AbstractMethodAspectProcessor 提供默認(rèn)實現(xiàn)
*/
@Component
public class MismatchMethodAspectProcessor<R> extends AbstractMethodAspectProcessor<R> {
@Override
public boolean isMatched(ProceedingJoinPoint point) {
return false;
}
@Override
public R getOnThrow(ProceedingJoinPoint point, Throwable e) {
// 不會被調(diào)用
return null;
}
}
此時我們再定義 DemoService 中方法的專用方法切面處理器 ServiceMethodProcessor,把之前方案中的代碼拿過來就行:
/**
* 業(yè)務(wù)方法切面處理器
*/
@Component
public class ServiceMethodProcessor extends AbstractMethodAspectProcessor<BaseResponse> {
/**
* 是否是要處理的方法<br/>
* 限定方法類型入?yún)⑵ヅ?BaseRequest,返回值匹配 BaseResponse
*
* @param point 方法的連接點
* @return 是要處理的方法返回 true,否則返回 false
*/
@Override
public boolean isMatched(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Class returnType = signature.getReturnType();
// returnType 是 BaseResponse 或其子類型
if (BaseResponse.class.isAssignableFrom(returnType)) {
Class[] parameterTypes = signature.getParameterTypes();
// 參數(shù)必須是 BaseRequest 或其子類型
return parameterTypes.length == 1
&& BaseRequest.class.isAssignableFrom(parameterTypes[0]);
}
return false;
}
/**
* 構(gòu)建拋出異常時的返回值<br/>
*
* @param point 方法的連接點
* @param e 拋出的異常
* @return 拋出異常時的返回值
*/
@Override
@SuppressWarnings("unchecked")
public BaseResponse getOnThrow(ProceedingJoinPoint point, Throwable e) {
MethodSignature signature = (MethodSignature) point.getSignature();
Class<? extends BaseResponse> returnType = signature.getReturnType();
// 構(gòu)造拋出異常時的返回值
BaseResponse response = newInstance(returnType);
response.setPrompt(e.getMessage());
response.setSuccess(false);
return response;
}
/**
* 切面完成時,執(zhí)行的動作
*
* @param point 方法的連接點
* @param startTime 執(zhí)行的開始時間
* @param result 執(zhí)行獲得的結(jié)果
*/
@Override
public void onComplete(ProceedingJoinPoint point, long startTime, boolean forbidden, boolean thrown, BaseResponse result) {
// 設(shè)置方法調(diào)用的時間
result.setSysTime(startTime);
// 設(shè)置方法調(diào)用的機器
result.setHost(getHost());
// 設(shè)置方法調(diào)用耗時
result.setCostTime(System.currentTimeMillis() - startTime);
Logger logger = getLogger(point);
// point.getArgs() 獲得方法調(diào)用入?yún)? Object request = point.getArgs()[0];
// 記錄方法調(diào)用信息
logger.info("{}, request={}, response={}", getLogTag(point), request, result);
}
private BaseResponse newInstance(Class<? extends BaseResponse> type) {
try {
return type.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
return new CommonResponse();
}
}
/**
* 模擬獲得服務(wù)器名稱
*/
private String getHost() {
return UUID.randomUUID().toString().substring(0, 8);
}
}
我們還需要一個方法,來通過注解獲取 和被注解方法匹配的 方法切面處理器,在 MethodAspectProcessor 加入一個靜態(tài)方法:
/**
* 通過注解獲取 和被注解方法匹配的 切面處理器
*
* @param anno 注解
* @return 匹配的切面處理器
* @throws Exception 反射創(chuàng)建切面處理器時的異常
*/
static MethodAspectProcessor from(MethodAspectAnno anno) throws Exception {
Class<? extends MethodAspectProcessor> processorType = anno.value();
// 如果指定的是接口或者抽象類(即使用方非要搞事情)
if (processorType.isInterface() || Modifier.isAbstract(processorType.getModifiers())) {
processorType = MismatchMethodAspectProcessor.class;
}
return processorType.newInstance();
}
修改下之前的方法切面,同樣的,因為該方法切面不僅僅是可以處理 Service 方法了,于是改名叫 MethodAspect。通過在 @Around 中加入 @annotation(anno),可以將注解實例注入到參數(shù)中:
@Aspect
@Component
public class MethodAspect {
/**
* 方法連接點(處理被 @MethodAspectAnno 注解的方法)
*/
@Pointcut("@annotation(org.mizhou.aop.aspect.anno.MethodAspectAnno)")
public void methodPointcut() { }
/**
* 切入被 @MethodAspectAnno 注解的方法
*
* @param point 連接點
* @param anno 注解
*
* @return 方法返回值
* @throws Throwable 可能拋出的異常
*/
@Around("methodPointcut() && @annotation(anno)")
public Object doAround(ProceedingJoinPoint point, MethodAspectAnno anno) throws Throwable {
// 通過注解獲取處理器
MethodAspectProcessor processor = MethodAspectProcessor.from(anno);
// 方法不匹配,即不是要處理的業(yè)務(wù)方法
if (!processor.isMatched(point)) {
// 方法不匹配時的執(zhí)行動作
processor.onMismatch(point);
// 直接執(zhí)行該方法并返回結(jié)果
return point.proceed();
}
// 執(zhí)行之前
boolean permitted = processor.onBefore(point);
// 開始執(zhí)行的時間
long startTime = System.currentTimeMillis();
// 方法返回值
Object result;
// 是否拋出了異常
boolean thrown = false;
// 目標(biāo)方法被允許執(zhí)行
if (permitted) {
try {
// 執(zhí)行目標(biāo)方法
result = point.proceed();
} catch (Throwable e) {
// 拋出異常
thrown = true;
// 處理異常
processor.onThrow(point, e);
// 拋出異常的情況下,則構(gòu)造一個返回值的實例,用于業(yè)務(wù)服務(wù)方法的返回
result = processor.getOnThrow(point, e);
}
}
// 目標(biāo)方法被禁止執(zhí)行
else {
// 禁止執(zhí)行時的返回值
result = processor.getOnForbid(point);
}
// 切面結(jié)束
processor.onComplete(point, startTime, !permitted, thrown, result);
return result;
}
}
最后在 DemoServiceImpl 的業(yè)務(wù)方法上,應(yīng)用 @MethodAspectAnno,并指定處理方法的方法切面處理器:
@MethodAspectAnno(ServiceMethodProcessor.class)
public DivisionResponse divide(DivisionRequest request) throws Exception {
DivisionResponse response = new DivisionResponse();
// 請求參數(shù)
int dividend = request.getDividend();
int divisor = request.getDivisor();
// 模擬檢查業(yè)務(wù)參數(shù)
// ...檢查業(yè)務(wù)參數(shù)...
TimeUnit.MILLISECONDS.sleep(300);
// 模擬執(zhí)行業(yè)務(wù)
int result = dividend / divisor;
// 設(shè)置業(yè)務(wù)執(zhí)行結(jié)果
response.setData(result);
return response;
}
以及在不匹配的方法上,應(yīng)用 @MethodAspectAnno(ServiceMethodProcessor.class):
@Service
public class NumberServiceImpl implements NumberService {
@Override
// 不匹配的方法處理器
@MethodAspectAnno(ServiceMethodProcessor.class)
public int divide(int dividend, int divisor) throws Exception {
// 模擬檢查業(yè)務(wù)參數(shù)
// ...檢查業(yè)務(wù)參數(shù)...
TimeUnit.MILLISECONDS.sleep(300);
// 模擬執(zhí)行業(yè)務(wù)
int result = dividend / divisor;
return result;
}
}
大功告成,來測試一下:
正常調(diào)用(http://localhost:8080/division.do?a=2&b=1):

調(diào)用出錯(http://localhost:8080/division.do?a=2&b=0):

測試與切面處理器不匹配的方法(http://localhost:8080/another.do?a=2&b=1):

優(yōu)化
此時我的耳邊又響起了一個聲音(為什么我想的總是這么多...):
不管是 MismatchMethodAspectProcessor 還是用于業(yè)務(wù)方法的 ServiceMethodProcessor,或者將來定義的一些其他的 MethodAspectProcessor,它們因為沒有定義變量或者沒有與其他類分享變量,所以它們是線程安全的,沒必要每次在執(zhí)行切面調(diào)用時,都去新建一個對應(yīng)的方法切面處理器。
緩存
于是想到了 Netty 里面的 @Sharable,用來標(biāo)記一個 ChannelHandler 是可共享的。所以我們也可以先定義一個 @Sharble 注解,用來標(biāo)記一個 MethodAspectProcessor 是可共享的,即線程安全的。然后對被 @Sharable 注解的方法處理器,進行緩存 —— 緩存的鍵就是方法切面處理器的 Class,值就是方法處理器的實例。定義 @Sharable 注解:
/**
* 標(biāo)記一個類可共享
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sharable {
}
然后修改 MethodAspectProcessor 中從注解獲取方法切面處理器的 from 方法:
public interface MethodAspectProcessor<R> {
/**
* 用于緩存被 @Sharable 注解的 MethodAspectProcessor(即線程安全可共享的)
*/
Map<Class, MethodAspectProcessor> PROCESSOR_CACHE = new ConcurrentHashMap<>();
......
/**
* 獲取 和被注解方法匹配的 切面處理器
*
* @param anno 注解
* @return 匹配的切面處理器
* @throws Exception 反射創(chuàng)建切面處理器時的異常
*/
static MethodAspectProcessor from(MethodAspectAnno anno) throws Exception {
// 獲取方法切面處理器的類型
Class<? extends MethodAspectProcessor> processorType = anno.value();
Sharable sharableAnno = processorType.getAnnotation(Sharable.class);
// processorType 上存在 @Sharable 注解,方法處理器可共享
if (sharableAnno != null) {
// 嘗試先從緩存中獲取
MethodAspectProcessor processor = PROCESSOR_CACHE.get(processorType);
// 緩存中存在對應(yīng)的方法處理器
if (processor != null) {
return processor;
}
}
// 如果指定的處理器類是接口或者抽象類
if (processorType.isInterface() || Modifier.isAbstract(processorType.getModifiers())) {
processorType = MismatchMethodAspectProcessor.class;
}
// 創(chuàng)建切面處理器
MethodAspectProcessor processor = processorType.newInstance();
// 處理器可共享
if (sharableAnno != null) {
// 對 方法處理器 進行緩存
PROCESSOR_CACHE.put(processorType, processor);
}
return processor;
}
}
OK,完美,非常滿意~

后記
在最近的實踐中,發(fā)現(xiàn)我們的 MethodAspectProcessor 許多時候都不能脫離 Spring 容器,即需要讓 MethodAspectProcessor 成為 Spring 容器中的 Bean,從而結(jié)合 Spring 容器中的其他 Bean,完成更加復(fù)雜的功能。例如某個方法需要實現(xiàn) 3 秒內(nèi)防重復(fù)調(diào)用,我們便需要使用到緩存,而緩存相關(guān)的 Bean 是由 Spring 來管理的。所以我們現(xiàn)在改造我們的 AOP 方法,讓所有的 MethodAspectProcessor 都交給 Spring 管理。首先我們修改各個 MethodAspectProcessor,使用 @Component 注解讓其成為 Spring 容器中的 Bean:
@Component
public class MismatchMethodAspectProcessor<R> extends AbstractMethodAspectProcessor<R>
@Component
public class ServiceMethodProcessor extends AbstractMethodAspectProcessor<BaseResponse>
修改 MethodAspect,讓其從 Spring 容器中獲取方法切面處理器:
@Aspect
@Component
public class MethodAspect implements ApplicationContextAware {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private ApplicationContext appContext;
/**
* 方法連接點(處理被 @MethodAspectAnno 注解的方法)
*/
@Pointcut("@annotation(xyz.mizhoux.aop.aspect.anno.MethodAspectAnno)")
public void methodPointcut() { }
/**
* 切入被 @MethodAspectAnno 注解的方法
*
* @param point 連接點
* @param anno 注解
* @return 方法返回值
* @throws Throwable 可能拋出的異常
*/
@Around("methodPointcut() && @annotation(anno)")
public Object doAround(ProceedingJoinPoint point, MethodAspectAnno anno) throws Throwable {
// 通過注解獲取處理器
MethodAspectProcessor processor = getProcessor(anno);
.......
}
private MethodAspectProcessor getProcessor(MethodAspectAnno anno) {
Class<? extends MethodAspectProcessor> processorType = anno.value();
try {
return appContext.getBean(processorType);
} catch (BeansException ex) {
logger.error("{} 在 Spring 容器中不存在", processorType.getName());
}
return appContext.getBean(MismatchMethodAspectProcessor.class);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
appContext = applicationContext;
}
}
本文最終方案的代碼可見:aop-method

