SpringBoot 统一功能处理
一.拦截器
什么是拦截器,以及为什么要拦截器,首先我们后端不是随意的人,也不是垃圾桶,不是前端发来什么请求都直接接收并传给方法函数,应该有一个东西来拦截并审核,符合要求的才进去,不符合要求的就直接拦截在外面,生活中就比如学校的保安,是学生或老师等相关人员才能入校,非相关人员不得入校,网页上就比如想访问某一个商品详情页,登录账号了才能进行访问,未登录则需先登录才能继续访问,这些都是拦截器的功能

1.拦截器的基本使用
分为两步,定义拦截器,注册配置拦截器
定义拦截器:要求实现HandlerInterceptor接口,并重写所有方法
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle目标方法执行前...");
//true 放行, false 拦截
return true;
}
@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视图渲染完毕后执行....");
}
}
其中HandlerInterceptor接口有三个方法需要我们重写,分别是preHandle,postHandle,afterCompletion,其中afterCompletion涉及视图,与后端几乎无关,可以不用管,主要解决前两个方法
preHandle的返回值类型是布尔类型,其中如果返回为true就表示符合要求,可以通行,如果返回为false则表示不符合要求,就直接被拦截了,根本进不去后面
以登录为例,就可以把拦截器写成如下
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle目标方法执行前...");
//true 放行, false 拦截
HttpSession session = request.getSession(false);
if (!checkUser(session)){
response.setContentType("text/html;charset=utf-8");
response.setStatus(401);
String msg = "用户未登录";
response.getOutputStream().write(msg.getBytes("UTF-8"));
return 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;
}
这样就实现了如果用户处于未登录状态,则不能进行访问,登录状态才可以
但是拦截器也不是所有请求都进行拦截,就好比学校里除了大门口有保安,总不能楼道口,教室门,办公室门,厕所门都有保安吧,那这一天不是被保安进行拦截,就是在去被保安拦截的路上,根本不能正常干事了
因此我们就需要执行第二步,注册配置拦截器,即将保安放在哪里
注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptors方法
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**");
}
}
因为刚刚LoginInterceptor是通过@Component交给Spring管理的,所以可以直接注入,然后addInterceptors方法就是用来注册配置拦截器,其中InterceptorRegistry是注册拦截器,addInterceptor方法里的参数就是我们刚刚定义的拦截器,addPathPatterns就是配置拦截器的拦截路径,/**表示拦截所有请求
其中拦截路径的使用
/* :一级路径,比如能匹配/user,/book,但不能匹配/user/login
/** :任意级路径,能匹配所有路径
/book/* :/book下的一级路径,比如能匹配/book/addBook,不能匹配/book/addBook/1
/book/** :/book下的任意级路径,比如能匹配/book/addBook,/book/addBook/1,不能匹配/user
而既可以用addPathPatterns方法来指定要拦截哪些请求,也可以用excludePathPatterns方法来指定不拦截哪些请求,两个方法可以一起使用
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/user/login");
}
2.原码翻译简介
首先当Tomcat启动后,会有一个核心的类DispatcherServlet,它会控制程序的执行顺序,其中调度方法为doDispatch,此时就会检查是否有拦截器,有的话就先执行拦截器的代码,然后再将请求转给Controller
而DispatcherServlet初始化方法init()在其父类HttpServletBean实现的,而在父类中的init()方法又调用了initServletBean(),这个方法是在FrameworkServlet类中实现的,目的是建立WebApplicationContext容器(也叫上下文),并加载SpringMVC配置文件中定义的Bean到该容器中,最后将该容器添加到ServletContext中
3.适配器模式
拦截器实现的原码中,就使用了适配器模式
适配器模式其实就是让两个接口不兼容的接口通过适配器来进行转化
在生活中的例子就是转换插头,墙壁上只有一个两孔插座,怎么给一个三孔插头的设备充电呢,那就需要将转换插头插在插座上,然后选取转换插头上适配的孔,这样就实现了插头和插座不适配的情况下进行连接
在代码中,slf4j就使用了适配器模式,slf4j提供了一系列的api,但其底层调用的却是log4j或者logback来打印日志,但我们作为调用者,就只需要调用slf4j的api就行了
适配器模式应用场景主要在版本更新,即在原有代码保持不变的基础上,通过对现在运行的代码进行改造,使得原有代码能够实现新的功能
因此适配器模式可以看作是一种“补偿模式”,用来补救设计上的缺陷,是后期的无奈之举,因此在设计初期,我们就需要协调规避接口不兼容的问题,就不需要使用适配器模式
4.统一数据返回格式
使用@ControllerAdvice和ResponseBodyAdvice就能实现
@ControllerAdvice加在类上,表示该类是控制器通知类
创建ResponseAdvice这个类,并实现ResponseBodyAdvice接口,并添加@ControllerAdvice注解
其中需要重写两个方法,一个是supports,一个是beforeBodyWrite
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
//false: 不处理 true: 处理
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof String){
return objectMapper.writeValueAsString(Result.success(body));
}
if (body instanceof Result) {
return body;
}
return Result.success(body);
}
}
其中supports方法的返回值类型为布尔类型,如果返回是true,就beforeBodyWrite这个方法,而返回false,就不使用beforeBodyWrite这个方法
而beforeBodyWrite这个方法就实现的是统一数据返回格式的内容,上面代码的内容就是我们统一返回类型为json对象类型,而其中需要特殊处理的是String类型,因为它会被直接序列化为字符串,而不是转为json对象,因此需要用objectMapper的writeValueAsString将该字符串转为json字符串类型,并用Lombok中的@SneakyThrows注解来捕获可能抛出的异常
如果细心看上面代码的话可以发现,明明这个类没有使用spring管理的五大注解,就能使用@Autowired自动注入Objectmapper呢,其实在@ControllerAdvice注解的原码中,可以看到@ControllerAdvice是派生于@Component组件的,因此也生效
这样就实现了统一返回结果格式,这样前后端就降低了沟通成本,有利于项目统一数据的维护和修改
5.统一异常处理
使用的是@ControllerAdvice和@ExceptionHandler实现的
@ControllerAdvice不陌生了,就是统一数据返回格式的使用过的那个,用于表示控制器通知类,而@ExceptionHandler则是异常处理器,加在方法上
作用就是当出现异常时,根据异常的类型,执行对应的方法
@Slf4j
@ControllerAdvice
@ResponseBody
@RestControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler
public Result handler(Exception e){
log.error("发生异常, e:", e);
return Result.fail("内部错误, 请联系管理员");
}
@ExceptionHandler
public Result handler(NullPointerException e){
log.error("发生异常, e:", e);
return Result.fail("发生空指针异常, 请联系管理员");
}
@ExceptionHandler
public Result handler(IndexOutOfBoundsException e){
log.error("发生异常, e:", e);
return Result.fail("数组越界异常, 请联系管理员");
}
这样就根据不同的异常类型,执行不同的方法
@ExceptionHandler(Exception.class)
public Result handler(Exception e){
log.error("发生异常, e:", e);
return Result.fail("内部错误, 请联系管理员");
}
@ExceptionHandler(NullPointerException.class)
public Result handler2(Exception e){
log.error("发生异常, e:", e);
return Result.fail("发生空指针异常, 请联系管理员");
}
@ExceptionHandler(IndexOutOfBoundsException.class)
public Result handler3(Exception e){
log.error("发生异常, e:", e);
return Result.fail("数组越界异常, 请联系管理员");
}
也可以将代码写成这样,将要处理的异常类作为@ExceptionHandler的属性
要记得异常的捕获是先去匹配最具体的异常类,如果最后没有找到匹配的异常类,才会匹配父类异常,所以虽然Exception是最开始的,但是也是先把下面的子类异常匹配完之后才会匹配Exception
更多推荐
所有评论(0)