AOP是Aspect Oriented Programing的簡稱,面向切面編程。AOP適合於那些具有橫切邏輯的應用:如性能監測,訪問控制,事務管理、緩存、對象池管理以及日志記錄。AOP將這些分散在各個業務邏輯中的代碼通過橫向切割的方式抽取到一個獨立的模塊中。AOP 實現的關鍵就在於 AOP 框架自動創建的 AOP 代理,AOP 代理則可分為靜態代理和動態代理兩大類,其中靜態代理是指使用 AOP 框架提供的命令進行編譯,從而在編譯階段就可生成 AOP 代理類,因此也稱為編譯時增強;而動態代理則在運行時借助於 JDK 動態代理、CGLIB 等在內存中“臨時”生成 AOP 動態代理類,因此也被稱為運行時增強。
代理對象的方法 = 增強處理 + 被代理對象的方法
Spring AOP 則采用運行時生成 AOP 代理類,因此無需使用特定編譯器進行處理。由於 Spring AOP 需要在每次運行時生成 AOP 代理,因此性能略差一些。
AOP使用場景
AOP用來封裝橫切關注點,具體可以在下面的場景中使用
Authentication 權限
Caching 緩存
Context passing 內容傳遞
Error handling 錯誤處理
Lazy loading 懶加載
Debugging 調試
logging, tracing, profiling and monitoring 記錄跟蹤 優化 校准
Performance optimization 性能優化
Persistence 持久化
Resource pooling 資源池
Synchronization 同步
Transactions 事務
AOP相關概念
方面(Aspect):一個關注點的模塊化,這個關注點實現可能另外橫切多個對象。事務管理是J2EE應用中一個很好的橫切關注點例子。方面用Spring的 Advisor或攔截器實現。
連接點(Joinpoint): 程序執行過程中明確的點,如方法的調用或特定的異常被拋出
通知(Advice): 在特定的連接點,AOP框架執行的動作。各種類型的通知包括“around”、“before”和“throws”通知。通知類型將在下面討論。許多AOP框架包括Spring都是以攔截器做通知模型,維護一個“圍繞”連接點的攔截器鏈。Spring中定義了四個advice: BeforeAdvice, AfterAdvice, ThrowAdvice和DynamicIntroductionAdvice
切入點(Pointcut): 指定一個通知將被引發的一系列連接點的集合。AOP框架必須允許開發者指定切入點:例如,使用正則表達式。 Spring定義了Pointcut接口,用來組合MethodMatcher和ClassFilter,可以通過名字很清楚的理解, MethodMatcher是用來檢查目標類的方法是否可以被應用此通知,而ClassFilter是用來檢查Pointcut是否應該應用到目標類上
引入(Introduction): 添加方法或字段到被通知的類。 Spring允許引入新的接口到任何被通知的對象。例如,你可以使用一個引入使任何對象實現 IsModified接口,來簡化緩存。Spring中要使用Introduction, 可有通過DelegatingIntroductionInterceptor來實現通知,通過DefaultIntroductionAdvisor來配置Advice和代理類要實現的接口
目標對象(Target Object): 包含連接點的對象。也被稱作被通知或被代理對象。POJO
AOP代理(AOP Proxy): AOP框架創建的對象,包含通知。 在Spring中,AOP代理可以是JDK動態代理或者CGLIB代理。
織入(Weaving): 組裝方面來創建一個被通知對象。這可以在編譯時完成(例如使用AspectJ編譯器),也可以在運行時完成。Spring和其他純Java AOP框架一樣,在運行時完成織入。
日志應用:
實現登陸和日志管理(使用Spring AOP
1)LoginService LogService TestMain
2)用Spring 管理 LoginService 和 LogService 的對象
3)確定哪些連接點是切入點,在配置文件中
4)將LogService封裝為通知
5)將通知植入到切入點
6)客戶端調用目標
<aop:config>
<aop:pointcut expression="execution(* cn.com.spring.service.impl.*.*(..))" id="myPointcut"/>
<!--將哪個-->
<aop:aspect id="dd" ref="logService">
<aop:before method="log" pointcut-ref="myPointcut"/>
</aop:aspect>
</aop:config>
execution(* * cn.com.spring.service.impl.*.*(..))
1)* 所有的修飾符
2)* 所有的返回類型
3)* 所有的類名
4)* 所有的方法名
5)* ..所有的參數名
1.ILoginService.java
package cn.com.spring.service;
public interface ILoginService {
public boolean login(String userName, String password);
}
2.LoginServiceImpl.java
package cn.com.spring.service.impl;
import cn.com.spring.service.ILoginService;
public class LoginServiceImpl implements ILoginService {
public boolean login(String userName, String password) {
System.out.println("login:" + userName + "," + password);
return true;
}
}
3.ILogService.java
package cn.com.spring.service;
import org.aspectj.lang.JoinPoint;
public interface ILogService {
//無參的日志方法
public void log();
//有參的日志方法
public void logArg(JoinPoint point);
//有參有返回值的方法
public void logArgAndReturn(JoinPoint point,Object returnObj);
}
4.LogServiceImpl.java
package cn.com.spring.service.impl;
import org.aspectj.lang.JoinPoint;
import cn.com.spring.service.ILogService;
public class LogServiceImpl implements ILogService {
@Override
public void log() {
System.out.println("*************Log*******************");
}
//有參無返回值的方法
public void logArg(JoinPoint point) {
//此方法返回的是一個數組,數組中包括request以及ActionCofig等類對象
Object[] args = point.getArgs();
System.out.println("目標參數列表:");
if (args != null) {
for (Object obj : args) {
System.out.println(obj + ",");
}
System.out.println();
}
}
//有參並有返回值的方法
public void logArgAndReturn(JoinPoint point, Object returnObj) {
//此方法返回的是一個數組,數組中包括request以及ActionCofig等類對象
Object[] args = point.getArgs();
System.out.println("目標參數列表:");
if (args != null) {
for (Object obj : args) {
System.out.println(obj + ",");
}
System.out.println();
System.out.println("執行結果是:" + returnObj);
}
}
}
5.applicationContext.java
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<bean id="logService" class="cn.com.spring.service.impl.LogServiceImpl"></bean>
<bean id="loginService" class="cn.com.spring.service.impl.LoginServiceImpl"></bean>
<aop:config>
<!-- 切入點 -->
<aop:pointcut
expression="execution(* cn.com.spring.service.impl.LoginServiceImpl.*(..))"
id="myPointcut" />
<!-- 切面: 將哪個對象中的哪個方法,織入到哪個切入點 -->
<aop:aspect id="dd" ref="logService">
<!-- 前置通知
<aop:before method="log" pointcut-ref="myPointcut" />
<aop:after method="logArg" pointcut-ref="myPointcut">
-->
<aop:after-returning method="logArgAndReturn" returning="returnObj" pointcut-ref="myPointcut"/>
</aop:aspect>
</aop:config>
</beans>
6.TestMain.java
public class TestMain {
public static void testSpringAOP(){
ApplicationContext ctx = new ClassPathXmlApplicationContext("app*.xml");
ILoginService loginService = (ILoginService)ctx.getBean("loginService");
loginService.login("zhangsan", "12344");
}
public static void main(String[] args) {
testSpringAOP();
}
}
7.輸出結果:
login:zhangsan,12344
目標參數列表:
zhangsan,
12344,
執行結果是:true
解析:1.先調用了login()方法System.out.println("login:" + userName + "," + password);
2.再調用了logArgAndReturn()方法輸出了日志,並且返回了login()方法是否成功
System.out.println("目標參數列表:");
if (args != null) {
for (Object obj : args) {
System.out.println(obj + ",");
}
System.out.println();
System.out.println("執行結果是:" + returnObj);
}
權限控制
首先定義一個用戶:
Java代碼 收藏代碼
public class User {
private String username;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
用戶有三種人:未注冊用戶,注冊用戶,與管理員
注冊用戶可以可以發表,回復帖子
管理員除了可以發表,回復帖子,還可以刪除帖子!
下面定義TestCommunity接口:
Java代碼 收藏代碼
public interface TestCommunity {
public void answerTopic();
public void deleteTopic();
}
實現上面接口的TestCommunityImpl類:
Java代碼 收藏代碼
public class TestCommunityImpl implements TestCommunity {
//注冊用戶與管理員擁有的功能
public void answerTopic() {
System.out.println("可以發表,回復帖子");
}
//管理員擁有的功能
public void deleteTopic() {
System.out.println("可以刪除帖子!");
}
}
下一步,建立一下依賴注入的實現類TestResultImpl:
Java代碼 收藏代碼
public class TestResultImpl {
private TestCommunity test;
public void setTest(TestCommunity test) {
this.test = test;
}
public void answerTopic()
{
test.answerTopic();
}
public void deleteTopic()
{
test.deleteTopic();
}
}
接下來,就是最重要的一個類,攔截器,Around處理類型的,類TestAuthorityInterceptor:
Java代碼 收藏代碼
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
//創建Around處理應該實現MethodInterceptor接口
public class TestAuthorityInterceptor implements MethodInterceptor {
private User user;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
// invoke方法返回調用的結果
public Object invoke(MethodInvocation invocation) throws Throwable {
String methodName = invocation.getMethod().getName();
if (user.getUsername().equals("unRegistedUser")) {
System.out.println("你的身份是未注冊用戶,沒有權限回復,刪除帖子!");
return null;
}
if ((user.getUsername().equals("user"))
&& (methodName.equals("deleteTopic"))) {
System.out.println("你的身份是注冊用戶,沒有權限刪除帖子");
return null;
}
// proceed()方法對連接點的整個攔截器鏈起作用,攔截器鏈中的每個攔截器都執行該方法,並返回它的返回值
return invocation.proceed();
}
}
配置文件:
Java代碼 收藏代碼
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="authTarget" class="org.test.lighter.TestCommunityImpl" />
<!-- 其中的username可以寫為admin,user,和unRegistedUser -->
<bean id="user" class="org.test.lighter.User">
<property name="username" value="user" />
</bean>
<!-- 配置攔截器 -->
<bean id="TestAuthorityInterceptor"
class="org.test.lighter.TestAuthorityInterceptor">
<property name="user" ref="user" />
</bean>
<!-- 配置代理工廠bean -->
<bean id="service"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>org.test.lighter.TestCommunity</value>
</property>
<property name="target" ref="authTarget"/>
<property name="interceptorNames">
<list>
<value>TestAuthorityInterceptor</value>
</list>
</property>
</bean>
<bean id="testResult" class="org.test.lighter.TestResultImpl">
<property name="test" ref="service" />
</bean>
</beans>
再寫一個執行文件BeanTest:
Java代碼 收藏代碼
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
public class BeanTest {
public static void main(String[] args) throws Exception
{
ApplicationContext ctx = new FileSystemXmlApplicationContext("src/bean.xml");
TestResultImpl test = (TestResultImpl)ctx.getBean("testResult");
test.answerTopic();
test.deleteTopic();
}
}
執行結果:大家猜一下啦
Java代碼 收藏代碼
1、如果是管理員,打印出:
可以發表,回復帖子
可以刪除帖子!
2、如果是注冊用戶:
可以發表,回復帖子
你的身份是注冊用戶,沒有權限刪除帖子
3、未注冊用戶:
你的身份是未注冊用戶,沒有權限回復,刪除帖子!
Spring為我們提供了,根據beanName匹配后進行自動代理的解決方法
1、范例
日常業務中存在的問題
使用大量的try/catch來捕獲異常
導致整個控制層代碼可讀性極差,並且此類工作重復枯燥、容易復制錯。
一份糟糕的控制器代碼如下:@RequestMapping("test/run/old")
public JsonResponse testRunOld() {
try {
exampleService.runTest();
System.out.println("正常運行");
return JsonResponse.newOk();
}catch (DataNotCompleteException e) {
logger.error("something error occured!");
return JsonResponse.newError(ErrorMsgEnum.DATA_NO_COMPLETE);
} catch (Exception e) {
return JsonResponse.newError();
}
}
我們要把代碼變成這樣:
@Controller
public class TestController {
@Autowired
private IExampleService exampleService;
@RequestMapping("test/run/aop")
public JsonResponse testRunAop() throws Exception {
exampleService.runTest();
System.out.println("正常運行");
return JsonResponse.newOk();
}
}
@Service
public class ExampleService implements IExampleService{
@Override
public void runTest() throws Exception {
// do something
System.out.println("run something");
throw new CustomException(ErrorMsgEnum.DATA_NO_COMPLETE);
}
}
這樣做以后,代碼里少了很多try和catch,這些到處復制的代碼本來就應該統一起來,只是在aop以前沒有什么更好的處理方式,只能復制。
其次,service拋出異常后,不用再去controller里加一段catch,這種操作每次都要浪費5-15秒(如果你不熟悉IDE中的快捷鍵,這就是噩夢)
現在你的異常只要往上拋出去就不管了(throws Exception),可以專心寫業務代碼
如何完成?其實原理相當簡單。
把那些煩人的try丟到AOP中處理
我們將采用Spring AOP統一處理異常,統一返回后端接口的結果。
使用一個自定義異常和一個錯誤前端提示枚舉來逐層傳遞消息
一個錯誤枚舉來代替新建異常信息類,減少業務異常信息文件的數量
幾個核心類代碼
ErrorMsgEnum 錯誤枚舉public enum ErrorMsgEnum {
//正常返回的枚舉
SUCCESS(true, 2000,"正常返回", "操作成功"),
// 系統錯誤,50開頭
SYS_ERROR(false, 5000, "系統錯誤", "親,系統出錯了哦~"),
PARAM_INVILAD(false, 5001, "參數出現異常", "參數出現異常"),
DATA_NO_COMPLETE(false, 5002, "數據填寫不完整,請檢查", "數據填寫不完整,請檢查");
private ErrorMsgEnum(boolean ok, int code, String msg ,String userMsg) {
this.ok = ok;
this.code = code;
this.msg = msg;
this.userMsg = userMsg;
}
private boolean ok;
private int code;
private String msg;
private String userMsg;
}
控制層返回結果POJO類
public class JsonResponse{
String msg;
Object data;
public JsonResponse() {
msg = "";
data = null;
}
public static JsonResponse newOk() {
JsonResponse response = new JsonResponse();
response.setState(State.newOk());
return response;
}
public static JsonResponse newOk(Object data) {
JsonResponse response = new JsonResponse();
response.setData(data);
response.setState(State.newOk());
return response;
}
public static JsonResponse newError() {
JsonResponse response = new JsonResponse();
response.setMsg("無情的系統異常!");
return response;
}
public static JsonResponse newError(ErrorMsgEnum errorMsgEnum) {
JsonResponse response = new JsonResponse();
state.setMsg(errorMsgEnum.getErrorMsg());
return response;
}
}
自定義異常類
public class CustomException extends Exception {
private ErrorMsgEnum errorMsgEnum;
public CustomException(ErrorMsgEnum errorMsgEnum) {
this.errorMsgEnum = errorMsgEnum;
}
}
AOP捕獲異常處理類
@Around("execution(public * com.jason.*.controller..*.*(..))")
public JsonResponse serviceAOP(ProceedingJoinPoint pjp) throws Exception {
JsonResponse newResultVo = null;
try {
return (JsonResponse) pjp.proceed();
} catch (CustomException e) {
logger.info("自定義業務異常:" + e.getMessage());
ErrorMsgEnum errorMsgEnum = e.getErrorMsgEnum();
if (Objects.nonNull(errorMsgEnum)) {
newResultVo = JsonResponse.newError(errorMsgEnum);
} else {
newResultVo = JsonResponse.newError(e.getMessage());
}
} catch (Exception e) {
//可以順便處理你的日志,此處能取到方法名,參數等等
logger.error("出現運行時異常:", e);
newResultVo = JsonResponse.newError();
}
return newResultVo;
}
Test && End
至此,我們已經可以直接在 Service 或 Controller 中隨意拋出一個異常,
直接每個控制器方法拋出的異常定義為 throws Exception 即可
經過這次處理:
最大的好處是:沒有try
異常處理和返回結果得到統一,不怕你的隊友復制錯了。
2、范例
利用spring aop統一處理異常和打日志
spring aop的概念,很早就寫博客介紹了,現在在工作中真正使用。
我們很容易寫出的代碼
我們很容易寫出帶有很多try catch 和 logger.warn(),logger.error()的代碼,這樣一個方法本來的業務邏輯只有5行,有了這些,代碼就變成了10行或者更多行,如:
public ResultDTO<UserDTO> queryUserByCardId(String cardId) {
ResultDTO<UserDTO> result = new ResultDTO<UserDTO>();
StringBuilder log = new StringBuilder();
log.append("queryUserByCardId:" + cardId);
try {
checkCardIdNotNull(cardId);
StationUserDO userDO = userDAO.queryUserByCardId(cardId);
UserDTO stationUserDTO = DataTypeConvertUtils.DOToDTO(userDO);
result.setData(stationUserDTO);
logger.warn(log.append(" result:").toString() + result);
} catch (StationErrorCodeException e) {
//logger.error(log.append("catch StationErrorCodeException!").toString(), e);
result.setSuccess(false);
result.setErrorCode(e.getErrorCode().getErrorCode());
result.setErrorMessage(e.getErrorCode().getErrorMessage());
} catch (Exception e) {
logger.error(log.append("catch Exception!").toString(), e);
result.setSuccess(false);
result.setErrorCode(StationErrorCodeConstants.STA10001.getErrorCode());
result.setErrorMessage(StationErrorCodeConstants.STA10001.getErrorMessage());
}
return result;
}1234567891011121314151617181920212223
實際上,我們的業務邏輯就幾行而已,中間卻夾雜着那么多的異常處理代碼及日志信息代碼。
如何改進代碼
我們可以使用springaop,做一個切面,這個切面專門做記錄日志和異常處理的工作,這樣就能減少重復代碼。
代碼如下:
@Override
public ResultDTO<StationUserDTO>queryUserByCardId(String cardId) {
ResultDTO<StationUserDTO> result = new ResultDTO<StationUserDTO>();
checkCardIdNotNull(cardId);
StationUserDO userDO = stationUserDAO.queryStationUserByCardId(cardId);
StationUserDTO stationUserDTO = DataTypeConvertUtils.DOToDTO(userDO);
result.setData(stationUserDTO);
return result;
}
12345678910
我們在切面中做異常處理和記錄日志:
@Aspect
public class CardServiceAspect {
private final Logger logger = LoggerFactory.getLogger("card");
// 切入點表達式按需配置
@Pointcut("execution(* *.*(..)))")
private void myPointcut() {
}
@Before("execution(* *.*(..)))")
public void before(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
logger.warn(className + "的" + methodName + "執行了");
Object[] args = joinPoint.getArgs();
StringBuilder log = new StringBuilder("入參為");
for (Object arg : args) {
log.append(arg + " ");
}
logger.warn(log.toString());
}
@AfterReturning(value = "execution(* *.*(..)))", returning = "returnVal")
public void afterReturin(Object returnVal) {
logger.warn("方法正常結束了,方法的返回值:" + returnVal);
}
@AfterThrowing(value = "StationCardServiceAspect.myPointcut()", throwing = "e")
public void afterThrowing(Throwable e) {
if (e instanceof StationErrorCodeException) {
logger.error("通知中發現異常StationErrorCodeException", e);
} else {
logger.error("通知中發現未知異常", e);
}
}
@Around(value = "StationCardServiceAspect.myPointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
logger.warn("前置增強...");
Object result = null;
try {
result = proceedingJoinPoint.proceed();
} catch (Exception e) {
ResultDTO resultDTO = new ResultDTO();
if (e instanceof StationErrorCodeException) {
StationErrorCodeException errorCodeException = (StationErrorCodeException) e;
resultDTO.setSuccess(false);
resultDTO.setErrorCode(errorCodeException.getErrorCode().getErrorCode());
resultDTO.setErrorMessage(errorCodeException.getErrorCode().getErrorMessage());
} else {
resultDTO.setSuccess(false);
resultDTO.setErrorCode(StationErrorCodeConstants.STA10001.getErrorCode());
resultDTO.setErrorMessage(StationErrorCodeConstants.STA10001.getErrorMessage());
}
return resultDTO;
}
return result;
}
}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
然后我們在spring配置文件中配置切面
<!-- 配置切面的類 -->
<bean id="serviceAspect" class="com.lirui.StationCardServiceAspect"/>
<!-- 配置成注解方式尋找要被代理的對象 -->
<aop:aspectj-autoproxy/>1234
這樣,我們就可以統一處理異常和日志了。
不足點
利用這種方式,只能打入參和出參,還有拋出異常時打異常日志,不能打方法運行中的中間值,目前我只能想到,方法中間值的日志,就是用原來的方式打出,不知道大家有沒有什么好的方法。
spring aop的其他使用
推薦使用aspectJ來完成面向切面編程。我們還可以利用aop完成其他功能如記錄程序運行時間等
3、范例
AOP在SpringBoot中的使用
使用切面管理異常的原因:
今天的內容干貨滿滿哦~並且是我自己在平時工作中的一些問題與解決途徑,對實際開發的作用很大,好,閑言少敘,讓我們開始吧~~
我們先看一張錯誤信息在APP中的展示圖:
是不是體驗很差,整個后台錯誤信息都在APP上打印了。
作為后台開發人員,我們總是在不停的寫各種接口提供給前端調用,然而不可避免的,當后台出現BUG時,前端總是丑陋的講錯誤信息直接暴露給用戶,這樣的用戶體驗想必是相當差的(不過后台開發一看就知道問題出現在哪里)。同時,在解決BUG時,我們總是要問前端拿到參數去調適,排除各種問題(網絡,Json體錯誤,接口名寫錯……BaLa……BaLa……BaLa)。在不考慮前端容錯的情況下。我們自己后台有沒有優雅的解決這個問題的方法呢,今天這篇我們就來使用AOP統一對異常進行記錄以及返回。
SpringBoot引入AOP
在SpringBoot中引入AOP是一件很方便的事,和其他引入依賴一樣,我們只需要在POM中引入starter就可以了:
<!--spring切面aop依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
返回體報文定義
接下來我們先想一下,一般我們返回體是什么樣子的呢?或者你覺得一個返回的報文應該具有哪些特征。
- 成功標示:可以用boolean型作為標示位。
- 錯誤代碼:一般用整型作為標示位,羅列的越詳細,前端的容錯也就能做的更細致。
- 錯誤信息:使用String作為錯誤信息的描述,留給前端是否展示給用戶或者進入其他錯誤流程的使用。
- 結果集:在無錯誤信息的情況下所得到的正確數據信息。一般是個Map,前端根據Key取值。
以上是對一個返回體報文一個粗略的定義了,如果再細致點,可以使用簽名進行驗簽功能活着對明文數據進行對稱加密等等。這些我們今天先不討論,我們先完成一個能夠使用的接口信息定義。
我們再對以上提到這些信息做一個完善,去除冗余的字段,對差不多的類型進行合並於封裝。這樣的想法下,我們創建一個返回體報文的實體類。
public class Result<T> {
// error_code 狀態值:0 極為成功,其他數值代表失敗
private Integer status;
// error_msg 錯誤信息,若status為0時,為success
private String msg;
// content 返回體報文的出參,使用泛型兼容不同的類型
private T data;
public Integer getStatus() {
return status;
}
public void setStatus(Integer code) {
this.status = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData(Object object) {
return data;
}
public void setData(T data) {
this.data = data;
}
public T getData() {
return data;
}
@Override
public String toString() {
return "Result{" +
"status=" + status +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
現在我們已經有一個返回體報文的定義了,那接下來我們可以來創建一個枚舉類,來記錄一些我們已知的錯誤信息,可以在代碼中直接使用。
public enum ExceptionEnum {
UNKNOW_ERROR(-1,"未知錯誤"),
USER_NOT_FIND(-101,"用戶不存在"),
;
private Integer code;
private String msg;
ExceptionEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
我們在這里把對於不再預期內的錯誤統一設置為-1,未知錯誤。以避免返回給前端大段大段的錯誤信息。
接下來我們只需要創建一個工具類在代碼中使用:
public class ResultUtil {
/**
* 返回成功,傳入返回體具體出參
* @param object
* @return
*/
public static Result success(Object object){
Result result = new Result();
result.setStatus(0);
result.setMsg("success");
result.setData(object);
return result;
}
/**
* 提供給部分不需要出參的接口
* @return
*/
public static Result success(){
return success(null);
}
/**
* 自定義錯誤信息
* @param code
* @param msg
* @return
*/
public static Result error(Integer code,String msg){
Result result = new Result();
result.setStatus(code);
result.setMsg(msg);
result.setData(null);
return result;
}
/**
* 返回異常信息,在已知的范圍內
* @param exceptionEnum
* @return
*/
public static Result error(ExceptionEnum exceptionEnum){
Result result = new Result();
result.setStatus(exceptionEnum.getCode());
result.setMsg(exceptionEnum.getMsg());
result.setData(null);
return result;
}
}
以上我們已經可以捕獲代碼中那些在編碼階段我們已知的錯誤了,但是卻無法捕獲程序出的未知異常信息。我們的代碼應該寫得漂亮一點,雖然很多時候我們會說時間太緊了,等之后我再來好好優化。可事實是,我們再也不會回來看這些代碼了。項目總是一個接着一個,時間總是不夠用的。如果真的需要你完善重構原來的代碼,那你一定會非常痛苦,死得相當難看。所以,在第一次構建時,就將你的代碼寫完善了。
一般系統拋出的錯誤是不含錯誤代碼的,除去部分的404,400,500錯誤之外,我們如果想把錯誤代碼定義的更細致,就需要自己繼承RuntimeException這個類后重新定義一個構造方法來定義我們自己的錯誤信息:
public class DescribeException extends RuntimeException{
private Integer code;
/**
* 繼承exception,加入錯誤狀態值
* @param exceptionEnum
*/
public DescribeException(ExceptionEnum exceptionEnum) {
super(exceptionEnum.getMsg());
this.code = exceptionEnum.getCode();
}
/**
* 自定義錯誤信息
* @param message
* @param code
*/
public DescribeException(String message, Integer code) {
super(message);
this.code = code;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
同時,我們使用一個Handle來把Try,Catch中捕獲的錯誤進行判定,是一個我們已知的錯誤信息,還是一個未知的錯誤信息,如果是未知的錯誤信息,那我們就用log記錄它,便於之后的查找和解決:
@ControllerAdvice
public class ExceptionHandle {
private final static Logger LOGGER = LoggerFactory.getLogger(ExceptionHandle.class);
/**
* 判斷錯誤是否是已定義的已知錯誤,不是則由未知錯誤代替,同時記錄在log中
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Result exceptionGet(Exception e){
if(e instanceof DescribeException){
DescribeException MyException = (DescribeException) e;
return ResultUtil.error(MyException.getCode(),MyException.getMessage());
}
LOGGER.error("【系統異常】{}",e);
return ResultUtil.error(ExceptionEnum.UNKNOW_ERROR);
}
}
這里我們使用了 @ControllerAdvice ,使Spring能加載該類,同時我們將所有捕獲的異常統一返回結果Result這個實體。
此時,我們已經完成了對結果以及異常的統一返回管理,並且在出現異常時,我們可以不返回錯誤信息給前端,而是用未知錯誤進行代替,只有查看log我們才會知道真實的錯誤信息。
可能有小伙伴要問了,說了這么久,並沒有使用到AOP啊。不要着急,我們繼續完成我們剩余的工作。
我們使用接口若出現了異常,很難知道是誰調用接口,是前端還是后端出現的問題導致異常的出現,那這時,AOP久發揮作用了,我們之前已經引入了AOP的依賴,現在我們編寫一個切面類,切點如何配置不需要我多說了吧:
@Aspect
@Component
public class HttpAspect {
private final static Logger LOGGER = LoggerFactory.getLogger(HttpAspect.class);
@Autowired
private ExceptionHandle exceptionHandle;
@Pointcut("execution(public * com.zzp.controller.*.*(..))")
public void log(){
}
@Before("log()")
public void doBefore(JoinPoint joinPoint){
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//url
LOGGER.info("url={}",request.getRequestURL());
//method
LOGGER.info("method={}",request.getMethod());
//ip
LOGGER.info("id={}",request.getRemoteAddr());
//class_method
LOGGER.info("class_method={}",joinPoint.getSignature().getDeclaringTypeName() + "," + joinPoint.getSignature().getName());
//args[]
LOGGER.info("args={}",joinPoint.getArgs());
}
@Around("log()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Result result = null;
try {
} catch (Exception e) {
return exceptionHandle.exceptionGet(e);
}
if(result == null){
return proceedingJoinPoint.proceed();
}else {
return result;
}
}
@AfterReturning(pointcut = "log()",returning = "object")//打印輸出結果
public void doAfterReturing(Object object){
LOGGER.info("response={}",object.toString());
}
}
我們使用@Aspect來聲明這是一個切面,使用@Pointcut來定義切面所需要切入的位置,這里我們是對每一個HTTP請求都需要切入,在進入方法之前我們使用@Before記錄了調用的接口URL,調用的方法,調用方的IP地址以及輸入的參數等。在整個接口代碼運作期間,我們使用@Around來捕獲異常信息,並用之前定義好的Result進行異常的返回,最后我們使用@AfterReturning來記錄我們的出參。
以上全部,我們就完成了異常的統一管理以及切面獲取接口信息,接下來我們心新寫一個ResultController來測試一下:
@RestController
@RequestMapping("/result")
public class ResultController {
@Autowired
private ExceptionHandle exceptionHandle;
/**
* 返回體測試
* @param name
* @param pwd
* @return
*/
@RequestMapping(value = "/getResult",method = RequestMethod.POST)
public Result getResult(@RequestParam("name") String name, @RequestParam("pwd") String pwd){
Result result = ResultUtil.success();
try {
if (name.equals("zzp")){
result = ResultUtil.success(new UserInfo());
}else if (name.equals("pzz")){
result = ResultUtil.error(ExceptionEnum.USER_NOT_FIND);
}else{
int i = 1/0;
}
}catch (Exception e){
result = exceptionHandle.exceptionGet(e);
}
return result;
}
}
在上面我們設計了一個controller,如果傳入的name是zzp的話,我們就返回一個用戶實體類,如果傳入的是pzz的話,我們返回一個沒有該用戶的錯誤,其他的,我們讓他拋出一個by zero的異常。
我們用POSTMAN進行下測試:
我們可以看到,前端收到的返回體報文已經按我們要求同意了格式,並且在控制台中我們打印出了調用該接口的一些接口信息,我們繼續測試另外兩個會出現錯誤情況的請求:
我們可以看到,如是我們之前在代碼中定義完成的錯誤信息,我們可以直接返回錯誤碼以及錯誤信息,如果是程序出現了我們在編碼階段不曾預想到的錯誤,則統一返回未知錯誤,並在log中記錄真實錯誤信息。
以上就是我們統一管理結果集以及使用切面來記錄接口調用的一些真實情況,在平時的使用中,大家要清楚切點的優先級以及在不同的切點位置該使用哪些注解來幫助我們完成開發,並且在切面中,如果遇到同步問題該如何解決等等。希望這篇文章能讓你更好的思考如何設計好接口,我們在實際開發中又是怎樣一步步完善我們的功能與代碼的。也希望大家能好好梳理這些內容,如果有疑惑的地方,還請留言,我如果看到,一定會解答的。這里預告下:下周,我們將使用ORM框架來做數據庫的交互~~~