简介
一个SpringBoot的社区项目。
@ControllerAdvice
实现异常处理,AOP实现日志记录。
异常处理
当用户请求的页面不存在时,自动跳转到src/main/resources/templates/error/404.html
页面。
当服务器端错误时,自动跳转到src/main/resources/templates/error/500.html
页面。
服务端出现错误时,只跳转到500.html
页面是不行的,还应该进行将异常信息记录到日志中,如果是异步请求中出现错误,也不应该返回500.html
页面,而是应该返回一个json格式的异常提示。
在三层架构(controller、service、dao)下,service层和dao层的异常最终都会上抛给controller层,所以我们只用处理在controller层出现的异常。
使用@ControllerAdvice
注解修饰一个类,使该类成为controller的全局配置类,在这个类中用@ExceptionHandler
修饰一个方法,使该方法在controller层中出现异常后被调用,用于处理捕获到的异常。
@ControllerAdvice
提供了多种指定Advice规则的定义方式,默认什么都不写,则是Advice所有Controller。写成
@ControllerAdvice("org.my.pkg")
或者@ControllerAdvice(basePackages="org.my.pkg")
,则匹配org.my.pkg包及其子包下的所有Controller,当然也可以用数组的形式指定,如:@ControllerAdvice(basePackages={"org.my.pkg", "org.my.other.pkg"})
。也可以通过指定注解来匹配,比如我自定了一个
@CustomAnnotation
注解,我想匹配所有被这个注解修饰的 Controller, 可以这么写:@ControllerAdvice(annotations={CustomAnnotation.class})
@ControllerAdvice 的介绍及三种用法Ethan.Han的博客-CSDN博客@controlleradvice
异常处理流程:
-
记日志
-
返回响应
- 浏览器是异步请求,期望返回一个json格式的异常提示
- 浏览器是普通请求,期望重定向到
500.html
页面
@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {
private static final Logger logger= LoggerFactory.getLogger(ExceptionAdvice.class);
@ExceptionHandler({Exception.class})
public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
//记日志
logger.error("服务器发生异常:"+e.getMessage());
for (StackTraceElement element:e.getStackTrace()){
logger.error(element.toString());
}
String xRequestedWith = request.getHeader("x-requested-with");
if ("XMLHttpRequest".equals(xRequestedWith)) {
//浏览器是异步的请求,希望返回的是json
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(1, "服务器异常!"));
} else {
//浏览器是普通请求,希望返回的是页面
response.sendRedirect(request.getContextPath() + "/error");//重定向到错误页面
}
}
}
通过HttpServletRequest request
获取请求头,如果请求头中包含x-requested-with
字段,并且内容为XMLHttpRequest
,表示这是一个异步请求,反之则表示这次请求是一个普通请求。
X-Requested-With请求头只存在于jquery ajax请求中,原生ajax请求并不存在该请求头
Web服务器收到客户端的http请求,会针对每一次请求,分别创建一个用于代表请求的request对象、和代表响应的response对象。 request和response对象即然代表请求和响应,那我们要获取客户机提交过来的数据,只需要找request对象就行了。要向客户机输出数据,只需要找response对象就行了。
- 请求转发的时候,url不会产生变化
- 重定向的时候,url会发生变化
@RequestMapping(path = "/error", method = RequestMethod.GET)
public String getErrorPage() {
return "/error/500";
}
日志记录
使用AOP统一记录日志
Spring基础 - Spring核心之面向切面编程(AOP) | Java 全栈知识体系 (pdai.tech)
@Component
@Aspect
public class ServiceLogAspect {
private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
//声明切点
@Pointcut("execution(* asia.daijizai.community.service.*.*(..))")
public void pointcut() {
}
//前置通知,在开始时织入
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
if (attributes == null) {
logger.info(String.format("系统内部调用,在[%s],访问了[%s].", now, target));
return;
}
HttpServletRequest request = attributes.getRequest();
String ip = request.getRemoteHost();
logger.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target));
}
}
AspectJ使用org.aspectj.lang.JoinPoint接口表示目标类连接点对象
Signature getSignature() :获取连接点的方法签名对象;
joinPoint.getSignature().getDeclaringTypeName()
获取类名
joinPoint.getSignature().getName()
获取方法名
- RequestContextHolder:持有上下文的Request容器
- 通过RequestContextHolder的静态方法可以随时随地取到当前请求的request对象
通过RequestContextHolder.getRequestAttributes()
获取RequestAttributes
类型,我们将RequestAttributes
类型强转为它的子类型ServletRequestAttributes
,这样功能多一点。
Servlet-based implementation of the
RequestAttributes
interface.
通过ServletRequestAttributes attributes
获取到HttpServletRequest request
,通过request
获取用户的ip地址。
为什么在before()
方法中获取ServletRequestAttributes attributes
后要进行判空呢?
切点在service,只要调用的了service中的方法就会执行before()
方法。
如果是controller层调用service中的方法,attributes不会为null
,而EventConsumer
消费事件时也会调用service中的方法,这时attributes就为null
了,即系统的内部调用。