1、拦截器
在完成强制登录这一功能时,后端程序需要根据Session来判断用户是否登录,但是实现方法是很麻烦的:1、需要修改每个接口的处理逻辑 2、需要修改每个接口的返回结果。 3、接口定义修改,前端代码也需要跟着修改。
有没有一种简单的方法能统一拦截所有的请求,并进行Session校验呢?
这就需要我们的拦截器出马了~~
快速上手拦截器
什么是拦截器
拦截器是Spring提供的核心功能之一,主要用来拦截用户请求,在指定方法前后,根据业务需要执行预先设定的代码。也就是说,允许开发人员提前预定义一些逻辑,在用户的请求响应前后执行,也可以在用户请求前阻止其执行。
在拦截器中,开发人员可以在应用程序中做一些通用性的操作,比如拦截前端发来的请求,判断Session中是否有登录用户的信息,如果有就放行,没有就进行拦截。
拦截器的使用主要分为两步:
1、定义拦截器
2、注册配置拦截器
自定义拦截器
自定义拦截器需要实现HandlerInterceptor接口,并重写方法。
@Slf4j
@Component
public class StringInterceptor implements HandlerInterceptor{
//请求执行前的逻辑
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("目标方法执行前执行");
return true;
}
//请求执行后的逻辑
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("目标方法执行后执行");
}
//视图渲染后的逻辑
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("视图渲染后执行");
}
}
- preHandle():目标方法执行前执行,返回true:继续执行后续操作;返回false:中断后续操作。
- postHandle():目标方法执行后执行
- afterCompletion():视图渲染完成后执行,最后执行(后端开发现在几乎不涉及视图了,暂不了解)
注册配置拦截器
注册配置拦截器需要实现WebMvcConfigurer接口并重写addInterceptors方法。
@Configuration
public class Webconfig implements WebMvcConfigurer {
//自定义拦截器对象
@Autowired
private StringInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
// addPathPatterns():设置拦截的请求路径,/**表示所有路径 excludePathPatterns()设置不拦截的路径
registry.addInterceptor(interceptor).
addPathPatterns("/**").
excludePathPatterns("/test/t2");
}
}
Controller测试类:
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/t1")
public void t1(){
log.info("t1执行中");
}
@RequestMapping("/t2")
public void t2(){
log.info("t2执行中");
}
@RequestMapping("/t3")
public void t3(){
log.info("t3执行中");
}
}
测试结果:
t1: t2:
t3:
可以看到拦截器没有拦截t2请求,并执行拦截器中重写的方法。
在拦截器中除了可以设置/**拦截所有资源外,还有一些常见拦截路径设置:
拦截器执行流程
正常调用顺序:
有了拦截器后:
1、添加拦截器后,执行Controller方法之前,请求会先被拦截住并执行preHandle()方法, 这个方法需要返回一个boolean类型的值。如果返回true,则表示放行,继续访问Controller中的方法。如果返回false,则不会放行,不会执行Controller中的方法。
2、controller当中的方法执行完毕后,再回过头来执行postHandle这个方法以及afterCompletion()方法,执行完毕之后,最终给浏览器响应数据。
2、统一返回类型
在开发环境中,我们的方法往往有许多不同的返回类型。如果返回的类型不同,前端就要使用各种不同的类型进行接收,这样会增加前端开发人员的工作量。SpringBoot为我们提供了统一返回类型的功能实现。
快速入门
统一数据返回类型需要使用@ControllerAdvice(标识控制器通知类,这里的通知并不是我们日常生活中的通知,而是某些功能的实现)并实现ResponseBodyAdvice接口。
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper mapper;
//什么请求需要处理 true——所有请求都处理 false——所有请求都不处理
@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 Result){
return body;
}
if(body instanceof String){
//如果是字符串,转成json字符串再返回,否则可能会产生类型转换异常
return mapper.writeValueAsString(Result.success(body));
}
return Result.success(body);
}
}
supports方法:判断是否要执行beforeBodyWrite方法。true为执行,flase不执行。提供该方法可以选择那些类或那些方法的response要进行处理,其他的不进行处理。
从returnType获取类名和方法名:
//获取执行的类 Class<?> declaringClass = returnType.getMethod().getDeclaringClass(); //获取执行的方法 Method method = returnType.getMethod();
beforeBodyWritr方法:对response方法进行具体操作处理
测试相关类及方法:
Controller:
@Slf4j
@RequestMapping("/book")
@RestController
public class BookController {
@Autowired
private BookSeverice bookSeverice;
//根据id查询图书
@RequestMapping("/queryBookById")
public BookInfo queryBookById(Integer bookId) {
log.info("根据id查询图书信息,id:" + bookId);
return bookSeverice.queryBookById(bookId);
}
}
Service:
@Service
public class BookSeverice {
@Autowired
private BookMapper mapper;
//根据id查询图书
public BookInfo queryBookById(Integer bookId) {
BookInfo bookInfo = mapper.queryById(bookId);
bookInfo.setStatusCN(BookStatus.getDescByCode(bookInfo.getStatus()).getDesc());
return bookInfo;
}
}
mapper:
@Select("select * from book_info where id = #{id}")
BookInfo queryById(Integer id);
Result:
@Data
public class Result<T> {
private ResultStatus code;//业务码 不是HTTP状态码 200——成功 -2 失败 -1未登录
private String errMsg;//错误信息 如果业务成功,errMsg为空
private T data;
public static<T> Result success(T data){
Result result = new Result<>();
result.setCode(ResultStatus.SUCCESS);
result.setData(data);
return result;
}
public static <T> Result noLogin(){
Result result = new Result<>();
result.setCode(ResultStatus.NOLOGIN);
result.setErrMsg("用户未登录");
return result;
}
public static Result fail(String msg){
Result result = new Result<>();
result.setCode(ResultStatus.FAIL);
result.setErrMsg(msg);
return result;
}
public static Result fail(String msg,ResultStatus resultStatus){
Result result = new Result<>();
result.setCode(resultStatus);
result.setErrMsg(msg);
return result;
}
}
业务码枚举类:
public enum ResultStatus {
SUCCESS(200),
FAIL(-1),
NOLOGIN(-2);
ResultStatus(int code) {
this.code = code;
}
private int code;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
Bookinfo:
@Data
public class BookInfo {
private Integer id;
private String bookName;
private String author;
private Integer count;
private BigDecimal price;
private String publish;
private Integer status;//1-正常 2-不可借阅
private String statusCN ;
private Date createTime;
private Date updateTime;
}
添加统一数据返回格式之前:
添加统一数据返回格式之后:
优点:
- 方便前端程序员更好地接收和解析后端数据接口返回地数据
- 降低前端程序员和后端程序员地沟通成本,按照某个格式实现就可以了,因为所有地接口都是这样返回地。
- 有利于项目统一数据的维护和修改
- 有利于后端技术部门的统一规范的标准指定,不会出现稀奇古怪的返回内容。
3、统一异常处理
统一异常处理使用的是@ControllerAdvice+@ExceptionHandler来实现的。@ControllerAdvice标识控制器通知类,@ExceptionHandler是异常处理器,两个结合表是当出现异常时执行某个通知,也就是执行某个方法事件。
代码如下:
@ResponseBody
@ControllerAdvice
@Slf4j
public class ExceptionAdvice {
/**
* 统一异常处理
* @param e
* @return
*/
@ExceptionHandler
public Result handleException(Exception e){
//建议打印日志
log.error("e: "+e);
return Result.fail("发生内部错误");
}
}
因为接口返回的是数据,所以需要加上@ResponseBody注解。
以上代码表示,如果代码出现Exception异常(包括它的子类),就返回一个Result对象,Result类代码:
@Data
public class Result<T> {
private ResultStatus code;//业务码 不是HTTP状态码 200——成功 -2 失败 -1未登录
private String errMsg;//错误信息 如果业务成功,errMsg为空
private T data;
public static<T> Result success(T data){
Result result = new Result<>();
result.setCode(ResultStatus.SUCCESS);
result.setData(data);
return result;
}
public static <T> Result noLogin(){
Result result = new Result<>();
result.setCode(ResultStatus.NOLOGIN);
result.setErrMsg("用户未登录");
return result;
}
public static Result fail(String msg){
Result result = new Result<>();
result.setCode(ResultStatus.FAIL);
result.setErrMsg(msg);
return result;
}
public static Result fail(String msg,ResultStatus resultStatus){
Result result = new Result<>();
result.setCode(resultStatus);
result.setErrMsg(msg);
return result;
}
}
我们还可以针对不同的异常返回不同的结果:
@ResponseBody
@ControllerAdvice
@Slf4j
public class ExceptionAdvice {
/**
* 统一异常处理
* @param e
* @return
*/
@ExceptionHandler
public Result handleException(Exception e){
//建议打印日志
log.error("e: "+e);
return Result.fail("发生内部错误");
}
@ExceptionHandler
public Object handler(NullPointerException e) {
return Result.fail("发⽣NullPointerException:"+e.getMessage());
}
@ExceptionHandler
public Object handler(ArithmeticException e) {
return Result.fail("发⽣ArithmeticException:"+e.getMessage());
}
}
模拟异常的代码
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1(){
return "string";
}
@RequestMapping("/t2")
public Integer t2(){
int a = 10/0;//抛出算数异常
return 1;
}
@RequestMapping("/t3")
public Boolean t3(){
String a = null;
System.out.println(a.length());//抛出空指针异常
return true;
}
}
t2:
t3: