aop 日志统一处理
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框架来做数据库的交互~~~