一. Spring AOP 用户同一登录验证问题
- 登录、注册页面不拦截,其他页面都拦截
- 当登录成功写入 session 之后,拦截的页面可正常访问
1.1 自定义拦截器
@Configuration
public class LoginAspect implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("do LoginAspect");
HttpSession session=request.getSession(false);
if(session==null||session.getAttribute("")==""){
response.setContentType("text/html;charset=utf8");
response.getWriter().write("当前未登录");
return false;
}
return true;
}
}
1.2 将自定义的拦截器加入到系统配置
@Configuration
public class WebMVC implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginAspect())
//拦截所有接口
.addPathPatterns("/**")
//排除接口
.excludePathPatterns("/user/login.html")
.excludePathPatterns("/user/reg.html")
//排除静态资源中image包下的所有资源
.excludePathPatterns("/image/**");
}
}
1.3 拦截器的调用顺序与实现原理
调用顺序:
正常情况下,程序会在调用 Controller 之前进行相应的业务处理(我们在切面中定义的事务),业务通过后,才会调用Controller 层,然后就是Controller -> Serrvice -> Mapper -> 数据库
实现原理:
所有的 Controller 执行都会通过一个调度器 DispatcherServlet 来实现,这一点可以从 Spring Boot 控制台的打印信息看出:
而所有方法都会执行 DispatcherServlet 中的 doDispatch 调度方法:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}
//调用预处理
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
//执行 Controller 中的业务
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new ServletException("Handler dispatch failed: " + var21, var21);
}
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
triggerAfterCompletion(processedRequest, response, mappedHandler, new ServletException("Handler processing failed: " + var23, var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
从上述源码中可以看出在开始执行 Controller 之前,会先调用预处理方法 applyPreHandle, 而 applyPreHandle 方法的实现源码如下:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
//获取项目中的拦截器
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
return true;
}
从上述源码中可以看出,在 applyHandle 中会获取所有的拦截器 HandlerInterceptor 并执行拦截器中的 preHandle 方法,此时就和我们定义的拦截器对应上了。
@Configuration
public class LoginAspect implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("do LoginAspect");
HttpSession session=request.getSession(false);
if(session==null||session.getAttribute("")==""){
response.setContentType("text/html;charset=utf8");
response.getWriter().write("当前未登录");
return false;
}
return true;
}
}
当我们的拦截器返回false后,applyHandle 也会返回false ,此时 DispatcherServlet 就会直接返回,不会再执行我们 Controller 层的业务代码了。
二. 统一异常处理
统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的, @ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件.
出现异常统一返回 json 格式报错信息:
定义返回报错信息格式:
@Data
public class ResultAjax {
private int code;
private String mes;
private String data;
}
实现异常通知
@ControllerAdvice
@ResponseBody
public class ExeceptionAdvice {
@ExceptionHandler(Exception.class)
public ResultAjax handler(Exception e){
ResultAjax resultAjax=new ResultAjax();
resultAjax.setCode(-1);
resultAjax.setMes(e.getMessage());
return resultAjax;
}
}
三. 统一数据返回格式
3.1 为什么需要统一数据返回格式?
- 方便前端程序员更好的接收和解析后端数据接口返回的数据.
- 降低前后端程序员的沟通成本
- 有利于项目统一数据的维护和修改
- 有利于后端技术部门的统一规范的标准制定,不会出现稀奇古怪的返回内容
3.2 统一数据返回格式的实现
统一的数据返回格式可以使用 @Controller + ResponseBodyAdvice 的方式实现:
定义返回数据格式:
@Data
public class ResultAjax {
private int code;
private String mes;
private Object data;
public static ResultAjax succ(String mes){
ResultAjax resultAjax=new ResultAjax();
resultAjax.setCode(200);
resultAjax.setMes(mes);
return resultAjax;
}
public static ResultAjax succ(Object data){
ResultAjax resultAjax=new ResultAjax();
resultAjax.setCode(200);
resultAjax.setMes("");
resultAjax.setData(data);
return resultAjax;
}
public static ResultAjax succ(String mes,Object data){
ResultAjax resultAjax=new ResultAjax();
resultAjax.setCode(200);
resultAjax.setMes(mes);
resultAjax.setData(data);
return resultAjax;
}
public static ResultAjax succ(int code,String mes,Object data){
ResultAjax resultAjax=new ResultAjax();
resultAjax.setCode(code);
resultAjax.setMes(mes);
resultAjax.setData(data);
return resultAjax;
}
public static ResultAjax fail(String mes){
ResultAjax resultAjax=new ResultAjax();
resultAjax.setCode(-1);
resultAjax.setMes(mes);
return resultAjax;
}
public static ResultAjax fail(String mes,Object data){
ResultAjax resultAjax=new ResultAjax();
resultAjax.setCode(-1);
resultAjax.setMes(mes);
resultAjax.setData(data);
return resultAjax;
}
}
对返回数据类型拦截进行统一判断和强制转换:
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper mapper;
/*
* 返回 ture 才会调用beforeBodyWrite 方法
* 否则就不会调用
* */
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
ResultAjax resultAjax=new ResultAjax();
//若返回数据类型就是 json 格式,直接返回
if(body instanceof ResultAjax){
return body;
}
//针对 String 类型的返回值需要额为处理
if(body instanceof String){
try {
//将ResultAjax 转换为 json 格式字符串
return mapper.writeValueAsString(resultAjax.succ(body));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return resultAjax.succ(body);
}
}
为什么需要对String 类型的返回值进行额外处理?
SpringMVC 在进行初始化的时候,默认会注册⼀些⾃带的 HttpMessageConverter,MessageConverter 意思是消息转换器。
1)ByteArrayHttpMessageConverter
2)StringHttpMessageConverter
3)SourceHttpMessageConverter
4)AllEncompassingFormHttpMessageConverter当我们返回数据的时候,框架会自动选择合适的消息转换器,这个选择转换器的时机是在我们对返回值进行包装之前就选定了。当我们返回的数据类型为String时,框架就会自动选择StringHttpMessageConverter进行处理,但是由于我们对返回类型进行了封装,所以后续调用StringHttpMessageConverter进行包装的时候类型就不是String的了,所以就出现了类型转换错误,因此我们需要将返回的数据转换为 json 格式的字符串,而不是直接返回对象。