• Java 之Interceptor--Servlet 中action 过不去的坎


    一、背景

    需求是这样的:在表格中添加一个创建时间栏目,显示每条数据详细的创建时间,如图

    本来这是一个很简单的需求,只需要在bootstrap 的column 中添加创建时间字段就可以,当然我也这么实现了,本以为没什么问题,但是测试妹子说删除功能不行了???不会吧,我就加一个字段而已,怎么删除不行了呢,之前还正常的啊。带着问题,我开始跟踪代码。

     1. 首先看到前台页面的删除功能的ajax 请求,没发现有什么问题

    //删除多个文章
    	$(".rightDelete").click(function(){
    		var rows = $("#articleListTable").bootstrapTable("getSelections");
    		if(rows!=""){
    			$.ajax({
    				url:xxx/delete.do",
    				type:'post',
    				dataType:'json',
    				data:JSON.stringify(rows),
    				contentType:'application/json',
    				success:function(msg) {
    					if (msg.result) {
    					} else {
    					}
    				}
    			});
    		}else{
    	    	<@ms.notify msg="请选择文章!" type="warning"/>
    	    }
    	});
    

    2. 接着看后台方法,也没有说明问题,和之前一样,唯一不同的是,当我在delete 这个方法打了断点之后,请求并没有进入到delete 这个方法中。

    @RequestMapping("/delete")
    	@RequiresPermissions("article:del")
    	public void delete(@RequestBody List<ArticleEntity> article, HttpServletRequest request, HttpServletResponse response) {
    		int appId = this.getAppId(request);
    		int[] ids = new int[article.size()];
    		//循环获取id数据
    		for(int i=0;i<article.size();i++){
    			ids[i] = article.get(i).getArticleID();
    		}
    		
    		if (ids.length == 0 || ids == null) {
    			this.outJson(response, ModelCode.CMS_ARTICLE, false, "", this.redirectBack(request, false));
    			return;
    		}
    		// 删除多个帖子
    		articleBiz.deleteBasic(ids);
    		this.outJson(response, ModelCode.CMS_ARTICLE, true, "", this.redirectBack(request, false));
    	}
    

    3. 进入调试模式,前台ajax 请求没有问题,后台没有进入到/delete 方法,但是这时候控制台的一条日志吸引了我的注意:

    [xxx 2018-06-05 08:46:39,055](DEBUG) - xxx.xxx.basic.interceptor.ActionInterceptor - (BaseInterceptor.java:259) url:/xxx/error/400.do 

    从这条信息可以看到一个陌生的类--ActionInterceptor ,这个类翻译为中文的大概意思就是动作拦截,并且拦截的结果是error/400.do。看到这里大概明白了一些,在我每次发起请求的时候是不是都会先经过这个这个拦截器,在拦截器做一些逻辑控制,如果请求的参数不符合要求则跳转到对应的错误页面,例如这里的400.do 页面。不多说,试一下就知道了呗,找到ActionInterceptor 入口的方法,发起请求,果然每个.do 方法都会被这个类拦截然后做逻辑判断,如果请求方法或者参数有错则会跳转到error/400.do,正常则直接跳转到请求的url 。

    4. 回到开始的问题,那么我在发起删除请求的时候,被拦截之后,是什么原因导致拦截之后跳转到400 页面呢?

    一步一步跟代码:ActionInterceptor.preHandle --> HandlerExecutionChain.applyPreHandle -->  DispatcherServlet.doDispatch 方法中抛出了异常,异常信息如下

    org.springframework.http.converter.HttpMessageNotReadableException: Could not read document: Can not deserialize value of type java.sql.Timestamp from String "2018-05-31 17:18:53": not a valid representation (error: Failed to parse Date value '2018-05-31 17:18:53': Can not parse date "2018-05-31 17:18:53Z": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSS'Z'', parsing fails (leniency? null))
    at [Source: java.io.PushbackInputStream@ef552ae; line: 1, column: 3322] (through reference chain: java.util.ArrayList[0]->com.mingsoft.cms.entity.ArticleEntity["basicDateTime"]); nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Can not deserialize value of type java.sql.Timestamp from String "2018-05-31 17:18:53": not a valid representation (error: Failed to parse Date value '2018-05-31 17:18:53': Can not parse date "2018-05-31 17:18:53Z": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSS'Z'', parsing fails (leniency? null))
    at [Source: java.io.PushbackInputStream@ef552ae; line: 1, column: 3322] (through reference chain: java.util.ArrayList[0]->xxx.xxx.entity.ArticleEntity["basicDateTime"])

    Json 格式的日期解析为ArticleEntity 中的basicDateTime 的日期格式(Date)异常,无法完成请求和注解POJO的映射,终于。问题原因找到了,解决办法有好几种,这里就不说了。

    5. 刨根问底

    根据第四点的描述可以知道,当我发起请求的时候,servlet 容器会先取访问DispatcherServlet 类中的 doDispatch 方法,我把这个方法的主要部分摘出来,主要看加粗部分:

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    		...
              HandlerExecutionChain mappedHandler = null; try { ... try { ... if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) {// 异常,刚才那个删除问题的异常会在这被捕获 dispatchException = ex; } catch (Throwable err) { ... } ... } catch (Exception ex) { ... } catch (Throwable err) { ... } finally { ... } }

    再进入HandlerExecutionChain.applyPreHandle 方法

          /**
    	 * Apply preHandle methods of registered interceptors.
    	 * @return {@code true} if the execution chain should proceed with the
    	 * next interceptor or the handler itself. Else, DispatcherServlet assumes
    	 * that this interceptor has already dealt with the response itself.
    	 * 执行已经注册的拦截的 preHandle()方法。如果返回true,则执行链可以执行下一个拦截器的preHandle()方法或 handler 自身
             */
    	boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    		HandlerInterceptor[] interceptors = getInterceptors();
    		if (!ObjectUtils.isEmpty(interceptors)) {
    			for (int i = 0; i < interceptors.length; i++) {
    				HandlerInterceptor interceptor = interceptors[i];
    				if (!interceptor.preHandle(request, response, this.handler)) {
    					triggerAfterCompletion(request, response, null);
    					return false;
    				}
    				this.interceptorIndex = i;
    			}
    		}
    		return true;
    	}             
    

    已经注册的拦截器也就是ActionInterceptor.preHandle,这个拦截器实现了HandlerInterceptorAdapter 的preHandle 抽象方法,从而达到拦截所有.do 请求的目的。

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    		...
    		// 为request 添加一些属性
                    // 初始化一些值    
    		...
    		return true;
    	}    
    

    二、Interceptor 简介

    Java 里的拦截器是动态拦截 action 调用的对象。它提供了一种机制可以使开发者可以定义在一个 action 执行的前后执行的代码,也可以在一个 action 执行前阻止其执行,同时也提供了一种可以提取 action 中可重用部分的方式。在AOP(Aspect-Oriented Programming)中拦截器用于在某个方法或字段被访问之前进行拦截,然后在之前或之后加入某些操作。

    三、Interceptor 原理

    拦截器 Interceptor 的拦截功能是基于 Java 的动态代理来实现的,具体可以参考博文“ 用 Java 实现拦截器 Interceptor 的拦截功能 ”,也可以通过阅读 Spring 源代码来了解更为权威的实现细节。

    四、自定义Interceptor 的实现

    第一步:自定义一个实现了Interceptor接口的类,或者继承抽象类AbstractInterceptor

      SpringMVC还提供了HandlerInterceptorAdapter 其是抽象类,也是HandlerInterceptor的子类,在实现了HandlerInterceptor的三个函数后还增加了一个函数。

    (1)preHandle: 在执行controller处理之前执行,返回值为boolean ,返回值为true时接着执行postHandle和afterCompletion,如果我们返回false则中断执行

    (2)postHandle:在执行controller的处理后,在ModelAndView处理前执行

    (3)afterCompletion :在DispatchServlet执行完ModelAndView之后执行

    (4)afterConcurrentHandlingStarted:这个方法会在Controller方法异步执行时开始执行,而Interceptor的postHandle方法则是需要等到Controller的异步执行完才能执行,只要继承这个类并实现其方法就可以了。

    package com.github.interceptor;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    
    /**
     * @date 2018年6月5日 上午10:57:04
     */
    public class ActionInterceptor extends HandlerInterceptorAdapter {
    
    	@Override
    	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    			throws Exception {
    		System.out.println("拦截器,拦截所有的action 请求");
    		// ...
    		// 初始化一些值
    		// 对request 做一些操作,例如设置属性值
    		// ...
    		return true;
    	}
    
    }

    第二步:在配置文件中注册定义的拦截器:

    <mvc:interceptors>
    		<!-- 使用 bean 定义一个 Interceptor,直接定义在 mvc:interceptors 下面的 Interceptor 将拦截所有的请求 -->  
       		 <!-- <bean class="com.github.interceptor.ActionInterceptor"/>  -->
    		<mvc:interceptor>
    			<mvc:mapping path="/**" />
    			<!-- 定义在 mvc:interceptor 下面的 Interceptor,表示对特定的请求进行拦截 -->
    			<bean class="com.github.interceptor.ActionInterceptor" />
    		</mvc:interceptor>
    	</mvc:interceptors>
    

      

    也可以直接在<mvc:interceptors> 定义一系列的拦截器,这些拦截器构成了拦截器链也可以称为拦截器栈,他们执行的先后顺序即为声明顺序:

    <mvc:interceptors>
       		 <bean class="com.github.interceptor.ActionInterceptor"/>
       		 <bean class="com.github.interceptor.LoginInterceptor"/>
       		 <bean class="com.github.interceptor.ContextInterceptor"/>
    	</mvc:interceptors>
    

      

    第三步:在需要使用Action中引用上述定义的拦截器,为了方便也可以将拦截器定义为默认的拦截器,这样在不加特殊说明的情况下,所有的都被这个拦截器拦截。

     五、过滤器与拦截器的区别

    过滤器可以简单的理解为“取你所想取”,过滤器关注的是web请求;拦截器可以简单的理解为“拒你所想拒”,拦截器关注的是方法调用,比如拦截敏感词汇。

    1. 拦截器是基于java反射机制来实现的,而过滤器是基于函数回调来实现的。(有人说,拦截器是基于动态代理来实现的)

    2. 拦截器不依赖servlet容器,过滤器依赖于servlet容器。

    3. 拦截器只对Action起作用,过滤器可以对所有请求起作用。

    4. 拦截器可以访问Action上下文和值栈中的对象,过滤器不能。

    5. 在Action的生命周期中,拦截器可以多次调用,而过滤器只能在容器初始化时调用一次。

     六、总结

    先说一下,回到最开始的问题,我们发起请求时,经过拦截器拦截,对request 作了一系列的操作,此时拦截器的注册没有问题。当json 数据转换为article 映射数据时某个字段解析异常导致,整个actico 请求出错。虽然错误的根本原因不在拦截器,但是由这个问题引出拦截器的概念,从而让我进一步认识拦截器,也是值得记录的。

    拦截器通对用户请求进行拦截可以对用户的请求(HttpServletRequest)进行预处理,也可以对返回给用户的响应(HttpServletResponse)进行访问后处理,还可以在请求完成后针对请求的异常信息进行处理。能够在request对象和response对象中进行操作的问题都可以通过拦截器进行统一处理,常用的场景包括用户登录控制、权限检查,跨域请求访问权限控制等。 

     ps: 回到1.3 的错误日志,为什么只打印了一个 error/400.do 呢,这个错误日志对开发人员来说根本不能直接定位问题出现原因,这也间接引出了我们对Interceptor 的探索,并且最后发现问题并不出在拦截器,但是如果我们已经了解拦截器,并且想要直接定位问题原因,那我们应该如何做呢?这就牵涉到使用spring利用HandlerExceptionResolver实现全局异常捕获 。后面对这一知识点将会详细展开。

    参考:https://blog.csdn.net/qq924862077/article/details/53524507  

    版权声明:本文为博主原创文章,未经博主允许不得转载。 
  • 相关阅读:
    MySQL数据库优化的八种方式(经典必看)
    HTML5之应用缓存---manifest---缓存使用----HTML5的manifest缓存
    ajax方法总结
    十分钟入门less(翻译自:Learn lESS in 10 Minutes(or less))
    MySQL主从复制技术(纯干货)
    table不能遗露了tbody
    DOM 之selection
    DOM 其他一些特性
    CSSOM视图模式
    DOM 节点实例操作
  • 原文地址:https://www.cnblogs.com/hellovoyager1/p/9138589.html
Copyright © 2020-2023  润新知