一、前言
在进行项目开发的过程中,我们不可避免地都要对代码中可能存在的异常进行处理和捕获,而我们通常的做法有如下两种:
方式一:
1、对于在Service层的异常,使用try-catch进行捕获处理,其Service层捕获异常代码如下所示:
@Service
public class UserServiceImpl implements UserService {
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
@Autowired
private UserMapper userMapper;
@Override
public User selectUserById(Integer id) {
User user = null;
try {
user = userMapper.selectUserById(id);
} catch (Exception e) {
logger.error("user信息有误,需要回滚");
e.printStackTrace();
}
logger.info("代码继续往下执行");
return user;
}
}
对于如上在Service层使用try-catch对异常进行捕获并处理的代码,其存在如下缺陷:
1、由于Service层是与Dao层进行交互的,我们通常都会把事务配置在Service层,当操作数据库失败时,会抛出异常并由Spring事务管理器帮我们进行回滚,而当我们使用try-catch对异常进行捕获处理时,这时Spring事务管理器并不会帮我们进行回滚,而代码也会继续往下执行,这时我们就有可能读取到脏数据。
2、由于我们在Service层对异常进行了捕获处理,在Controller层调用该Service层的相关方法时,并不能把其相关异常信息抛给Controller层,也就无法把异常信息展示给前端页面,而当用户进行相关操作失败时,也就无法得知其操作失败的缘由。
方式二:
1、在Service层将异常抛出,并在Controller层对Service层抛出的异常使用try-catch进行捕获处理,其Controller层捕获异常代码如下所示:
@RestController
public class UserController {
@Autowired
private UserService userService;
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@GetMapping(value = "/User/{id}")
public User selectUserById(@PathVariable(value = "id") Integer id) {
User user = null;
try {
user = userService.selectUserById(id);
} catch (UserNotExistException e) {
logger.error("用户不存在");
e.printStackTrace();
} catch (Exception e) {
logger.error(e.getMessage());
e.printStackTrace();
}
return user;
}
}
对于如上在Controller层使用try-catch对异常进行捕获并处理的代码,其存在如下缺陷:
1、由于Controller层是与Service层进行交互的,这样一来,在Controller层调用Service层抛出异常的方法时,我们都需要在Controller层的方法体中,写一遍try-catch代码来捕获处理Service层抛出的异常,就会使得代码很难看而且也很难维护。
2、对于Service层抛出的不同异常,那么Controller层的方法体中需要catch多个异常分别进行处理。
这里就会有人说了,我直接在Controller层的方法上使用 throws关键字 把Service层的异常继续往上抛不就行了,还不需要使用try-catch来进行捕获处理,确实是可以,但是这样会把大量的异常信息带到前端页面,对用户来说是非常不友好的,例如如下界面显示:
二、简介
使用@ControllerAdvice/@RestControllerAdvice + @ExceptionHandler注解能够实现全局处理Controller层的异常,并能够自定义其返回的信息(前提:Controller层不使用try-catch对异常进行捕获处理)
优缺点:
优点:将 Controller 层的异常和数据校验的异常进行统一处理,减少模板代码,减少编码量,提升扩展性和可维护性。
缺点:能处理 Controller 层的异常 (未使用try-catch进行捕获) 和 @Validated 校验器注解的异常,但是对于 Interceptor(拦截器)层的异常 和 Spring 框架层的异常,就无能为力了。
三、使用
1、导入相关依赖jar包
<properties>
<spring.version>5.1.5.RELEASE</spring.version>
</properties>
<dependencies>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
<scope>provided</scope>
</dependency>
<!-- javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- slf4j的相关依赖包 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
2、编写applicationContext-spring.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd">
<!-- 配置包扫描器,扫描注解的类 -->
<context:component-scan base-package="com.exception"/>
</beans>
3、编写web.xml配置文件
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<!-- post乱码过滤器 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 前端控制器 -->
<servlet>
<servlet-name>ExceptionHandlerProject</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- contextConfigLocation不是必须的, 如果不配置contextConfigLocation, springmvc的配置文件默认在:WEB-INF/servlet的name+"-servlet.xml" -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/applicationContext-spring.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>ExceptionHandlerProject</servlet-name>
<!-- 拦截所有请求jsp除外 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
4、自定义异常信息的枚举类
package com.exception.pojo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString
@AllArgsConstructor
public enum ReturnEnum {
/**
* 请求成功返回信息
**/
SERVICE_SUCCESS("200", "success", "请求成功"),
/**
* 请求失败返回信息
**/
SERVICE_ERROR("500", "error", "请求失败"),
/**
* 用户不存在返回信息
**/
USER_NOT_EXIST("404", "user not exist", "用户不存在");
/**
* 自定义异常状态码
**/
private String code;
/**
* 异常的英文信息,便于国际化切换使用
**/
private String detailsInEnglish;
/**
* 异常的中文信息
**/
private String detailsInChinese;
}
5、自定义封装异常信息返回前端的JSON类
package com.exception.pojo;
import lombok.*;
import java.io.Serializable;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class JsonResult<T> implements Serializable {
private String code;
private String message;
private T data;
public JsonResult(ReturnEnum returnEnum, T data){
this.code = returnEnum.getCode();
this.message = returnEnum.getDetailsInChinese();
this.data = data;
}
}
6、自定义异常类
package com.exception;
import com.exception.pojo.ReturnEnum;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UserNotExistException extends Exception {
/**
* 异常信息的枚举类
**/
private ReturnEnum returnEnum;
public UserNotExistException(ReturnEnum returnEnum){
super(returnEnum.getDetailsInChinese());
this.returnEnum = returnEnum;
}
}
7、创建User实体类
package com.exception.pojo;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class User {
private String name;
private String password;
}
8、编写UserService接口 (这里我就不给出Dao层了,直接在Service层模拟调用Dao层)
package com.exception.service;
import com.exception.UserNotExistException;
import com.exception.pojo.User;
public interface UserService {
/**
* 通过id查询用户信息
*
* @param id
* @return com.exception.pojo.User
* @throws UserNotExistException
**/
User selectUserById(Integer id) throws UserNotExistException;
}
9、编写UserServiceImpl实现类
package com.exception.service.impl;
import com.exception.UserNotExistException;
import com.exception.pojo.ReturnEnum;
import com.exception.pojo.User;
import com.exception.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Override
public User selectUserById(Integer id) throws UserNotExistException {
// id等于0,表示没找到用户
if(0 == id){
throw new UserNotExistException(ReturnEnum.USER_NOT_EXIST);
}
return new User();
}
}
10、编写UserController
package com.exception.controller;
import com.exception.UserNotExistException;
import com.exception.pojo.User;
import com.exception.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping(value = "/user/{id}")
public User selectUserById(@PathVariable(value = "id") Integer id) throws UserNotExistException {
//根据id查找用户信息,并继续抛出Service层的异常
User user = userService.selectUserById(id);
return user;
}
}
11、编写全局异常处理类,其中@ExceptionHandler(value = UserNotExistException.class)注解中的value用于指定需要捕获的异常
package com.exception.handler;
import com.exception.UserNotExistException;
import com.exception.pojo.JsonResult;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 用于对UserNotExistException异常进行处理
*
* @param ex
* @return com.exception.pojo.JsonResult
* @throws
**/
@ExceptionHandler(value = UserNotExistException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public JsonResult handleUserNotExistException(UserNotExistException ex){
//封装异常信息
JsonResult jsonResult = new JsonResult(ex.getReturnEnum(),null);
return jsonResult;
}
}
12、启动项目,访问 http://localhost:8080/user/0
这时我们就会有疑惑了,为什么我对异常进行了处理,界面还是会把大量的异常信息带到前端页面呢?相信使用过Spring的开发人员都用过@RequestBody、@ResponseBody注解,可以直接将输入解析成Json、将输出解析成Json,但HTTP 请求和响应是基于文本的,意味着浏览器和服务器需要通过交换原始文本才能进行通信,而这里其实就是通过HttpMessageConverter消息转换器发挥着作用,所以我们需要配置HttpMessageConverter消息转换器。
解决方式一:使用SpringMVC默认的HttpMessageConverter消息转换器
1、在pom.xml文件中 添加 jackson-databind jar包
<!-- Jackson Json处理工具包 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
2、在applicationContext-spring.xml配置文件中 添加如下配置
2.1、在<beans>标签中添加 mvc命名空间
xmlns:mvc="http://www.springframework.org/schema/mvc"
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd
2.2、开启SpringMVC注解驱动
关于更多<mvc:annotation-driven/>的默认配置,请点击<mvc:annotation-driven/>到底帮我们做了什么
关于更多<mvc:message-converters/>的介绍,请点击<mvc:message-converters/>的简单介绍
<!--
<mvc:annotation-driven/>
等价于
<mvc:annotation-driven>
// 消息转换器
<mvc:message-converters>
// 支持返回值类型为byte[],content-type为application/octet-stream,*/*
<bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/>
// 支持的返回值类型为String,content-type为 text/plain;charset=ISO-8859-1,*/*
<bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
// 支持的返回值类型为Resource,content-type为 */*
<bean class="org.springframework.http.converter.ResourceHttpMessageConverter"/>
// 支持的返回值类型为DomSource,SAXSource,Source,StreamSource,content-type为application/xml,text/xml,application/*+xml
<bean class="org.springframework.http.converter.SourceHttpMessageConverter"/>
// 支持的返回值类型为MultiValueMap,content-type为application/x-www-form-urlencoded,multipart/form-data
<bean class="org.springframework.http.converter.AllEncompassingFormHttpMessageConverter"/>
// Json消息转换器,如果发现jackson相关jar包,则会自动Json消息转换器
//注意: Spring3.x 的Json消息转换器为 MappingJacksonHttpMessageConverter
Spring4.x 的Json消息转换器为 MappingJackson2HttpMessageConverter
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
-->
<!-- 开启SpringMVC注解驱动,该注解帮我们注册了多个特性,包括JSR-303校验支持、信息转换以及对域格式化的支持 -->
<mvc:annotation-driven/>
3、运行结果如下所示
4、可以看到页面上返回了我们 自定义封装异常信息返回前端的JSON类,但是可以看到data:null也返回到了前端页面上,有时候我们需要把为null的字段 不展示到前端页面中,这时我们需要在 自定义封装异常信息返回前端的JSON类上添加这么一个注解@JsonInclude(JsonInclude.Include.NON_NULL) 即可,运行结果如下所示:
解决方式二:使用FastJson自定义HttpMessageConverter消息转换器
1、在pom.xml文件中 添加 fastjson jar包
<!-- FastJson Json处理工具包 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
2、在applicationContext-spring.xml配置文件中 添加如下配置
2.1、在<beans>标签中添加 mvc命名空间
xmlns:mvc="http://www.springframework.org/schema/mvc"
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd
2.2、自定义HttpMessageConverter消息转换器,注意:序列化特征配置对泛型字段不起作用
关于更多serializerFeatures 序列化特性配置 和日期转换格式的介绍,请点击常用FastJSON的SerializerFeature特性及日期转换格式
<!-- 自定义HttpMessageConverter消息转换器 -->
<mvc:annotation-driven>
<!-- register-defaults="false" 表示设置不使用默认的消息转换器 -->
<mvc:message-converters register-defaults="false">
<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
<!-- 配置支持的消息类型 -->
<property name="supportedMediaTypes">
<list>
<value>text/html</value>
<value>application/json;charset=UTF-8</value>
</list>
</property>
<property name="fastJsonConfig" ref="fastJsonConfig" />
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<bean id="fastJsonConfig" class="com.alibaba.fastjson.support.config.FastJsonConfig">
<!-- 序列化特性配置 -->
<property name="serializerFeatures">
<list>
<!-- 格式化输出 -->
<value>PrettyFormat</value>
<!-- 是否输出值为null的字段,默认为false -->
<value>WriteMapNullValue</value>
<!-- List字段如果为null,输出为[],而非null-->
<value>WriteNullListAsEmpty</value>
<!-- 字符类型字段如果为null,输出为"",而非null -->
<value>WriteNullStringAsEmpty</value>
<!-- Boolean字段如果为null,输出为false,而非null -->
<value>WriteNullBooleanAsFalse</value>
<!-- 消除对同一对象循环引用的问题,默认为false(如果不配置有可能会进入死循环)-->
<value>DisableCircularReferenceDetect</value>
</list>
</property>
<!-- 日期格式配置 -->
<property name="dateFormat" value="yyyy-MM-dd HH:mm:ss"/>
</bean>