异常处理与日志记录 | 社区项目

简介

一个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


异常处理流程:

  1. 记日志

  2. 返回响应

    • 浏览器是异步请求,期望返回一个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请求并不存在该请求头

X-Requested-With_mellicapen的博客-CSDN博客

Web服务器收到客户端的http请求,会针对每一次请求,分别创建一个用于代表请求的request对象、和代表响应的response对象。 request和response对象即然代表请求和响应,那我们要获取客户机提交过来的数据,只需要找request对象就行了。要向客户机输出数据,只需要找response对象就行了。

HttpServletResponse详解平庸的俗人的博客-CSDN博客httpservletresponse

  • 请求转发的时候,url不会产生变化
  • 重定向的时候,url会发生变化

重定向和转发的区别lpw666的博客-CSDN博客_重定向和转发的区别

@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() :获取连接点的方法签名对象;

spring aop JoinPoint 用法勇敢的炮灰的博客-CSDN博客aop joinpoint详解

  • joinPoint.getSignature().getDeclaringTypeName()获取类名
  • joinPoint.getSignature().getName()获取方法名

  • RequestContextHolder:持有上下文的Request容器
  • 通过RequestContextHolder的静态方法可以随时随地取到当前请求的request对象

RequestContextHolder昨天今天明天好多天的博客-CSDN博客requestcontextholder

通过RequestContextHolder.getRequestAttributes()获取RequestAttributes类型,我们将RequestAttributes类型强转为它的子类型ServletRequestAttributes,这样功能多一点。

Servlet-based implementation of the RequestAttributes interface.

ServletRequestAttributes (Spring Framework 5.3.22 API)

通过ServletRequestAttributes attributes获取到HttpServletRequest request,通过request获取用户的ip地址。


为什么在before()方法中获取ServletRequestAttributes attributes后要进行判空呢?

切点在service,只要调用的了service中的方法就会执行before()方法。

如果是controller层调用service中的方法,attributes不会为null,而EventConsumer消费事件时也会调用service中的方法,这时attributes就为null了,即系统的内部调用。

猜你喜欢

转载自juejin.im/post/7123555194410893343