Spring -- 通過攔截器使用註解方式校驗參數

前言:

上一篇介紹了,使用AOP的方式去攔截校驗參數,本章講解使用攔截器去校驗參數,以及遇到的問題。

簡介:

Spring web mvc 處理攔截器,就是案例所用到的去校驗參數,類似與serlvet開發中裏的filter過濾器。用於對攔截前及後處理。

常見場景:

日記記錄、校驗參數、權限檢查等等。比喻我們在學習jdbc的時候,獲取連接,最後關閉連接。其實本質也是AOP的方法(面向切面編程),也就是說符合AOP的橫向切入點的功能都可以使用攔截器去實現。

一、攔截器類:

public interface HandlerInterceptor {

	boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
	    throws Exception;

	
	void postHandle(
			HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
			throws Exception;

	
	void afterCompletion(
			HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception;

}

preHandle:預處理回調方法,實現攔截器的預處理,第三個參數爲響應的處理器;返回值:true表示繼續流程;false表示流程中斷(如校驗失敗,參數缺少),不會繼續調用其他的攔截器或處理器,此時我們需要通過response來產生響應;
postHandle:後處理回調方法,實現處理攔截器的後處理(返回請求結果之前)。
afterCompletion:整個請求處理完畢回調方法,在返回結果之後進行調用,但僅調用處理器執行鏈中preHandle返回true的攔截器的afterCompletion。

二、攔截器適配器

有個場景,就是我們實現攔截器的時候只需要實現某個方法,或者不需要去實現全部的方法,可以使用攔截器的適配器類(使用了一種適配器模式的設計模式),允許我們只實現需要的方法。
public abstract class HandlerInterceptorAdapter implements AsyncHandlerInterceptor {

	/**
	 * This implementation always returns {@code true}.
	 */
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
		throws Exception {
		return true;
	}

	/**
	 * This implementation is empty.
	 */
	@Override
	public void postHandle(
			HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
			throws Exception {
	}

	/**
	 * This implementation is empty.
	 */
	@Override
	public void afterCompletion(
			HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
	}

	/**
	 * This implementation is empty. 這個方法實現自AsyncHandlerInterceptor

	 */
	@Override
	public void afterConcurrentHandlingStarted(
			HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
	}

}

三、流程圖如下:

1、正常流程如下圖:

2、被攔截運行流程圖如下:

3、運行流程介紹如下;

1、攔截器執行順序是按照Spring配置文件中定義的順序而定的。
2、會先按照配置攔截器的順序執行所有攔截器的preHandle方法,一直遇到return false爲止,比如第二個preHandle方法是return false,則第三個以及以後所有攔截器都不會執行。若都是return true,則按配置的攔截器配置順序加載完preHandle方法。
3、然後執行目標方法(目標controller接口),若中間拋出異常,則跟return false效果一致,不會繼續執行postHandle,只會倒序執行afterCompletion方法。
4、在目標方法執行完業務邏輯(頁面還未渲染數據)時,按倒序執行postHandle方法。若第三個攔截器的preHandle方法return false,則會執行第二個和第一個的postHandle方法和afterCompletion(postHandle都執行完纔會執行這個,也就是頁面渲染完數據後,執行after進行清理工作)方法。(postHandle和afterCompletion都是倒序執行)

四、校驗參數demo:

以下內容和上篇AOP方式的註解一樣,可以忽略:
註解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CheckParam {

    /**
     * 請求當前接口所需要的參數,多個以小寫的逗號隔開
     * @return
     */
    String fieldNames() default "";

    /**
     *傳遞參數的對象類型
     */
    Class<?> parameter() default Object.class;

    /**
     * 默認不校驗參數;
     * @return
     */
    boolean require() default false;
}
請求中獲取流:
public class HttpHelper  {

    public static String getBodyString(HttpServletRequest request) throws IOException {
        StringBuilder sb = new StringBuilder();
        try(BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream(), Charset.forName("UTF-8")))) {
            String line = "";
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        }
        return sb.toString();
    }

}
攔截器實現:
public class OpenHandlerInterceptor extends HandlerInterceptorAdapter {

    private static final Logger LOGGER = Logger.getLogger(OpenHandlerInterceptor.class);

    protected static final String APPLICATION_CHARSET="application/json; charset=utf-8";

