Java全局异常处理
- 1、Java中异常相关概念
- 1.1异常类
- 1.2异常的处理方式
- 1.3注意事项
- 1.4自定义异常
- 2、配置全局异常处理
- 2.1准备工作
- 2.2全局异常处理实现
- 2.3特殊情况filter中的异常如何捕捉
1、Java中异常相关概念
1.1异常类
- Throwable类:Java中所有异常类的父类,它包含了最终要的两个类Exception和Error。
- Error类:属于程序无法处理的错误,是JVM需要承担的,无法通过try-catch进行捕捉,例如系统崩溃、内存不足、堆栈溢出,编译器不会对这类异常进行检查,一旦发生就容易导致程序运行终止,仅靠程序本身无法恢复。
- Exception:程序本身可以处理的异常,可以通过catch进行捕捉,也是我们需要处理的,以保证程序能够正常运行
Exception又分为运行时异常(RunTimeException,又叫非受检查异常unchecked Exception)和非运行时异常(又叫受检查异常checked Exception)。
运行时异常我们可处理可不处理,一般由程序逻辑错误引起,我们应该在编码时尽量避免这种错误,比如:NullPointException
非运行时异常时Exception中除RunTimeException以外的异常,比如:IOException、SQLException等以及我们自定义的Exception异常,这种异常,Java编译器会强制要求我们处理
@SneakyThrows注解:作用在方法上,加上以后可以对非运行时异常不进行处理
1.2异常的处理方式
- try-catch:try中放可能发生异常的代码,如果发生异常,后面的代码不会再执行,直接进入catch,在catch中拿到异常对象,我们进行处理
- try-catch-finally:finally是无论异常是否发生都会执行的,通常用来释放资源
- try-finally:相当于没有捕捉异常
- throws:在方法名后面进行抛出,表明该方法对此异常不进行处理,由调用者进行处理,谁用谁处理,调用者也可继续向上抛出。
- throw:在方法内进行抛出,我们手动抛出一个异常对象
1.3注意事项
- 对于非运行时异常,程序必须进行处理,用try-catch或throws都可以,在写代码时idea会提示
- 对运行时异常,程序中没有处理,默认处理方法时throws
- 子类重写父类的方法时,对抛出异常的规定:子类重写的方法,所抛出的异常类型不能大于父类异常的类型,可以是一样的类型或者是父类异常的子类
1.4自定义异常
- 自定义异常类继承Exception或RunTimeException
- 继承Exception属于非运行时异常
- 继承RunTimeException属于运行时异常
2、配置全局异常处理
在项目中我们通常会写很多接口,各种各样的异常出现会让我们的返回结果很受影响,因为我们的接口都会写通用的返回格式,但是异常出现时返回的错误就和我们的返回格式产生分歧,所以为了保证这种情况不出现,我们就需要配置全局异常处理,在异常发生时也按照我们想要的返回格式。
核心:@RestControllerAdvice+@ExceptionHandler
2.1准备工作
常见的操作码
/**
* 枚举了一些常用API操作码
*/
public enum ResultCode implements IErrorCode {
SUCCESS(200, "操作成功"),
FAILED(400, "操作失败"),
VALIDATE_FAILED(404, "参数检验失败"),
UNAUTHORIZED(401, "暂未登录或token已经过期"),
FORBIDDEN(403, "没有相关权限");
private int code;
private String message;
private ResultCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
封装API的错误码
/**
* 封装API的错误码
*/
public interface IErrorCode {
int getCode();
String getMessage();
}
通用的返回体
import com.lcp.fitness.common.api.IErrorCode;
import com.lcp.fitness.common.api.ResultCode;
import lombok.Data;
import java.io.Serializable;
@Data
public class CommonResponse<T> implements Serializable {
private int code;
private String msg;
private T data;
private boolean success;
public CommonResponse(int code, String msg) {
this.code = code;
this.msg = msg;
}
public CommonResponse(int code, String msg, T data, boolean success) {
this.code = code;
this.msg = msg;
this.data = data;
this.success = success;
}
//失败返回结果
public static <T> CommonResponse fail() {
return new CommonResponse(ResultCode.FAILED.getCode(), ResultCode.FAILED.getMessage(), null, false);
}
//失败返回结果
public static <T> CommonResponse fail(String msg) {
return new CommonResponse(ResultCode.FAILED.getCode(), msg, null, false);
}
//失败返回结果
public static <T> CommonResponse fail(IErrorCode errorCode) {
return new CommonResponse(errorCode.getCode(), errorCode.getMessage(), null, false);
}
//失败返回结果
public static <T> CommonResponse fail(IErrorCode errorCode, String msg) {
return new CommonResponse(errorCode.getCode(), msg, null, false);
}
//失败返回结果
public static <T> CommonResponse fail(int code, String msg) {
return new CommonResponse(code, msg, null, false);
}
//成功返回结果
public static <T> CommonResponse success() {
return new CommonResponse(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), null, true);
}
//成功返回结果
public static <T> CommonResponse success(T data) {
return new CommonResponse(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data, true);
}
//成功返回结果
public static <T> CommonResponse success(String msg, T data) {
return new CommonResponse(ResultCode.SUCCESS.getCode(), msg, data, true);
}
/**
* 参数验证失败返回结果
*/
public static <T> CommonResponse<T> validateFailed() {
return fail(ResultCode.VALIDATE_FAILED);
}
/**
* 参数验证失败返回结果
* @param message 提示信息
*/
public static <T> CommonResponse<T> validateFailed(String message) {
return new CommonResponse<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null, false);
}
/**
* 未登录返回结果
*/
public static <T> CommonResponse<T> unauthorized(T data) {
return new CommonResponse<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data, false);
}
/**
* 未授权返回结果
*/
public static <T> CommonResponse<T> forbidden(T data) {
return new CommonResponse<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data, false);
}
2.2全局异常处理实现
自定义我们的异常类
import com.lcp.fitness.common.api.IErrorCode;
/**
* 自定义API异常
*/
public class ApiException extends RuntimeException {
private IErrorCode errorCode;
public ApiException(IErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public ApiException(String message) {
super(message);
}
public ApiException(Throwable cause) {
super(cause);
}
public ApiException(String message, Throwable cause) {
super(message, cause);
}
public IErrorCode getErrorCode() {
return errorCode;
}
}
全局异常处理:
这里可以使用@RestControllerAdvice+@ExceptionHandler或者@ControllerAdvice+@ExceptionHandler+@ResponseBody,都是可以的,@RestControllerAdvice=@ControllerAdvice+@ResponseBody。
import com.lcp.fitness.utils.CommonResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(value = ApiException.class)
public CommonResponse handle(ApiException e) {
log.error(e.getMessage());
return CommonResponse.fail(e.getMessage());
}
@ExceptionHandler(value = Exception.class)
public CommonResponse exception(Exception e) {
log.error(e.getMessage(), e);
return CommonResponse.fail(e.getMessage());
}
/**
* springsecurity权限认证失败返回
* @param e
* @return
*/
@ExceptionHandler(value = AccessDeniedException.class)
public CommonResponse accessDeniedException(AccessDeniedException e) {
log.error(e.getMessage());
return CommonResponse.fail("用户无权限访问");
}
}
这样在我们某个接口再有运行时异常时,就不会有奇奇怪怪的格式了,我们希望即使有错误也都是我们定义好的这种格式
如果没有处理过的话就是像这样的报错,和我们想要的格式完全不一样,前端也不好处理
用了全局的异常处理我们就可以随心所欲了,可以完全按照我们的格式返回错误码和错误信息。
2.3特殊情况filter中的异常如何捕捉
从我们全局异常的注解名字@RestControllerAdvice我们也可以看出,他是针对controller层做了切面处理,也就是说如果异常最终出现在了controller层中,我们可以进行处理,但是我就遇到了一种特殊情况,请求以后代码报错了,但是我的接口没有返回任何信息
我的后台日志也收到了请求,并打印了错误
这里是因为该异常没有经过controller,在filter中就失败返回了,所以最终返回类型是void,这是我在认证token的过滤器中,尽管抛出了异常,但是接口的返回结果仍然是空的,这也证实了**@RestControllerAdvice注解只在controller层起到了作用这一点,一旦我们的异常没有到达controller就结束,全局异常的配置是没有任何作用的。**
import com.lcp.fitness.common.component.RedisCache;
import com.lcp.fitness.dto.LoginUser;
import com.lcp.fitness.utils.JwtTokenUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(request, response);
return;
}
//解析token
String userId = null;
try {
Claims claims = JwtTokenUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token无效");
}
//从redis中获取用户信息
String redisKey = "login:" + userId;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
//存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
解决filter中不起作用,我们没有办法改变@RestControllerAdvice注解的作用域,我的解决思路是将filter中的异常扔到controller层中,为此需要定义一个controller,专门用来接收这些特殊情况的异常。
import com.lcp.fitness.common.exception.ApiException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* 全局异常处理-filter中的异常处理(全局异常只能处理controller层的异常,而filter中的异常捕捉不到,
* 所以需要将filter中的异常全部重定向到该controller中,以实现全局异常的统一处理格式)
*/
@RestController
@RequestMapping("/exception")
public class ExceptionController {
@RequestMapping("/handler")
public void exception(HttpServletRequest request) {
String msg = (String) request.getAttribute("msg");
throw new ApiException(msg);
}
}
在filter中,将原来throw抛出异常的代码改成下面的代码,使用重定向将异常信息转到controller层中
request.setAttribute("msg", "token无效");
request.getRequestDispatcher("/exception/handler").forward(request, response);
然后在filter中发生异常时我们的接口返回了通用的格式,但是后台日志又报了另外的错误
出现这个问题的原因就是在发生异常时我们进行了重定向,但是因为filter中的代码并没有结束,依然在向下执行,所以这里我们在重定向后filter中的代码理应结束了,我们加上一个return就好了。
request.setAttribute("msg", "token无效");
request.getRequestDispatcher("/exception/handler").forward(request, response);
return;