Springboot自定义全局异常处理器-封装接口异常的响应体,防止异常时暴露程序包名类名路径信息(附swagger文档的使用教程与资源拦截问题)

目录

问题现象:

问题分析:

解决方法:

1、依赖包

2、自定义接口异常处理后的响应体对象

3、自定义异常类

4、自定义注解-用于改写响应体的body区

5、自定义异常处理器handler

6、测试异常处理接口

拓展:

7、新增一个异常处理器

8、修改 application.properties 配置文件

9、新增处理器放行swagger文档访问的资源文件地址

总结:


问题现象:

        今天在项目中,遇到了一个需求:

        如何解决接口调用报错时,暴露了接口涉及的包名、类名等敏感信息的问题?


问题分析:

        其实在很多正常的小项目,对这种情况是不做处理的,因为即使暴露了         

        起因是因为甲方在使用安全测试工具检测接口的时候,发现接口返回的报文中存在敏感信息,会暴露接口逻辑中用到的包名、类名,举个例子如:

        

        如上图,接口:/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;
import java.util.TimeZone;

/**
 * 时间格式转换工具类
 *
 * @author Stephen
 * @date 2022/2/18 11:32
 * JDK 1.8
 */
public class CalendarUtil {
//  SimpleDateFormat是线程不安全的,多线程并发容易出问题,所以转为ThreadLocal变量,详情可见main方法最下面的【模拟多线程并发测试】
//	public static SimpleDateFormat longestFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
//	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 SimpleDateFormat longFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

	private static final ThreadLocal<SimpleDateFormat> longestFormatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"));
	private static final ThreadLocal<SimpleDateFormat> longFormatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
	private static final ThreadLocal<SimpleDateFormat> monthFormatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM"));
	private static final ThreadLocal<SimpleDateFormat> dayFormatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
	private static final ThreadLocal<SimpleDateFormat> hourFormatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:00:00"));

	private static SimpleDateFormat getLongestFormatter() {
		return longestFormatter.get();
	}

	private static SimpleDateFormat getLongFormatter() {
		return longFormatter.get();
	}

	private static SimpleDateFormat getMonthFormatter() {
		return monthFormatter.get();
	}

	private static SimpleDateFormat getDayFormatter() {
		return dayFormatter.get();
	}

	private static SimpleDateFormat getHourFormatter() {
		return hourFormatter.get();
	}

	//获取指定日期对应的年份
	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 ( getHourFormatter().format(date).matches("^[0-9]*$") ) {
				return getHourFormatter().parse(getHourFormatter().format(date), new ParsePosition(0));
			}
		}
		return null;
	}

	//获取指定日期时间的上一个小时的时间(date类型)
	public static Date getLastHourOfDate(Date date) {
		if ( date != null ) {
			String dateString = getHourFormatter().format(date);
			Integer hour = Integer.valueOf(dateString.substring(11, 13));
			if ( hour == 0 ) {
				Date yesterday = getYesterdayOfDate(date);
				System.out.println("yesterday:" + yesterday);
				String dayString = toDateString(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 getHourFormatter().parse(dateString, new ParsePosition(0));
		}
		return null;
	}

	//字符串转为date类型并精确到时
	public static Date toHourOfDate(String dateString) {
		if ( !StringUtils.isEmpty(dateString) ) {
			return getHourFormatter().parse(dateString, new ParsePosition(0));
		}
		return null;
	}

	//字符串转为date类型并精确到天
	public static Date toDayOfDateString(String dateString) {
		if ( !StringUtils.isEmpty(dateString) ) {
			return getDayFormatter().parse(dateString, new ParsePosition(0));
		}
		return null;
	}

	public static String toStringOfDateString(String dateString) {
		if ( !StringUtils.isEmpty(dateString) ) {
			Date date = getDayFormatter().parse(dateString, new ParsePosition(0));
			return getDayFormatter().format(date);
		}
		return null;
	}

	//字符串转为date类型并精确到天
	public static Date toMonthOfDate(Date date) {
		if ( date != null ) {
			return getMonthFormatter().parse(getMonthFormatter().format(date), new ParsePosition(0));
		}
		return null;
	}

	//规范date类型格式精确到天
	public static String toDayStringOfDate(Date date) {
		if ( date != null ) {
			return getDayFormatter().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 getLongFormatter().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 getLongFormatter().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 getLongFormatter().parse(getLongFormatter().format(date), new ParsePosition(0));
		}
		return null;
	}

	//字符串转为date类型[长时间格式]
	public static Date toDate(String dateString) {
		if ( !StringUtils.isEmpty(dateString) ) {
			return getLongFormatter().parse(dateString, new ParsePosition(0));
		}
		return null;
	}

	//字符串转为date类型[超长时间格式](精确到毫秒)
	public static Date toMSDate(String dateString) {
		if ( !StringUtils.isEmpty(dateString) ) {
			return getLongestFormatter().parse(dateString, new ParsePosition(0));
		}
		return null;
	}

	//date类型转为字符串[长时间格式]
	public static String toDateString(Date date) {
		if ( date != null ) {
			getLongFormatter().setTimeZone(TimeZone.getTimeZone("GMT+8"));
			return getLongFormatter().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(getLongFormatter().format(date).split(" ")[1]);
			return getLongFormatter().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 && minutes >= 0.0 ) {
			Calendar cal = Calendar.getInstance();
			cal.setTime(date);
			cal.add(Calendar.MINUTE, minutes.intValue());// 24小时制
			return cal.getTime();
		}
		return null;
	}

	//计算某个日期过了指定分钟后的date时间
	public static Date minutesAfter(Date date, int minutes) {
		if ( date != null && minutes >= 0 ) {
			Calendar cal = Calendar.getInstance();
			cal.setTime(date);
			cal.add(Calendar.MINUTE, minutes);// 24小时制
			return cal.getTime();
		}
		return null;
	}

	//计算某个日期过了指定秒钟后的date时间
	public static Date secondsAfter(Date date, int seconds) {
		if ( date != null && seconds >= 0 ) {
			Calendar cal = Calendar.getInstance();
			cal.setTime(date);
			cal.add(Calendar.SECOND, seconds);// 24小时制
			return cal.getTime();
		}
		return null;
	}

	//计算某个日期过了指定秒钟后的date时间
	public static Date msAfter(Date date, int ms) {
		if ( date != null && ms >= 0 ) {
			Calendar cal = Calendar.getInstance();
			cal.setTime(date);
			cal.add(Calendar.MILLISECOND, ms);// 24小时制
			return cal.getTime();
		}
		return null;
	}

	//计算某个日期过了指定分钟后的date时间
	public static Date msAfter(Date date, Long ms) {
		if ( date != null && ms != null ) {
			Calendar cal = Calendar.getInstance();
			cal.setTime(date);
			cal.add(Calendar.MILLISECOND, ms.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 getDateBeforeDays(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 getDateAfterDays(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工具。因此这也是我为什么注释掉RestResponseEntityExceptionHandler 这个文件的原因之一。当然,这种情况也是有办法解决的,请继续跟踪下面的内容。


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 处理器配置类或者InterceptorConfig拦截器配置类也可以(本质上都是实现了WebMvcConfigurer接口类),addResourceHandlers 方法中给所需访问的资源文件地址放行。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值