一、拦截器
在前面学习的小项目(如图书管理系统),如果要完成强制登录,需要:
1.修改每个接口的处理逻辑(如什么情况下为未登录,什么情况下为已登录,根据不同情况返回不同结果);
2.修改每个接口的返回结果;
3.修改对应的前端代码。
可以看到,很多接口都涉及到一段相同的处理逻辑,都要写一段相同的代码,非常麻烦,对此Spring给我们提供了一种功能——拦截器
1.1 拦截器入门
拦截器是Spring提供的核心功能之一,主要用来拦截用户的请求,在指定方法前后,根据也需要执行预先设定的代码(如判断用户是否登录的代码)。
拦截器的使用大体分为两步:
<1>定义拦截器(实现HandlerInterceptor接口)
<2>注册配置拦截器(实现WebMvcConfigurer接口)
一、定义拦截器
定义一个类实现HandlerInterceptor接口即可:
@Component @Slf4j public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("preHandle目标方法执行前"); return true;//true拦截 false不拦截 } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info("postHandle目标方法执行后执行"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("afterCompletion视图渲染完毕后执行");//现在Spring已经不用来处理视图了,因此这个方法无需理会 } }
二、注册配置拦截器
定义一个类实现WedMvcConfiguer接口即可:
@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/book/**");//拦截book下的任意路径 // .excludePathPatterns("/user/login");//不拦截的路径 } }
实现WebMvcConfigurer接口后,通过addIntercepter方法将定义好的拦截器添加进来,再通过addPathPatterns方法指定拦截哪些请求,通过excludePathPatterns方法指定不拦截哪些请求。
下面是一些常见的拦截路径设置:
拦截路径 含义 举例 /* 一级路径 /user、/login /** 任意级路径 /user、/user/login、/book、/bookList /book/* /book路径下的一级路径 /book/selectById、/user/login /book/** /book路径下的任意级路径 /book、/book/bookList、/book/bookList/2
1.2 使用拦截器实现强制登录
要实现强制登录,只需要重写preHandle方法,加上一些登录逻辑判断即可,如:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle目标方法执行前");
if(!checkUser(request.getSession(false))){
response.setContentType("text/html;charset=utf-8");
response.setStatus(401);
String msg = "用户未登录";
response.getOutputStream().write(msg.getBytes("UTF-8"));
return false;
}
//true:放行 false:拦截
return true;
}
public boolean checkUser(HttpSession session){
if(session == null || session.getAttribute(Constants.SESSION_USER_KEY)==null){
//用户未登录
log.warn("用户未登录");
return false;
}
UserInfo userInfo = (UserInfo)session.getAttribute(Constants.SESSION_USER_KEY);
if(userInfo == null || userInfo.getId()<=0){
//用户未登录
log.warn("用户未登录");
return false;
}
log.info("用户已登录");
return true;
}
接下来,不通过登录页面登录,直接进入图书列表页面:
可以看到,访问被成功拦截
二、统一结果返回
在项目的各种接口中,会涉及到各种不同的返回结果,如果需要对这些返回结果进一步封装,加上code(判断用户是否登录)和errMsg(错误信息),岂不是需要对所有接口的返回结果手动在封装一层,这样非常麻烦,因此Spring提供了统一结果返回的功能。
如需要将所有Controller返回结果封装为下面的Result:
@Data
public class Result<T> {
//用于验证用户是否登录
private ResultCodeEnum code;//code = -1 用户未登录、code = -2 后端出错,code = 200 正常
private String errMsg;//告知用户错误信息
private T data;//将Controller层返回结果封装一层
public static <T>Result success(T data){
Result result = new Result();
result.setCode(ResultCodeEnum.SUCCESS);
result.setErrMsg("");
result.setData(data);
return result;
}
public static Result fail(String errMsg){
Result result = new Result();
result.setCode(ResultCodeEnum.FAIL);
result.setErrMsg(errMsg);
return result;
}
public static <T>Result fail(String errMsg,T data){
Result result = new Result();
result.setCode(ResultCodeEnum.FAIL);
result.setErrMsg(errMsg);
result.setData(data);
return result;
}
public static Result unLogin(){
Result result = new Result();
result.setCode(ResultCodeEnum.UNLOGIN);
result.setErrMsg("用户未登录");
return result;
}
}
使用Spring的统一结果返回主要有以下步骤:
1.实现ResponseBodyAdvice接口,并使用注解@ControllerAdvice(通过实现 ResponseBodyAdvice
接口,@ControllerAdvice
可以全局拦截控制器的返回值,并统一封装为固定格式)
2.重写supports方法和beforeBodyWriter方法(supports的返回值为true表示执行beforeBodyWriter方法,反之不执行)
接下里使用Spring的统一结果返回对图书管理系统中的返回结果进行封装(分装为上方的Result):
@ControllerAdvice public class ResponseAdvice implements ResponseBodyAdvice { @Autowired private ObjectMapper objectMapper; @Override public boolean supports(MethodParameter returnType, Class converterType) { return true; } @SneakyThrows @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if(body instanceof String){ //当返回结果为String时,需要额外处理 return objectMapper.writeValueAsString(Result.success(body)); } if(body instanceof Result){ return body;//如果返回结果已经是Result类型,直接返回 } return Result.success(body); } }
接下来在postman进行测试:
可以看到,成功封装为Result
但是,需要注意的一点是,在上面的代码中,对于类型为String的返回值,需要进行额理,主要是因为Spring的
HttpMessageConverter
机制对String类型和其他对象类型的处理方式不同。
三、统一异常处理
统一异常处理是使用注解@ControllerAdvice和@ExceptionHandler来实现的,@ControllerAdvice表示控制器通知类,@ExceptionHandler表示异常处理器,两者结合表示出现异常是执行某个通知
实现代码如下:
@Slf4j
@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {
@ExceptionHandler//这个注解也可以指定捕获的异常类型
public Result handler(Exception e){//也可以接收具体的异常
log.error("发生异常,e:",e);
//如果代码出现Exception异常(包括Exception的⼦类),就返回⼀个Result的对象
return Result.fail("内部错误,请联系管理员");
}
}
上述代码表示,只要出现Exception异常(以及它的子类),就会返回一个Result对象,下面写一个接口,进行测试,代码如下:
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/t1")
public Integer test(){
int a = 10/0;
return 1;
}
}
测试结果: