相关文章
前言
在上两篇文章中,学习了 SpringBoot 的一个初步使用和通过 SpringBoot 来操作数据库的相关知识,接下来就学习下如何使用 SpringBoot 来进行异常的处理和记录日志的功能。
异常处理
在处理业务的时候,有时候需要捕获一些特定的异常,进行相应的处理,如跳转到一个特定的错误页面之类的,在 SpringBoot 中,处理异常一般使用 @ControllerAdvice 和 @ExceptionHandler 处理进行处理,接下来,先看看这两个注解:
@ControllerAdvice
首先来看下该注解的一个定义:
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Componentpublic @interface ControllerAdvice {}
从上述定义中可以看到,该注解的 Target 为 ElementType.TYPE,也就是说,该注解只能应用于 类, 接口和枚举上;该注解还被 修改,表示被 @ControllerAdvice 修改的类也可以被注入到 Spring 容器中去,被 Spring 进行管理。
此外,被 @ControllerAdvice 修饰的类,也是一个 Controller 层;被 @ControllerAdvice 修饰的类的内部用 @ExceptionHandler、@InitBinder、@ModelAttribute 注解修改的方法,只能应用于被 @RequestMapping修饰的方法(靠,怎么感觉有点绕啊),看下 javadoc的描述吧:
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)@Documentedpublic @interface ExceptionHandler {}
该注解只能用于在方法上,也就是当特定异常发生的时候,会执行相应的方法。
下面就用这两个注解来进行一个异常的处理:
首先,先自定义一个异常:
/** * 自定义异常 */public class MyException extends Exception { public MyException(String message) { super(message); }}
接下来,就使用 @ControllerAdvice 和 @ExceptionHandler 编写异常的处理类:
/** * 异常处理, 当出现异常的时候,会执行相应的方法 */@ControllerAdvicepublic 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)下的异常:
测试一波:
/** * 测试 */@RestControllerpublic 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 层,抛出异常:
@Servicepublic 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@Componentpublic 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@Repositorypublic interface SysLogDao { void saveSysLog(SysLog sysLog);}
模拟service层:
@Servicepublic 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@ToStringpublic 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"); }}
结果:
以上就是 SpringBoot 来记录操作日志,即通过 AOP 技术来实现的。