    protected static final String UTF_8 = "UTF-8";
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        try {
            if(handler instanceof HandlerMethod){
                HandlerMethod handlerMethod = (HandlerMethod) handler;
                Method method = handlerMethod.getMethod();
                CheckParam checkParam = method.getAnnotation(CheckParam.class);
                if(checkParam == null || !checkParam.require() || StringUtils.isEmpty(checkParam.fieldNames())) {
                    return true;
                }
                //防止中文,符號
                String fieldNames = checkParam.fieldNames().replace(",",",");
                boolean jsonParam = CheckFieldsNotNull(request, fieldNames);
                if(!jsonParam){
                    ErrorMsg(response);
                    return false;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            ErrorMsg(response);
            return false;
        }
        return true;

    }

    /**
     * 校驗必傳參數屬性是否存在
     * @param request
     * @param fieldNames
     * @return
     * @throws Exception
     */
    public static boolean CheckFieldsNotNull(HttpServletRequest request, String fieldNames) throws Exception{
        LOGGER.info(String.format("校驗字段:{%s}",fieldNames));
        String requestParam = HttpHelper.getBodyString(request);
        if(StringUtils.isEmpty(requestParam)){
            return false;
        }
        Gson gson = new Gson();
        ArrayList<Map> arrayList = gson.fromJson(requestParam, ArrayList.class);
        for (String fieldName : fieldNames.split(",")) {
            Preconditions.checkNotNull(arrayList.get(0).get(fieldName),fieldName + "is null");
        }
        return true;
    }

    /**
     * 錯誤信息
     * @param response
     * @throws IOException
     */
    public static void ErrorMsg( HttpServletResponse response) throws Exception {
        PrintWriter out = response.getWriter();
        response.setCharacterEncoding(UTF_8);
        response.setContentType(APPLICATION_CHARSET);
        out.write(JsonMapper.toJsonString(Result.fail(ErrorMessageEnum.MISS_REQUEST_PARAM.getValue(),ErrorMessageEnum.MISS_REQUEST_PARAM.getDesc())));
    }
}
以上代碼就實現了攔截器的參數校驗,但是從中遇到一個問題,流只能讀一次。所以我們在controller再次獲取的是null值,爲了解決請求中的流多次複用,所以實現了一個Filter,通過Filter來解決流只能讀取一次的問題。代碼如下:
public class RequestReaderHttpServletRequestWrapper extends HttpServletRequestWrapper  {

    private final byte[] body;

    public RequestReaderHttpServletRequestWrapper(ServletRequest request) throws IOException {
        super(request);
        body = HttpHelper.getBodyString(request).getBytes(Charset.forName(OpenHandlerInterceptor.UTF_8));
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {

        final ByteArrayInputStream bais = new ByteArrayInputStream(body);

        return new ServletInputStream() {

            @Override
            public int read() throws IOException {
                return bais.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }
        };
    }
}

以上類繼承自:
HttpServletRequestWrapper
這個類是HttpServletRequest的一個包裝類,大家應該知道裝飾器模式。這個類對接下來過濾器中的使用是有非常重要的意義的。
解決問題就是,流一旦讀取就無法再次讀取,所以通過Filter的重寫回寫請求對象中;使請求中的流多次複用。
接下來是過濾器的實現,過濾器的實現使用了責任鏈設計模式:
public class HttpServletRequestReplacedFilter implements Filter {
    @Override
    public void destroy() {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if(request instanceof HttpServletRequest) {
            requestWrapper = new RequestReaderHttpServletRequestWrapper((HttpServletRequest) request);
        }
        //獲取請求中的流如何,將取出來的字符串,再次轉換成流,然後把它放入到新request對象中。
        // 在chain.doFiler方法中傳遞新的request對象
        if(requestWrapper == null) {
            chain.doFilter(request, response);
        } else {
            chain.doFilter(requestWrapper, response);
        }
    }

    @Override
    public void init(FilterConfig arg0) throws ServletException {

    }
}

總結:
除了以上方法:使用servlet提供的HttpServletRequestWrapper類,重寫相關ServletRequest方法,實現流的多次讀取。
另外一個方法:在Filter時把請求中的流放入ThreadLocal,然後在需要請求參數的地方調用ThreadLocal的get方法即可。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章