SpringBoot学习三:异常处理和记录日志

相关文章

SpringBoot 学习一

SpringBoot 学习二:操作数据库

前言

在上两篇文章中,学习了 SpringBoot 的一个初步使用和通过 SpringBoot 来操作数据库的相关知识,接下来就学习下如何使用 SpringBoot 来进行异常的处理和记录日志的功能。

异常处理

在处理业务的时候,有时候需要捕获一些特定的异常,进行相应的处理,如跳转到一个特定的错误页面之类的,在 SpringBoot 中,处理异常一般使用 @ControllerAdvice 和 @ExceptionHandler 处理进行处理,接下来,先看看这两个注解:

@ControllerAdvice

首先来看下该注解的一个定义:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {}

从上述定义中可以看到,该注解的 Target 为 ElementType.TYPE,也就是说,该注解只能应用于 类, 接口和枚举上;该注解还被 @Component 修改,表示被 @ControllerAdvice 修改的类也可以被注入到 Spring 容器中去,被 Spring 进行管理。

此外,被 @ControllerAdvice 修饰的类,也是一个 Controller 层;被 @ControllerAdvice 修饰的类的内部用 @ExceptionHandler、@InitBinder、@ModelAttribute 注解修改的方法,只能应用于被 @RequestMapping修饰的方法(靠,怎么感觉有点绕啊),看下 javadoc的描述吧:

<p>It is typically used to define {@link ExceptionHandler @ExceptionHandler},
* {@link InitBinder @InitBinder}, and {@link ModelAttribute @ModelAttribute}
* methods that apply to all {@link RequestMapping @RequestMapping} methods.

@ExceptionHandler

先看下定义:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {}

该注解只能用于在方法上,也就是当特定异常发生的时候,会执行相应的方法。

下面就用这两个注解来进行一个异常的处理:

首先,先自定义一个异常:

/**
 * 自定义异常
 */
public class MyException extends Exception {

    public MyException(String message) {
        super(message);
    }
}

接下来,就使用  @ControllerAdvice 和 @ExceptionHandler 编写异常的处理类:

/**
 * 异常处理, 当出现异常的时候,会执行相应的方法
 */
@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    public void defaultExceptionHandler(Exception e){
        System.out.println(String.format("开始异常处理: %s", e.getMessage()));
    }

    @ExceptionHandler(value = MyException.class)
    public void myExceptionHandler(MyException e){
        System.out.println("出现自定义异常了 : " + e.getMessage());
    }
}

当程序产生异常的时候,会进入相应的 @ExceptionHandler  方法,但是被 @ExceptionHandler  修饰的方法只能捕获到被 @RequestMapping 修饰方法中方法的异常,如果在 service 层发生了异常,则 @ExceptionHandler  修饰的方法是不能捕获到的;

此外, @ControllerAdvice 注解还有两个属性 value 和 basePackages,它们都是一个意思,都是指定捕获对应包下的异常,默认的是捕获类路径(classpath)下的异常:

测试一波:


/**
 * 测试
 */
@RestController
public class ControllerTest {

    @RequestMapping("/testException/{i}")
    public String testException(@PathVariable("i") int i) throws Exception{
        int result = 100 / i;
        return String.format("100 除以 %s = %s", i, result);
    }

    @RequestMapping("/testMyException")
    public void testMyException() throws MyException{
        throw new MyException("手动抛出自定义异常...");
    }
}

之后,启动应用程序:

在浏览器输入 http://localhost:8080/testException/0, 在后台会打印:

开始异常处理: / by zero

在浏览器输入 http://localhost:8080/testMyException, 在后台会打印:

出现自定义异常了 : 手动抛出自定义异常...
可以看到,当在被  @RequestMapping 修饰的方法发生异常的时候,会进入对应的 @ExceptionHandler 方法。

 

接下来测试下,在 service  层发生异常的时候,异常处理器会不会也捕获的到:

先模拟一个 service  层,抛出异常:

@Service
public class ServiceTest {

    public void add(String string) throws MyException {
        System.out.println(string);
        throw new MyException("Service 层抛出的自定义异常...");
    }

    public void delete(String id) throws Exception{
        System.out.println(Integer.parseInt(id));
    }
}

Junit测试:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = MyspringbootApplication.class)
public class TestException {

    @Autowired
    private ServiceTest serviceTest;

    @Test
    public void test() throws MyException {
        serviceTest.add("tsmyk");
    }

    @Test
    public void test2() throws Exception {
        serviceTest.delete("a");
    }
}

在控制台没有打印上述信息,也就是异常处理器没有捕获到异常,这也证明了只能应用于被 @RequestMapping 修饰的方法。

 

下面来测试一下,只捕获对应包下的异常,通过 value 和 basePackages 属性来控制:

先定义两个异常处理器,分别捕获 ex1 和 ex2 包下的异常:

@ControllerAdvice(basePackages = "myspringboot.myspringboot.exception.ex1")
public class MyExceptionHandler2 {

    @ExceptionHandler(value = Exception.class)
    public void defaultExceptionHandler(Exception e){
        System.out.println(String.format("处理ex1包下的异常: %s", e.getMessage()));
    }

