相关文章
前言
在上两篇文章中,学习了 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");
}
}
结果:
以上就是 SpringBoot 来记录操作日志,即通过 AOP 技术来实现的。