日升时奋斗,日落时自省
目录
1、Spring Boot统一处理模块
1.1、拦截器的实现原理
1.2、用户统一登录验证的问题
1.3、拦截器源码分析
2、数据格式统一处理
2.1、优点
2.2、统一数据返回格式的实现
2.3、String类型格式处理
2.3.1、使用ObjectMapper类
2.3.2、删除StringHttpMessageConverter操作
3、统一异常处理
3.1、空指针异常(事例)
3.2、所有异常处理
<1>统一用户登录权限验证
<2>统一数据格式返回
<3>统一异常处理
用户登录我们应该写过不少,即便没有写过太多,也肯定见过不少,页面登录过后是需要验证你登录了没有(这个可能看的不明显),未登录的时候,你点击那个页面或者操作都需要都会提示你进行登录,这就是因为登录权限验证没有通过(难道每个页面都要写一下吗)
每个方法中都有相同的用户登录验证权限,它也有很大的缺点:
<1>每个方法中都要单独写用户登录验证的方法,即使封装成公共方法,也一样要传参调用和在方法中进行判断
<2>添加控制器越多,调用用户登录验证的方法也越多,这样就增加了后期的修改成本和维护成本
<3>用户登录验证的方法要和很多业务挂钩,但是每个方法都要写一遍,所以提供了一个公共的AOP方法来进行统一的用户权限验证
这些判断是是什么时候处理呢,程序开始跑目标方法就接收不了(已经到控制层了),需要在控制层之前就进行处理,所以拦截器是在控制层之前的
<1>未使用拦截器之前的程序流程
<2>使用拦截器后的程序
Spring中的拦截器也通过动态代理和环绕通知的思想实现(AOP思想)
用户在调用控制层时经过拦截器的预处理是怎样进行的
如何解决用户统一登录验证的问题,使用拦截器去拦截进行验证,这个拦截就是使用的AOP思想,下面我们来演示一下登录拦截的操作:
拦截器是一个工具,所以我们就写在common层面,我们写了登录的拦截器类:LoginInterceptor
实现了:HandlerInterceptor类 重写里面的 preHandle方法
HandlerInterceptor翻译过来就是处理程序拦截器
鼠标右击->generate->override methods
<1>拦截器需要先拿到session,判断一下session是否是存在的
<2>session存在的话,直接返回true 表示当前是登录状态
<3>如果session不存在的话,返回一个统一的信息给前端,页面也就不会直接报错了(前端会处理)
注:拦截器的类也设置@Component注解 是为了和Spring Boot一起启动,此处只是指定了拦截规则,但是还没有真正的进行拦截
@Component
public class LoginInterceptor implements HandlerInterceptor {
// 调用目标方法 之前执行的方法
//此处返回的是一个boolean 类型的值
//如果返回是 true说明 拦截器验证成功 可以执行的后续目标方法
// 如果返回的是 false 表示拦截器 验证失败的, 不能再进行向下访问
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//用户登陆 拦截 判断业务
HttpSession session=request.getSession();
if(session!=null&session.getAttribute("session_userinfo")!=null){
//如果session存在 就直接返回true
return true;
}
//设置响应给页面的 格式 和 字符集 当然这里可以使用setCharacterEncoding("utf8"); 单独来设置
response.setContentType("application/json;charset=utf8");
//只要是登录验证为未登录 ,就返回一个数据格式 让前端判断
response.getWriter().println("{\"code\":-1,\"msg\":\"登陆失败\",\"data\":\"\"}");
return false;
}
}
设置注册,进行拦截,拦截操作我们这里放在config层 创建一个拦截类MyConfig实现接口WebMvcConfigurer重写其中的方法addInterceptors方法
操作:鼠标右击->generate->override methods
@Configuration
public class MyConfig implements WebMvcConfigurer {
//进行对象注入 将登录的拦截器定制的规则放到注册中
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)//添加拦截规则
.addPathPatterns("/**")//这里会拦截所有的controller层的类 /**表示所有路径的类
.excludePathPatterns("/image/**")//例如 要加载的图片是不能进行拦截的吧,所以可以直接把image下的所有图片进行不拦截操作
.excludePathPatterns("/user/index");//但是不会所有的类都要拦截,不需要拦截的去掉拦截 即可
}
}
下面我们在controller层 ,也一个操作类进行操作,但是我们不设置session所以,按理论来说应该是要拦截的
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public String login(){
return "登录成功";
}
@RequestMapping("/index")
public String index(){
return "登录成功";
}
@RequestMapping("/reg")
public String reg(){
return "登录成功";
}
}
运行效果:
将项目启动起来,然后去放问UserController类 ,我们只做了index不拦截处理,其他类都是进行拦截的,理论上我们访问其他路由的时候也应该显示是同一的格式,其中提示“登录失败”
注:这里没有体现通过拦截器的情况,这里针对登录是一个权限验证,所以需要和对比的是session值,如果session值是一样的那就证明已经登录,可以继续执行后面的操作。
所有的Controller执行都会通过的一个调度器DispatcherServlet来实现,何以见得,前面演示了一个拦截器,控制台打印信息就能看到调度器被调用的信息
所有方法都回执行DispatcherServlet中doDispatch的调度方法
doDispatch的调度方法中就有这么一个if语句就是判断使用拦截器
从applyPreHandle这个方法点进去(ctrl+鼠标左键)
<1>方便前端人员更好的接收和解析后端数据接口返回的数据
<2>提高可读性:使用统一的数据格式可以使代码更加易于阅读和理解。程序员可以更轻松地了解代码的功能和目的,从而更快地解决问题。
<3>减少错误:使用统一的数据格式可以减少错误发生的可能性。如果数据格式不一致,可能会导致程序出现错误或崩溃,这会影响到整个系统的稳定性和可靠性。
<4>方便维护:当数据格式统一后,修改和更新数据变得更加容易。程序员可以更容易地添加新功能或更改现有功能,而不会对整个系统造成影响。
<5>提高效率:使用统一的数据格式可以提高数据的处理速度和效率。由于数据格式相同,所以可以更快地读取和写入数据,从而加快程序的执行速度。
涉及到注解@ControllerAdvice注解+@ResponseBodyAdvice注解的方式实现
<1>@ControllerAdvice注解:和异常处理是同一个注解表示控制器通知类
<2>@ResponseBodyAdvice注解:用于全局处理Controller方法的返回值,用于全局处理Controller方法的返回值
common层创建类继承ResponseBodyAdvice类完成数据格式的同一处理
这里需要重写两个方法supports方法和beforeBodyWrite方法
supports方法是进行拦截的,这里需要将方法返回一个true 否则beforeBodyWrite方法就执行不了
beforeBodyWrite方法就可以写数据的返回格式了,所有类的返回类型都会经过这里进行处理后再返回给前端
@ControllerAdvice //控制器通知类
public class ResponseAdvice implements ResponseBodyAdvice {
/*
* 要想执行 beforeBodyWrite方法 需要supports方法返回true 才能继续执行 重写返回结果
* */
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
/*
* 返回数据之前执行数据重写
* body 就是原返回值
* 其他参数跟咱没有关系
* */
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//当然了也不是每一个返回的数据都需要进行处理
//如果是 JSON格式就直接返回就行
if(body instanceof HashMap){
return body;
}
//如果不是 就进行格式转换
//重写返回结果 让返回统一数据格式
HashMap result=new HashMap<>();
result.put("code",200);
result.put("data",body);
result.put("msg","");
return result;
}
}
现在就可以写点类进行测试一下(controller层随便写个类进行路由访问):
@RestController //复合注解 为了返回一个字符串
@RequestMapping("/user")
public class UserController {
@RequestMapping("/reg")
public int reg(){
return 1;
}
}
运行结果(我们在目标方法中返回的是1):
注:经过数据格式统一处理后变为JSON格式,
格式转换是有这么一个过程的,先从目标方法返回一个int类型,经过判定之后不是JSON格式,进行int类型转换为hash类型,再通过hash类型转换为JSON格式
特别注意:到这里数据格式转换还没有结束,有一个坑,就是String类型不能被转换为JSON格式需要特殊处理
String类型格式转换为JSON格式数据是会报错的,我们需要单独做点处理
这里演示一个String类型的转换
@RequestMapping("/sayhi")
public String sayhi(){
return "say hi";
}
访问路由结果(会报错):
该方法的处理进制
返回执行流程:
<1>方法返回的是String
<2>统一数据返回之前处理------>String Convert HashMap
<3>将HashMap转换成application/json字符串给前端(接口)
返回流程就是以上三步,问题就在第三步需要判断原Body的类型
<1>如果是String类型,走StringHttpMessageConverter进行类型转换(实则转换不了)
<2>不是String类型,走HttpMessageConverter进行类型转换
解决问题:
进行判定是否是String类型,如果是这里使用lombok提供的ObjectMapper类将String转为JSON格式(使用对象注入的方式)
还是在我们刚刚创建的ResponseAdvice类中写
/*
* lombok提供的类转化为JSON格式
* */
@Autowired
private ObjectMapper objectMapper;
这里在写writeValueAsString方法的时候会报错,需要抛出一个注解@SneakyThrows
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//当然了也不是每一个返回的数据都需要进行处理
//如果是 JSON格式就直接返回就行
if(body instanceof HashMap){
return body;
}
//如果不是 就进行格式转换
//重写返回结果 让返回统一数据格式
HashMap result=new HashMap<>();
result.put("code",200);
result.put("data",body);
result.put("msg","");
//进行判定body的类型是不是属于String
if(body instanceof String){
//如果属于那就进行转换下面这个方法就是进行转换的
return objectMapper.writeValueAsString(result);
}
return result;
}
String类型访问结果(本次访问就没事了):
如果body类型是String类型的也不需要StringHttpMessageConverter进行操作,移除以后,会有其他类进行管理
@Configuration
public class MyConfig implements WebMvcConfigurer {
/*
* 移除StringHttpMessageConverter
* */
@Override
public void configureMessageConverters(List> converters) {
converters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
}
}
注:和第一种方法效果是一样的
String类型访问结果:
为啥要有一个统一异常处理呢,其实是个保底处理措施,当程序在运行过程中出现了bug就会将错误进行统一的格式进行处理与正常处理无异,只不过前端可以照常接收这种数据,不至于报错一类,前端需要处理一类
统一异常处理使用的是@ControllerAdvice 注解 和 @ExceptionHandler注解来实现的
<1>@ControllerAdvice注解:表示控制器的通知类,扫描所有的类,用于全局处理Controller方法返回值的注解。它可以对Controller方法的返回值进行拦截和处理
<2>@ExceptionHandler注解:表示异常处理器,该注解可以设置对应的异常并且写一个方法进行处理当前异常返回数据也好,其他操作也罢,就是只要报错了并且是对应的错误,就必须走这个方法进行处理
这里我们采用哈希表来存储错误信息和状态码(当前这种方法不好),哈希表最终会转为JSON的数据格式进行显示出来
@ControllerAdvice //通知类 进行扫描所有的类 也就是controller层的操控类
@ResponseBody //返回 JSON格式 的 数据
public class MyExceptionAdvice {
/*
* 空指针异常 进行处理
* */
@ExceptionHandler(NullPointerException.class)
public HashMap doNullPointerException(NullPointerException e){
//这里只是假设的一个统一返回格式 以哈希的方式进行返回
HashMap result=new HashMap<>();
result.put("code",-250); //设置一个状态
result.put("msg","空指针:"+e.getMessage()); //设置错误信息
result.put("data",null); //因为错误所以返回数据为空 我们这里就设置为空
return result;
}
}
注:@ExceptionHandler中参数就是异常类,需要设置那个异常就放那个异常的类(这里作为演示就放空指针进行处理)
下面在controller层写一个空指针异常的类UserController
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public int login(){
Object object=null;
object.hashCode();//null是不会有哈希值的 会报空指针异常
return 1;
}
}
运行后看看结果(返回的结果还是在情理之中的):
注:此时是有个缺陷,如果不是空指针异常呢,要再写一个异常处理,这么多异常呢,返回反正都是统一的错误信息,就需要将所有异常都处理了(前端知道有错误信息就行)
其实剩下的什么都没有变,只是@ExceptionHandler注解参数变成了所有异常都继承的最终异常
/*
* 处理所有异常
* */
@ExceptionHandler(Exception.class)
public HashMap doException(Exception e){
//这里只是假设的一个统一返回格式 以哈希的方式进行返回
HashMap result=new HashMap<>();
result.put("code",-250); //设置一个状态
result.put("msg",e.getMessage()); //设置错误信息
result.put("data",null); //因为错误所以返回数据为空 我们这里就设置为空
return result;
}
我们当前写了一个空指针异常和所有异常的父类,现在演示一个分母不为0的异常处理,看是否能被处理
@RequestMapping("/index")
public int number(){
int num=10/0;
return 1;
}
运行结果(错误信息打印也确实是by zero):