aaa问题现象:
????????今天在项目中,遇到了一个需求:
? ? ? ? 如何解决接口调用报错时,暴露了接口涉及的包名、类名等敏感信息的问题?
问题分析:
? ? ? ? 其实在很多正常的小项目,对这种情况是不做处理的,因为即使暴露了? ? ? ? ?
????????起因是因为甲方在使用安全测试工具检测接口的时候,发现接口返回的报文中存在敏感信息,会暴露接口逻辑中用到的包名、类名,举个例子如:
????????
????????如上图,接口:/pms-amap-sgcc-service/report/deviceScaleStat/transmission 在调用时,
????????通过 删除部分请求体 来检测接口,这种时候接口很显然会报400状态码(参数异常)的错误,并返回相关信息;而问题就出现在响应体中的 message ,这里面提到了红框中所示的
1、com.fasterxml.jackson.databind.JsonMappingException;
2、PushbackInputStream;
3、com.thpower.rpt.pojo.dto.StatisticRequest;
这三个暴露了接口逻辑中涉及的路径和类名。
????????其实,第1、2点还能理解,毕竟这不是接口的代码逻辑问题,而是使用spring或springboot框架书写Controller层接口时,框架底层逻辑涉及的技术,当接口调用报错,默认就会返回这些信息。
????????而spring和springboot都是开源框架,所以这个暴露了,也说得过去,毕竟世人皆知:现在的java服务基本都是基于这框架开发的。
????????但不管怎么样,你终究是暴露了,毕竟不是所有接口都基于这些类来书写的,而且要是甲方硬性要求你解决,你也没办法。。。
????????第3点,也是最关键的地方,因为这个类名很显然是和开源框架无关了,完全是暴露了你项目中自定义的路径和类名了,因此是妥妥的敏感信息,因此必须改造接口调用报错的响应体。
? ? ? ? 所以,要如何实现“接口调用报错响应体的改造”呢?
? ? ? ? 一开始,我是想着修改接口的响应体类型,后来发现不行,因为这种报错是框架底层捕获到的接口调用后出现的状态码报错,而不是这个接口里写的逻辑代码报的错。
? ? ? ? 看到报错二字,我想起了曾经学过的一个知识点,总算解决了这个问题,那就是自定义springboot的异常处理器,可以完美的实现 “接口调用报错响应体的改造”!!!
解决方法:
? ? ? ? 说到自定义springboot的异常处理器,就得提一下自定义异常处理和全局异常处理了,一般来说这两个异常会一起配置。
1、依赖包
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.59</version>
</dependency>
</dependencies>
2、自定义接口异常处理后的响应体对象
先加入自定义的时间转换工具类CalendarUtil.java
import org.springframework.util.StringUtils;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
public class CalendarUtil {
public static SimpleDateFormat longestFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
public static SimpleDateFormat longFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static SimpleDateFormat monthFormatter = new SimpleDateFormat("yyyy-MM");
public static SimpleDateFormat dayFormatter = new SimpleDateFormat("yyyy-MM-dd");
public static SimpleDateFormat hourFormatter = new SimpleDateFormat("yyyy-MM-dd HH:00:00");
//获取指定日期对应的年份
public static int getYear(Date date) {
if ( date != null ) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
return calendar.get(Calendar.YEAR);
}
return -1;
}
//获取指定日期对应的月份
public static int getMonth(Date date) {
if ( date != null ) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
return calendar.get(Calendar.MONTH) + 1;
}
return -1;
}
//获取指定日期对应的是当月的第几天
public static int getDayOfMonth(Date date) {
if ( date != null ) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
return calendar.get(Calendar.DAY_OF_MONTH);
}
return -1;
}
//获取指定日期对应的是今年的第几周
public static int getWeekOfYear(Date date) {
if ( date != null ) {
Calendar calendar = Calendar.getInstance();
calendar.setFirstDayOfWeek(Calendar.MONDAY);
calendar.setTime(date);
return calendar.get(Calendar.WEEK_OF_YEAR);
}
return -1;
}
//获取指定日期对应的星期几
public static int getDayOfWeek(Date date) {
if ( date != null ) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
if ( calendar.get(Calendar.DAY_OF_WEEK) == 1 ) {
return 7;
}
return calendar.get(Calendar.DAY_OF_WEEK) - 1;
}
return -1;
}
//获取指定月份的最后一天
public static int getLastDayOfMonth(String dateString) {
if ( !StringUtils.isEmpty(dateString) && dateString.contains("-") ) {
Calendar calendar = Calendar.getInstance();
int year = Integer.valueOf(dateString.split("-")[0]);
int month = 0;
if ( !StringUtils.isEmpty(dateString.split("-")[1]) ) {
month = Integer.valueOf(dateString.split("-")[1]);
}
// 设置年份
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month - 1);
calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
return calendar.get(Calendar.DAY_OF_MONTH);
}
return -1;
}
//获取指定日期对应的时
public static int getHour(Date date) {
if ( date != null ) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
return calendar.get(Calendar.HOUR_OF_DAY);
}
return -1;
}
//获取指定日期对应的分
public static int getMinute(Date date) {
if ( date != null ) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
return calendar.get(Calendar.MINUTE);
}
return -1;
}
//获取指定日期对应的秒
public static int getSecond(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
return calendar.get(Calendar.SECOND);
}
//获取指定日期对应的是第几季度
public static int getSeason(Date date) {
if ( date != null ) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
int month = calendar.get(Calendar.MONTH) + 1;
if ( month >= 1 && month <= 3 ) {
return 1;
} else if ( month >= 4 && month <= 6 ) {
return 2;
} else if ( month >= 7 && month <= 9 ) {
return 3;
} else if ( month >= 10 && month <= 12 ) {
return 4;
} else {
return 0;
}
}
return -1;
}
//获取指定日期时间的昨天的对应日期时间
public static Date getYesterdayOfDate(Date date) {
if ( date != null ) {
Calendar calendar = Calendar.getInstance();
calendar.set(getYear(date), getMonth(date) - 1, getDayOfMonth(date));
calendar.add(Calendar.DATE, -1);//-1.昨天时间 0.当前时间 1.明天时间 *以此类推
return calendar.getTime();
}
return null;
}
//获取指定日期时间的小时(date类型)
public static Date getHourOfDate(Date date) {
if ( date != null ) {
if ( hourFormatter.format(date).matches("^[0-9]*$") ) {
return hourFormatter.parse(hourFormatter.format(date), new ParsePosition(0));
}
}
return null;
}
//获取指定日期时间的上一个小时的时间(date类型)
public static Date getLastHourOfDate(Date date) {
if ( date != null ) {
String dateString = hourFormatter.format(date);
Integer hour = Integer.valueOf(dateString.substring(11, 13));
if ( hour == 0 ) {
Date yesterday = CalendarUtil.getYesterdayOfDate(date);
System.out.println("yesterday:" + yesterday);
String dayString = CalendarUtil.toDateString(CalendarUtil.getHourOfDate(yesterday));
System.out.println("dayString:" + dayString);
dateString = new StringBuilder(dayString).replace(11, 13, "23").toString();
} else {
dateString = new StringBuilder(dateString).replace(11, 13, String.valueOf(hour - 1)).toString();
}
return hourFormatter.parse(dateString, new ParsePosition(0));
}
return null;
}
//字符串转为date类型并精确到时
public static Date toHourOfDate(String dateString) {
if ( !StringUtils.isEmpty(dateString) ) {
return hourFormatter.parse(dateString, new ParsePosition(0));
}
return null;
}
//字符串转为date类型并精确到天
public static Date toDayOfDateString(String dateString) {
if ( !StringUtils.isEmpty(dateString) ) {
return dayFormatter.parse(dateString, new ParsePosition(0));
}
return null;
}
public static String toStringOfDateString(String dateString) {
if ( !StringUtils.isEmpty(dateString) ) {
Date date = dayFormatter.parse(dateString, new ParsePosition(0));
return dayFormatter.format(date);
}
return null;
}
//字符串转为date类型并精确到天
public static Date toMonthOfDate(Date date) {
if ( date != null ) {
return monthFormatter.parse(monthFormatter.format(date), new ParsePosition(0));
}
return null;
}
//规范date类型格式精确到天
public static String toDayStringOfDate(Date date) {
if ( date != null ) {
return dayFormatter.format(date);
}
return null;
}
//规范date类型格式精确到天
public static String toDayStringOfDate(long time) {
return toDayStringOfDate(new Date(time));
}
//规范long类型毫秒值(时间戳)格式为String类型
public static String toDateString(Long time) {
if ( time != null ) {
return longFormatter.format(new Date(time));
}
return null;
}
//规范long类型毫秒值(时间戳)格式为String类型
public static Date toDate(Long time) {
if ( time != null ) {
return new Date(time);
}
return null;
}
//规范long类型毫秒值(时间戳)格式为String类型
public static Long toMillisTime(String dateString) {
if ( !StringUtils.isEmpty(dateString) ) {
return longFormatter.parse(dateString, new ParsePosition(0)).getTime();
}
return null;
}
//规范long类型毫秒值(时间戳)格式为Date类型
public static Long toMillisTime(Date date) {
if ( date != null ) {
return date.getTime();
}
return null;
}
//规范date类型格式为[长时间格式]
public static Date toLongFormatDate(Date date) {
if ( date != null ) {
return longFormatter.parse(longFormatter.format(date), new ParsePosition(0));
}
return null;
}
//字符串转为date类型[长时间格式]
public static Date toDate(String dateString) {
if ( !StringUtils.isEmpty(dateString) ) {
return longFormatter.parse(dateString, new ParsePosition(0));
}
return null;
}
//字符串转为date类型[超长时间格式](精确到毫秒)
public static Date toMSDate(String dateString) {
if ( !StringUtils.isEmpty(dateString) ) {
return longestFormatter.parse(dateString, new ParsePosition(0));
}
return null;
}
//date类型转为字符串[长时间格式]
public static String toDateString(Date date) {
if ( date != null ) {
return longFormatter.format(date);
}
return null;
}
//拼接为date类型[长时间格式]
public static Date joinDate(Integer year, Integer month, Integer day, Date date) {
if ( year != null && month != null && day != null && date != null ) {
StringBuilder sb = new StringBuilder();
sb.append(year);
sb.append("-");
sb.append(month);
sb.append("-");
sb.append(day);
sb.append(" ");
sb.append(longFormatter.format(date).split(" ")[1]);
return longFormatter.parse(sb.toString(), new ParsePosition(0));
}
return null;
}
//计算两个日期间隔多少天
public static int daysBetween(Date startDate, Date endDate) {
//直接通过计算两个日期的毫秒数,他们的差除以一天的毫秒数,即可得到想要的两个日期相差的天数。
if ( startDate != null && endDate != null ) {
return (int)((endDate.getTime() - startDate.getTime()) / (1000 * 3600 * 24));
}
return -1;
}
//计算两个日期间隔多少毫秒
public static Long msBetween(Date startDate, Date endDate) {
//直接通过计算两个日期的毫秒数,他们的差除以一天的毫秒数,即可得到想要的两个日期相差的天数。
if ( startDate != null && endDate != null ) {
return endDate.getTime() - startDate.getTime();
}
return Long.valueOf(-1);
}
//计算两个日期间隔多少小时
public static int hoursBetween(Date startDate, Date endDate) {
if ( startDate != null && endDate != null ) {
//直接通过计算两个日期的毫秒数,他们的差除以一天的毫秒数,即可得到想要的两个日期相差的天数。
return (int)((endDate.getTime() - startDate.getTime()) / (1000 * 3600));
}
return -1;
}
//计算两个日期间隔多少分钟
public static int minutesBetween(Date startDate, Date endDate) {
//直接通过计算两个日期的毫秒数,他们的差除以一分钟的毫秒数,即可得到想要的两个日期相差的分钟数。
if ( startDate != null && endDate != null ) {
return (int)((endDate.getTime() - startDate.getTime()) / (1000 * 60));
}
return -1;
}
//计算某个日期过了指定分钟后的date时间
public static Date minutesAfter(Date date, Double minutes) {
if ( date != null && minutes != null ) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.add(Calendar.MINUTE, minutes.intValue());// 24小时制
return cal.getTime();
}
return null;
}
//获取指定日期对应的月份
public static String getLastMonthOfYear(Date date) {
if ( date != null ) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
int lastMonth = calendar.get(Calendar.MONTH);
int year = calendar.get(Calendar.YEAR);
if ( lastMonth == 0 ) {
lastMonth = 12;
year--;
}
return new StringBuilder().append(year).append("-").append(lastMonth).toString();
}
return null;
}
//获取几天前的时间
public static Date getDateBefore(Date d, int day) {
Calendar now = Calendar.getInstance();
now.setTime(d);
now.set(Calendar.DATE, now.get(Calendar.DATE) - day);
return now.getTime();
}
//获取几天后的时间
public static Date getDateAfter(Date d, int day) {
Calendar now = Calendar.getInstance();
now.setTime(d);
now.set(Calendar.DATE, now.get(Calendar.DATE) + day);
return now.getTime();
}
//获取long类型时间差值转换为时分秒毫秒表达式
public static String getMilliSecondExpression(Long secondBetween) {
String expression = "";
if ( secondBetween != null ) {
Long hours = 0L;
Long minutes = 0L;
Long seconds = 0L;
Long milliSeconds = 0L;
Double floor = Math.floor(secondBetween / 3600000);
if ( floor > 0 ) {
hours = Math.round(floor);
secondBetween = secondBetween - (hours * 3600000);
expression += hours + "时 ";
}
floor = Math.floor(secondBetween / 60000);
if ( floor > 0 ) {
minutes = Math.round(floor);
secondBetween = secondBetween - (minutes * 60000);
expression += minutes + "分 ";
}
floor = Math.floor(secondBetween / 1000);
if ( floor > 0 ) {
seconds = Math.round(floor);
secondBetween = secondBetween - (seconds * 1000);
expression += seconds + "秒 ";
}
if ( secondBetween > 0 ) {
milliSeconds = secondBetween;
expression += milliSeconds + "毫秒";
}
}
if ( StringUtils.isEmpty(expression) ) {
expression = "0 毫秒";
} return expression;
}
}
GlobalResponse.java
import com.example.demo.utils.CalendarUtil;
import org.springframework.http.HttpStatus;
import java.io.Serializable;
public class GlobalResponse<T> implements Serializable {
private Integer code;
private String message;
private T data;
private final String timeStamp = CalendarUtil.toDateString(System.currentTimeMillis());
public static final String SUCCESS = "成功";
public static final String FAILURE = "失败";
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public String getTimeStamp() {
return timeStamp;
}
public static String getSUCCESS() {
return SUCCESS;
}
public static String getFAILURE() {
return FAILURE;
}
public GlobalResponse(int code, String message) {
this.code = code;
this.message = message;
}
public GlobalResponse(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public static <T> GlobalResponse<T> success() {
return new GlobalResponse<>(HttpStatus.OK.value(), SUCCESS);
}
public static <T> GlobalResponse<T>success(T data) {
return new GlobalResponse<>(HttpStatus.OK.value(), SUCCESS, data);
}
public static <T> GlobalResponse<T>fail(T data) {
return new GlobalResponse<>(HttpStatus.INTERNAL_SERVER_ERROR.value(), FAILURE, data);
}
public static <T> GlobalResponse<T>fail(int code,T data) {
return new GlobalResponse<>(code, FAILURE, data);
}
public static <T> GlobalResponse<T>fail(HttpStatus status,T data) {
return new GlobalResponse<>(status.value(), FAILURE, data);
}
}
3、自定义异常类
GlobalException.java
/**
* 自定义异常
*/
public class GlobalException extends RuntimeException {
public GlobalException() {
}
public GlobalException(String message) {
super(message);
}
public GlobalException(String message, Throwable t) {
super(message, t);
}
}
4、自定义注解-用于改写响应体的body区
ResponseResultBody.java
import org.springframework.web.bind.annotation.ResponseBody;
import java.lang.annotation.*;
/**
* @RequestBody 是作用在形参列表上,用于将前台发送过来固定格式的数据【xml格式 或者 json等】封装为对应的 JavaBean 对象,
* 封装时使用到的一个对象是系统默认配置的 HttpMessageConverter进行解析,然后封装到形参上。
*
* @ResponseBody 的作用其实是将java对象转为json格式的数据。
* @ResponseBody 注解的作用是将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,
* 通常用来返回JSON数据或者是XML数据。
* @ResponseBody是作用在方法上的,@ResponseBody 表示该方法的返回结果直接写入 HTTP response body 中,一般在异步获取数据时使用【也就是AJAX】。
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@ResponseBody
public @interface ResponseResultBody {
}
5、自定义异常处理器handler
这里我直接把自定义异常处理和全局异常处理的逻辑配置成一样了,有需要的小伙伴可以自行定义,修改方法?globalException 的逻辑即可。
注意:
1、不建议直接对 Exception 进行处理,最好是根据各类异常作分别处理。
2、可以通过查看springboot开源框架中 ResponseEntityExceptionHandler类的方法 handleException 的源代码来分别处理。
ResponseEntityExceptionHandler.handleException 方法的源代码:
@ExceptionHandler({HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class, HttpMediaTypeNotAcceptableException.class, MissingPathVariableException.class, MissingServletRequestParameterException.class, ServletRequestBindingException.class, ConversionNotSupportedException.class, TypeMismatchException.class, HttpMessageNotReadableException.class, HttpMessageNotWritableException.class, MethodArgumentNotValidException.class, MissingServletRequestPartException.class, BindException.class, NoHandlerFoundException.class, AsyncRequestTimeoutException.class})
@Nullable
public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
HttpHeaders headers = new HttpHeaders();
HttpStatus status;
if (ex instanceof HttpRequestMethodNotSupportedException) {
status = HttpStatus.METHOD_NOT_ALLOWED;
return this.handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException)ex, headers, status, request);
} else if (ex instanceof HttpMediaTypeNotSupportedException) {
status = HttpStatus.UNSUPPORTED_MEDIA_TYPE;
return this.handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException)ex, headers, status, request);
} else if (ex instanceof HttpMediaTypeNotAcceptableException) {
status = HttpStatus.NOT_ACCEPTABLE;
return this.handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException)ex, headers, status, request);
} else if (ex instanceof MissingPathVariableException) {
status = HttpStatus.INTERNAL_SERVER_ERROR;
return this.handleMissingPathVariable((MissingPathVariableException)ex, headers, status, request);
} else if (ex instanceof MissingServletRequestParameterException) {
status = HttpStatus.BAD_REQUEST;
return this.handleMissingServletRequestParameter((MissingServletRequestParameterException)ex, headers, status, request);
} else if (ex instanceof ServletRequestBindingException) {
status = HttpStatus.BAD_REQUEST;
return this.handleServletRequestBindingException((ServletRequestBindingException)ex, headers, status, request);
} else if (ex instanceof ConversionNotSupportedException) {
status = HttpStatus.INTERNAL_SERVER_ERROR;
return this.handleConversionNotSupported((ConversionNotSupportedException)ex, headers, status, request);
} else if (ex instanceof TypeMismatchException) {
status = HttpStatus.BAD_REQUEST;
return this.handleTypeMismatch((TypeMismatchException)ex, headers, status, request);
} else if (ex instanceof HttpMessageNotReadableException) {
status = HttpStatus.BAD_REQUEST;
return this.handleHttpMessageNotReadable((HttpMessageNotReadableException)ex, headers, status, request);
} else if (ex instanceof HttpMessageNotWritableException) {
status = HttpStatus.INTERNAL_SERVER_ERROR;
return this.handleHttpMessageNotWritable((HttpMessageNotWritableException)ex, headers, status, request);
} else if (ex instanceof MethodArgumentNotValidException) {
status = HttpStatus.BAD_REQUEST;
return this.handleMethodArgumentNotValid((MethodArgumentNotValidException)ex, headers, status, request);
} else if (ex instanceof MissingServletRequestPartException) {
status = HttpStatus.BAD_REQUEST;
return this.handleMissingServletRequestPart((MissingServletRequestPartException)ex, headers, status, request);
} else if (ex instanceof BindException) {
status = HttpStatus.BAD_REQUEST;
return this.handleBindException((BindException)ex, headers, status, request);
} else if (ex instanceof NoHandlerFoundException) {
status = HttpStatus.NOT_FOUND;
return this.handleNoHandlerFoundException((NoHandlerFoundException)ex, headers, status, request);
} else if (ex instanceof AsyncRequestTimeoutException) {
status = HttpStatus.SERVICE_UNAVAILABLE;
return this.handleAsyncRequestTimeoutException((AsyncRequestTimeoutException)ex, headers, status, request);
} else {
throw ex;
}
}
GlobalExceptionHandler.java
import com.alibaba.druid.support.json.JSONUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.lang.annotation.Annotation;
import java.util.List;
/**
* 异常拦截
*/
@RestControllerAdvice
public class GlobalExceptionHandler implements ResponseBodyAdvice<Object> {
private final static Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseResultBody.class;
/**
* 封装返回信息
* 不建议直接对 Exception 进行处理,最好是根据各类异常作分别处理
* 可以通过查看springboot开源框架中 ResponseEntityExceptionHandler类的方法 handleException
* 的源代码来分别处理
*
* @param ex
* @return
*/
private GlobalResponse getExceptionMessage(Exception ex) {
ex.printStackTrace();
if ( ex instanceof NullPointerException ) {
return GlobalResponse.fail("系统错误:空指针异常");
} else if ( ex instanceof HttpRequestMethodNotSupportedException ) {
return GlobalResponse.fail(HttpStatus.METHOD_NOT_ALLOWED, ex.getMessage());
} else if ( ex instanceof HttpMediaTypeNotSupportedException ) {
return GlobalResponse.fail(HttpStatus.UNSUPPORTED_MEDIA_TYPE, ex.getMessage());
} else if ( ex instanceof HttpMediaTypeNotAcceptableException ) {
return GlobalResponse.fail(HttpStatus.NOT_ACCEPTABLE, ex.getMessage());
} else if ( ex instanceof MissingPathVariableException ) {
return GlobalResponse.fail(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
} else if ( ex instanceof MissingServletRequestParameterException ) {
return GlobalResponse.fail(HttpStatus.BAD_REQUEST, ex.getMessage());
} else if ( ex instanceof ServletRequestBindingException ) {
return GlobalResponse.fail(HttpStatus.BAD_REQUEST, ex.getMessage());
} else if ( ex instanceof ConversionNotSupportedException ) {
return GlobalResponse.fail(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
} else if ( ex instanceof TypeMismatchException ) {
return GlobalResponse.fail(HttpStatus.BAD_REQUEST, ex.getMessage());
} else if ( ex instanceof HttpMessageNotReadableException ) {
return GlobalResponse.fail(HttpStatus.BAD_REQUEST, "参数异常");
} else if ( ex instanceof HttpMessageNotWritableException ) {
return GlobalResponse.fail(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
} else if ( ex instanceof MethodArgumentNotValidException ) {
return GlobalResponse.fail(HttpStatus.BAD_REQUEST, ex.getMessage());
} else if ( ex instanceof MissingServletRequestPartException ) {
return GlobalResponse.fail(HttpStatus.BAD_REQUEST, ex.getMessage());
} else if ( ex instanceof BindException ) {
return GlobalResponse.fail(HttpStatus.BAD_REQUEST, ex.getMessage());
} else if ( ex instanceof NoHandlerFoundException ) {
return GlobalResponse.fail(HttpStatus.NOT_FOUND, ex.getMessage());
} else if ( ex instanceof AsyncRequestTimeoutException ) {
return GlobalResponse.fail(HttpStatus.SERVICE_UNAVAILABLE, ex.getMessage());
}
return GlobalResponse.fail("未知错误,请联系管理员");
}
@ExceptionHandler(value = GlobalException.class)
public GlobalResponse<Object> globalException(GlobalException e) {
return getExceptionMessage(e);
}
@ExceptionHandler(Exception.class)
public GlobalResponse<Object> exception(Exception e) {
return getExceptionMessage(e);
}
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public GlobalResponse<Object> parameterExceptionHandler(MethodArgumentNotValidException e) {
e.printStackTrace();
BindingResult bindingResult = e.getBindingResult();
if ( bindingResult.hasErrors() ) {
List<ObjectError> errors = bindingResult.getAllErrors();
FieldError fieldError = (FieldError)errors.get(0);
// logger.warn("object name is " + fieldError.getObjectName());
// logger.warn("defaultMessage is " + fieldError.getDefaultMessage());
// logger.warn("field is" + fieldError.getField());
return GlobalResponse.fail(fieldError.getDefaultMessage());
} else {
return GlobalResponse.fail("参数绑定未知错误");
}
}
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return AnnotatedElementUtils.hasAnnotation(methodParameter.getContainingClass(), ANNOTATION_TYPE) || methodParameter.hasMethodAnnotation(ANNOTATION_TYPE);
}
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if ( o instanceof GlobalResponse ) {
return o;
}
if ( o instanceof String ) {
//obj转换为json字符串
return JSONUtils.toJSONString(GlobalResponse.success(o));
}
return GlobalResponse.success(o);
}
}
6、测试异常处理接口
Controller.java
import com.example.demo.handler.GlobalException;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/demo")
@CrossOrigin
public class Controller {
@GetMapping("/testException")
public int testException() {
int i = 0;
i = 1 / i;
return i;
}
@GetMapping("/testMyException")
public int testMyException() {
int i = 0;
try {
i = 1 / i;
} catch (Exception e) {
throw new GlobalException("捕获到自定义异常");
}
return i;
}
}
测试全局异常处理接口:
??通过查看后台输出日志,可以看出全局异常捕获成功(捕获到算术异常ArithmeticException):
测试自定义异常处理接口:?
?通过查看后台输出日志,可以看出自定义异常捕获成功(捕获到自定义异常GlobalException):
其余代码:
启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan({"com.example"})
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
application.properties配置文件:
# 服务端口
server.port=9000
项目结构:
????????至此所有的接口调用异常都已经做处理了!!!
拓展:
? ? ? ? 有细心的小伙伴可能已经发现了,我前面已经把示例项目中的代码都贴出来了,唯独有一个类没有贴出代码,也就是这个:
????????可以看出来已经被注释掉了(被注释掉的java文件会显示出后缀名),那么这个类是用来干嘛的呢?
? ? ? ? 其实这源自于我的一个测试,是这样的,虽然我们上面已经解决了所有的接口调用异常的处理,然而不属于接口调用异常的问题我们还没有处理到,比如说:接口不存在(状态码404)。
? ? ? ? 那么为什么我要注释掉呢?
????????这里需要我们回顾一下我写这篇文章的原因:
????????如何解决接口调用报错时,暴露了接口涉及的包名、类名等敏感信息的问题?
? ? ? ?而因为接口不存在(状态码404)的报错中并不会涉及到这个原因:
? ? ? ? 所以下面的我将提到的这部分改动,想要尝试的小伙伴请慎重,因为可能还涉及到一些我还没发现的潜在问题。
????????不过秉着科研精神,我就了解了一下如何处理这种情况,话不多说,下面直接贴代码。
7、新增一个异常处理器
RestResponseEntityExceptionHandler .java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
//用于捕获接口无法拦截到的错误状态码,如接口找不到错误:404
@ControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
private final static Logger logger = LoggerFactory.getLogger(RestResponseEntityExceptionHandler.class);
public RestResponseEntityExceptionHandler() {
super();
}
@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
logger.error(ex.getMessage(), ex);
if ( HttpStatus.INTERNAL_SERVER_ERROR.equals(status) ) {
request.setAttribute("javax.servlet.error.exception", ex, 0);
}
return new ResponseEntity(new GlobalResponse<>(status.value(), ex.getMessage()), headers, status);
}
}
8、修改 application.properties 配置文件
修改后:
# 服务端口
server.port=9000
#出现错误时, 直接抛出异常
spring.mvc.throw-exception-if-no-handler-found=true
#不要为我们工程中的资源文件建立映射
spring.web.resources.add-mappings=false
重启服务后,再次测试不存在的接口http://localhost:9000/demo/testExceptionxxxxx:
??通过查看后台输出日志,可以看出接口不存在异常捕获成功(捕获到接口不存在异常NoHandlerFoundException):
? ? ? ? ?至此,接口不存在异常(状态码404)也已经处理了!
????????然而,不能高兴的太早,因为我刚才说了有“潜在问题”的存在!!!!!!
????????怎么回事呢?
????????就是因为application.properties 配置文件新增了一个配置:
#不要为我们工程中的资源文件建立映射
spring.web.resources.add-mappings=false
? ? ? ? 这个配置会导致我们配置在resource目录路径下的资源文件失效!!!!!!
比如我们常用的swagger:
swagger依赖包:
<!--swagger包:用于支持swagger接口文档-->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.13</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.22</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-bean-validators</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
注意:? ? ? ?
????????建议使用springboot 2.5.x或以下版本,swagger使用2.9.x,可以解决兼容性问题;可以在pom.xml 中设置springboot版本,如下我设置为2.5.6版本:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
????????如果不兼容会报如下错误:
? ? ? ? 因为swagger 和 springboot 版本是有兼容性问题的;如果是springboot2.6.x以上的版本(现在最新的是2.7.x),需要和swagger 3.0.0以上的版本才能兼容,而且还需要改一些配置,详情可见文
解决方案之‘Failed to start bean ‘documentationPluginsBootstrapper‘; nested exception is java.lang.NullPoi_技术宅星云的博客-CSDN博客
swagger配置:
import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.Order;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* @author Stephen
* @date: 2022/05/23
* @description: swagger文档配置类
*/
@Configuration
@EnableSwagger2//注解开启 swagger2 功能
@EnableSwaggerBootstrapUI
@Import(BeanValidatorPluginsConfiguration.class)
public class SwaggerConfig {
@Bean(name = "demoApi")
@Order(value = 0)
public Docket groupRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(groupApiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo groupApiInfo(){
return new ApiInfoBuilder()
.title("demo服务 API文档")
.description("demo服务 开发接口文档")
.termsOfServiceUrl("http://localhost:9000")
.version("1.0.0")
.build();
}
}
先注释掉?RestResponseEntityExceptionHandler 类和新增的两个配置,重启服务。
?然后访问swagger地址:http://localhost:9000/swagger-ui.html
就能看见绿色样式的swagger文档了。
点开controller,就能看见我们上面定义的两个接口了:
?????????或者访问地址:http://localhost:9000/doc.html
? ? ? ? 可以看见蓝色样式的swagger文档:
接着我们把注释掉?RestResponseEntityExceptionHandler 类和新增的两个配置再次打开,重启服务。
????????再次访问swagger文档,:
? ? ? ? ?可以看到访问失败了,这报错就和我们配置了RestResponseEntityExceptionHandler 之后,访问一个不存在的接口时是一样的报错:
? ? ? ? 这就是我所说的“潜在问题”,因为swagger文档的页面是需要用到资源文件来构建的,然而我们加了配置?spring.web.resources.add-mappings=false 后,导致它访问不到它需要的资源文件了,就导致了404报错。
? ? ? ? 当然了,也有解决方法:
9、新增处理器放行swagger文档访问的资源文件地址
ResourceHandler.java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class ResourceHandler implements WebMvcConfigurer{
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/", "/static", "/public");
registry.addResourceHandler("/doc.html")
.addResourceLocations("classpath:/META-INF/resources/", "/static", "/public");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
重启服务后,再次访问swagger文档:
? ? ? ? 再访问不存在的接口试试:
????????发现达到了我们想要的结果了!!!
? ? ? ? 至此解决了因为新增?RestResponseEntityExceptionHandler 处理器导致swagger文档无法访问的问题了!!!
? ? ? ? 当然了,是否还存在其他“潜在问题”就不好说了,目前就测在这里。。。。。。
总结:
????????两个建议:
????????1、不要捕获这种接口不存在的异常,除非有硬性要求。
? ? ? ? 2、当用到需要访问到资源文件的技术框架时,需要在?ResourceHandler 处理器的 addResourceHandlers 方法中给所需访问的资源文件地址放行。
|