    @ExceptionHandler(value = MyException.class)
    public void myExceptionHandler(MyException e){
        System.out.println("处理ex1包下的异常 : " + e.getMessage());
    }
}
@ControllerAdvice(basePackages = "myspringboot.myspringboot.exception.ex2")
public class MyExceptionHandler3 {

    @ExceptionHandler(value = Exception.class)
    public void defaultExceptionHandler(Exception e){
        System.out.println(String.format("处理ex2包下的异常: %s", e.getMessage()));
    }

    @ExceptionHandler(value = MyException.class)
    public void myExceptionHandler(MyException e){
        System.out.println("处理ex2包下的异常 : " + e.getMessage());
    }
}

测试一下:

package myspringboot.myspringboot.exception.ex1;

/**
 * 测试
 */
@RestController
@RequestMapping("/ex1")
public class ControllerTest2 {

    @RequestMapping("/testException/{i}")
    public String testException(@PathVariable("i") int i) throws Exception{
        int result = 100 / i;
        return String.format("100 除以 %s = %s", i, result);
    }

    @RequestMapping("/testMyException")
    public void testMyException() throws MyException{
        throw new MyException("手动抛出自定义异常...");
    }
}

之后,启动应用程序:

在浏览器输入 http://localhost:8080/ex1/testException/0, 在后台会打印:

处理ex1包下的异常: / by zero

在浏览器输入 http://localhost:8080/ex1/testMyException, 在后台会打印:

处理ex1包下的异常 : 手动抛出自定义异常...

可以看到只有 MyExceptionHandler2 能捕获到异常,而 MyExceptionHandler3 并没有。

记录日志

在程序中有时候需要记录下用户的操作日志,如谁增加了用户,谁删除了用户之类的,就需要记录操作日志,可以使用 Spring 的 AOP 技术来统一记录,下面来看看 SpringBoot 是如何使用的。

首先需要定义一个注解,需要记录日志的方法添加该注解即可:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 标注该该注解的方法需要记录操作日志
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Logger {
    String value() default "";
}

之后,定义切面,即解析我们定义的注解:

import myspringboot.myspringboot.log.annotation.Logger;
import myspringboot.myspringboot.log.dao.SysLogDao;
import myspringboot.myspringboot.log.pojo.SysLog;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Date;

/**
 * 日志切面
 */
@Aspect
@Component
public class LoggerAspect {

    @Autowired
    private SysLogDao sysLogDao;

    @Pointcut("@annotation(myspringboot.myspringboot.log.annotation.Logger)")
    public void pointcut(){}

    @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint){
        try {
            joinPoint.proceed(); // 执行方法
        }catch (Throwable e){
        }
        saveSysLog(joinPoint);
    }

    private void saveSysLog(ProceedingJoinPoint joinPoint){
        SysLog sysLog = getSysLog(joinPoint);
        sysLogDao.saveSysLog(sysLog);
    }

    private SysLog getSysLog(ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 方法
        Method method = methodSignature.getMethod();
        // 方法上的注解
        Logger loggerAnnotation = method.getAnnotation(Logger.class);

        SysLog sysLog = new SysLog();

        if (loggerAnnotation != null){
            // 注解上的描述
            sysLog.setOperation(loggerAnnotation.value());
        }
        // 方法名
        String className = joinPoint.getTarget().getClass().getName();
        String methonName = methodSignature.getName();
        sysLog.setMethod(className + "." + methonName + "()");

        // 方法参数值
        Object[] args = joinPoint.getArgs();
        // 方法参数名
        LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
        String[] paramNames = discoverer.getParameterNames(method);
        if (args != null && paramNames != null){
            String params = "";
            for (int i = 0; i < args.length; i++){
                params += " " + paramNames[i] + ": " + args[i];
            }
            sysLog.setParams(params);
        }
        sysLog.setUsername("admin");
        sysLog.setLogDate(new Date());
        return sysLog;
    }
}

定义dao层的入库方法:

import myspringboot.myspringboot.log.pojo.SysLog;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

@Mapper
@Repository
public interface SysLogDao {

    void saveSysLog(SysLog sysLog);
}

模拟service层:

@Service
public class MyServiceImpl {

    @Logger("添加用户")
    public void add(String name){
        System.out.println("添加用户:" + name);
    }

    @Logger("删除用户")
    public void delete(String name, String job){
        System.out.println("删除用户:" + name + " , " + job);
    }

    @Logger("修改用户")
    public void update(int id){
        System.out.println("修改用户:" + id);
    }

    @Logger("查询用户")
    public void query(String name){
        System.out.println("查询用户:" + name);
    }
}

pojo 类:

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.io.Serializable;
import java.util.Date;

/**
 * 系统日志类
 */
@Getter
@Setter
@ToString
public class SysLog implements Serializable {

    private int id;

    private String username;

    private String operation;

    private String method;

    private String params;

    private Date logDate;
}

单元测试:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = MyspringbootApplication.class)
public class LogTest {

    @Autowired
    private MyServiceImpl service;

    @Test
    public void test(){
        service.add("tsmyk0715");
        service.delete("tsmyk0715", "java");
        service.update(100);
        service.query("tsmyk");
    }
}

结果:

6a2fbf7d18c815804ea6f9b16604b6a1aac.jpg

以上就是 SpringBoot 来记录操作日志,即通过 AOP 技术来实现的。

转载于:https://my.oschina.net/mengyuankan/blog/2222140

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值