Spring 實現(xiàn)一個支持 分發(fā) 轉(zhuǎn)發(fā)的 rest接口

需求概述

我們的需求是這樣的:
后臺的rest接口對接 安卓、IOS客戶端、小程序
考慮到安全,我們對請求參數(shù)做了簽名校驗,對部分接口做了登錄態(tài)校驗
安全是安全了,但是 對于我們后臺開發(fā)人員來說,進行接口測試就變得困難起來

接口測試,我們前面是這么做的

階段一

在本地進行接口測試,把關(guān)鍵的Filter 注釋掉
感受:太麻煩了,不小心就會把注釋的代碼提交了(我用的是烏龜git)

階段二

我用Python寫了一個本地代理,postman 配置好代理,讓請求走我的代理
這個代理會模擬客戶端,對參數(shù)進行簽名
工程在這里:proxy
感受:我們有些接口使用到了Protobuf,因為傳輸?shù)氖嵌M制數(shù)據(jù)流,接口返回的數(shù)據(jù),代理想轉(zhuǎn)為可視化的json數(shù)據(jù)很費勁
(目前是先用protobuf生成Python文件,再反序列化,再顯示,生成文件這一步需要經(jīng)常維護)
而且我們的登錄態(tài)用到了jwt,token動不動就過期了,需要更換;雖然使用postman的全局變量可以減少更換次數(shù),但是如果想切換用戶查看接口返回結(jié)果,又需要去數(shù)據(jù)庫找到對應(yīng)的token,再替換,有點費勁

經(jīng)歷過這兩個階段,后面就想著,能不能開發(fā)出這樣一個接口呢:
參數(shù)里寫上url method userId
然后這個接口根據(jù)這些參數(shù),進行分發(fā)(轉(zhuǎn)發(fā)),然后在Filter里面排除這個接口,不就避免了簽名校驗 登陸校驗嗎?

如何實現(xiàn)

剛開始實現(xiàn)的時候,也是一臉懵逼,用“轉(zhuǎn)發(fā)” 關(guān)鍵字 搜了一番,發(fā)現(xiàn)都不能用在Rest 接口
最后沒辦法,想著去了解一下spring是怎么實現(xiàn)的,我再重復(fù)實現(xiàn)一遍不就OK了嗎
于是去看了看 DispatcherServlet 源碼解析文章,找到了思路

先看看 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 {
            ModelAndView mv = null;
            Exception dispatchException = null;

            try {
                processedRequest = checkMultipart(request);
                multipartRequestParsed = (processedRequest != request);

                // Determine handler for the current request.
                                // 找到當(dāng)前request對應(yīng)的 handler
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null || mappedHandler.getHandler() == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }

                // Determine handler adapter for the current request.
                                // 找到當(dāng)前 handler 對應(yīng)的 適配器
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

                // Process last-modified header, if supported by the handler.
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (logger.isDebugEnabled()) {
                        logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                    }
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }

                                //執(zhí)行 HandlerExecutionChain 里面 攔截器的 preHandle
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                // Actually invoke the handler.
                                // 讓適配器運行handler,也就是執(zhí)行 Controller里的某個具體的方法
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }

                applyDefaultViewName(processedRequest, mv);

                                //執(zhí)行 HandlerExecutionChain 里面 攔截器的 postHandle
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
            catch (Exception ex) {
                dispatchException = ex;
            }
            catch (Throwable err) {
                // As of 4.3, we're processing Errors thrown from handler methods as well,
                // making them available for @ExceptionHandler methods and other scenarios.
                dispatchException = new NestedServletException("Handler dispatch failed", err);
            }
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
        catch (Exception ex) {
            triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
        }
        catch (Throwable err) {
            triggerAfterCompletion(processedRequest, response, mappedHandler,
                    new NestedServletException("Handler processing failed", err));
        }
        finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                // Instead of postHandle and afterCompletion
                if (mappedHandler != null) {
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                }
            }
            else {
                // Clean up any resources used by a multipart request.
                if (multipartRequestParsed) {
                    cleanupMultipart(processedRequest);
                }
            }
        }
    }

關(guān)鍵的地方我都寫了中文注釋

所以 我們自己實現(xiàn) 分發(fā) 效果的時候,參考這個邏輯即可
找到handler --> 找到適配器 --> 執(zhí)行攔截器 preHandle(如果需要) --> 執(zhí)行 handler --> 執(zhí)行攔截器 postHandle(如果需要)

實現(xiàn)代碼

下面是 Controller

package com.xxx.app.skmr.controller;

import java.util.HashMap;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.powermock.reflect.Whitebox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.ModelAndView;

import com.xxx.app.skmr.constant.Constants;
import com.xxx.app.skmr.filter.EncryptRequest;
import com.xxx.app.skmr.properties.CommonConfigProperties;
import com.xxx.app.skmr.service.ReqCacheService;
import com.xxx.app.skmr.util.AssertHelper;

@RequestMapping("/v1")
@RestController
public class InterfaceTestController {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(InterfaceTestController.class);
    
    @Autowired
    private DispatcherServlet dispatcherServlet;
    
    @Autowired
    private CommonConfigProperties commonConfigProperties;

    @RequestMapping(value = "/iTest", method = RequestMethod.POST)
    public void receiveRequest(
            @RequestParam(value="url", required=true) String url,
            @RequestParam(value="method", required=true) String method,
            @RequestParam(value="param", required=false) String param,
            @RequestParam(value="userId", required=false) String userId,
            HttpServletRequest request, 
            HttpServletResponse response) {
        
        LOGGER.info("InterfaceTestController receiveRequest, url={}", url);
        
        if (! Constants.SKMR_APP_ENV_NAME_DEV.equals(commonConfigProperties.getEnvName())) {
            //不是 dev 環(huán)境,直接退出
            return;
        }
        
        //設(shè)置登錄態(tài)
        if (StringUtils.isNotBlank(userId)) {
            ReqCacheService.setReqUserId(request, userId);
        }
        
        //改變request里的值
        EncryptRequest myRequest = (EncryptRequest) request;
        
        //強行設(shè)置 header 里的 accept 為 application/json,為客戶端省點事
        myRequest.setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
        
        myRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        
        //修改方法,方法要大寫
        myRequest.setMethod(method.toUpperCase());
        
        //修改 請求path
        myRequest.setRequestURI(url);
        
        //這個必須要設(shè)置,不然在 UrlPathHelper.getLookupPathForRequest()  方法里面 有問題,需要讓rest 為空
        myRequest.setServletPath(url);
        
        //設(shè)置url 查詢參數(shù),
        if (StringUtils.isNotBlank(param)) {
            HashMap<String, String> convertParam = convertParam(param);
            
            myRequest.setUseExternalParam(true);
            myRequest.setParamMap(convertParam);
        }
        
        //轉(zhuǎn)發(fā)
        redirect(myRequest, response);
        
        return ;
    }
    
    @RequestMapping(value = "/iTest", method = RequestMethod.GET)
    public void receiveRequestV2(
            @RequestParam(value="url", required=true) String url,
            @RequestParam(value="method", required=true) String method,
            @RequestParam(value="param", required=false) String param,
            @RequestParam(value="userId", required=false) String userId,
            HttpServletRequest request, 
            HttpServletResponse response) {
        
        //這個接口因為是 get 的,所以可以通過瀏覽器直接調(diào)用,不需要postman
        //當(dāng)然了,對于有body的,瀏覽器就不行了,還是需要postman
        
        LOGGER.info("InterfaceTestController receiveRequestV2, url={}", url);
        
        receiveRequest(url, method, param, userId, request, response);
        
        return ;
    }
    
    /**
     * <br>轉(zhuǎn)發(fā) 分發(fā)
     * <br>只有自身的url 會走 過濾器,轉(zhuǎn)發(fā)后的 沒有走過濾器
     *
     * @param request
     * @param response
     * @author YellowTail
     * @since 2019-02-15
     */
    private void redirect(HttpServletRequest request, HttpServletResponse response) {
        
        try {
            // 1. 得到 HandlerExecutionChain, 調(diào)用方法 getHandler 即可得到
            HandlerExecutionChain handlerExecutionChain = Whitebox.invokeMethod(dispatcherServlet, "getHandler", request);
            
            // 2. 取出 HandlerMethod,適配器要用
            HandlerMethod handlerMethod = (HandlerMethod) handlerExecutionChain.getHandler();
            
            // 3. 得到 適配器 HandlerAdapter,調(diào)用方法 getHandlerAdapter 得到
            HandlerAdapter ha = Whitebox.invokeMethod(dispatcherServlet, "getHandlerAdapter", handlerMethod);
            
            // 4. 執(zhí)行 HandlerExecutionChain 攔截器的 preHandler() 前置方法, CmdHandlerInterceptor 會去設(shè)置 cmd
            Whitebox.invokeMethod(handlerExecutionChain, "applyPreHandle", request, response);
            
            // 5. 執(zhí)行 handler
            ModelAndView mv = ha.handle(request, response, handlerMethod);
            
            // 6. 執(zhí)行 攔截器的 postHandler() 方法, LogHandlerInterceptor 會去記錄日志
            Whitebox.invokeMethod(handlerExecutionChain, "applyPostHandle", request, response, mv);
            
        } catch (Exception e) {
            LOGGER.error("error, ", e);
        }
    }
    
    /**
     * <br>將字符串形式的 參數(shù)  id=test&type=2 轉(zhuǎn)換為 map,方便使用
     *
     * @param param
     * @return
     * @author YellowTail
     * @since 2019-02-15
     */
    private HashMap<String, String> convertParam(String param) {
        HashMap<String, String> map = new HashMap<>();
        
        if (StringUtils.isBlank(param)) {
            return map;
        }
        
        String[] split = param.split("&");
        
        for(String eachParam : split) {
            String[] split2 = eachParam.split("=");
            
            AssertHelper.assertTrue(2 >= split2.length , eachParam + " eachParam should contains one =");
            
            if (2 == split2.length) {
                map.put(split2[0], split2[1]);
            }
        }
        
        return map;
    }
}

因為我們需要對 request 做很多操作,所以必須自己實現(xiàn)一個request ,且繼承 HttpServletRequestWrapper

package com.xxx.app.skmr.filter;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Vector;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.xxx.app.skmr.util.IOSystem;
import com.xxx.app.skmr.util.StringUtils;


public class EncryptRequest extends HttpServletRequestWrapper {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(EncryptRequest.class);
    
    /**
     * URL 的方法
     */
    private String _method;
    
    /**
     * URI
     */
    private String _requestURI;
    
    /**
     * ServletPath
     */
    private String _servletPath;
    
    
    /**
     * 是否使用 擴展的 參數(shù)
     */
    private boolean useExternalParam = false;
    
    /**
     * 參數(shù) map
     */
    private HashMap<String, String> paramMap;
    
    /**
     * header
     */
    private HashMap<String, String> _headers;
    
    /**
     * 存儲requestBody的內(nèi)容
     */
    private byte[] requestBody = null;
    

   public EncryptRequest(HttpServletRequest request) {
       super(request);
  
       try {
           this.requestBody = IOSystem.readToBytes(request.getInputStream());
       } catch (IOException e) {
           e.printStackTrace();
           LOGGER.error("EncryptRequest init getInputStream error", e);
           throw new RuntimeException(e);
       }
   }

    /**
     * 獲取requestbody
     */
    public byte[] getRequestBody() {
        return this.requestBody;
    }
    
    public String getRequestBodyString() {
        return StringUtils.toString(requestBody, this.getRequest().getCharacterEncoding());
    }
    
    public void setRequestBody(byte[] requestBody ) {
        this.requestBody = requestBody;
    }


    @Override
    public ServletInputStream getInputStream() throws IOException {
        // 如果是null 證明首次數(shù)據(jù)獲取失敗
        if (requestBody == null) {
            requestBody = new byte[0];
        }
        
        final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return bais.read();
            }

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

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

            @Override
            public void setReadListener(ReadListener listener) {
            }
        };
    }


    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new StringReader(this.getRequestBodyString()));
    }
    
    /**
     * 重寫 getMethod,方便 變更 method
     */
    @Override
    public String getMethod() {
        if (null == _method) {
            _method = super.getMethod();
        }
        return _method;
    }
    
    /**
     * <br>擴展方法:支持修改  method
     * <br>在 接口測試的接口里使用到了
     *
     * @param newMethod
     * @author YellowTail
     * @since 2019-02-14
     */
    public void setMethod(String newMethod) {
        _method = newMethod;
    }
    
    /**
     * 覆蓋,方便變更
     */
    @Override
    public String getRequestURI() {
        if (null == _requestURI) {
            _requestURI = super.getRequestURI();
        }
        return _requestURI;
    }
    
    /**
     * <br>擴展方法,支持變更 path
     * <br>在 接口測試的接口里使用到了
     *
     * @param value
     * @author YellowTail
     * @since 2019-02-14
     */
    public void setRequestURI(String value) {
        _requestURI = value;
    }
    
    @Override
    public String getServletPath() {
        if (null == _servletPath) {
            _servletPath = super.getServletPath();
        }
        return _servletPath;
    }
    
    public void setServletPath(String value) {
        _servletPath = value;
    }
    
    @Override
    public String[] getParameterValues(String name) {
        if (useExternalParam) {
            String string = paramMap.get(name);
            if (null == string) {
                return null;
            }
            
            return new  String [] {string};
        }
        return super.getParameterValues(name);
    }

    public void setUseExternalParam(boolean useExternalParam) {
        this.useExternalParam = useExternalParam;
    }

    public void setParamMap(HashMap<String, String> paramMap) {
        this.paramMap = paramMap;
    }
    
    
    /**
     * 覆蓋 header 獲取的方法,進行自定義擴展
     */
    @Override
    public Enumeration<String> getHeaders(String name) {
        
        //如果某個值被設(shè)置過,那么用自定義的值
        if (null != _headers && _headers.containsKey(name)) {
            String string = _headers.get(name);
            
            Vector<String> values = new Vector<String>();
            values.add(string);
            
            return values.elements();
        }
        
        return super.getHeaders(name);
    }
    
    /**
     * <br>設(shè)置 Header 里的值
     *
     * @param key
     * @param value
     * @author YellowTail
     * @since 2019-02-15
     */
    public void setHeader(String key, String value) {
        if (null == _headers) {
            _headers = new HashMap<>();
        }
        
        _headers.put(key, value);
    }
   
}

實現(xiàn)代碼解釋

因為 DispatcherServlet 的很多方法都是 protected, friendly 懶的搞繼承,直接反射調(diào)用了
關(guān)于攔截器,因為我在實現(xiàn)的時候,是需要攔截器的一些效果(自動上傳日志),所以就執(zhí)行了步驟 4 和 6
大家在實現(xiàn)的時候,可以根據(jù)實際情況進行取舍

為何自定義request
因為 HttpServletRequest 只有一堆的 get 方法,沒有 set 方法
看了下實現(xiàn),反射好費勁,算了,直接繼承一個,復(fù)寫方法

效果

Type value
接口地址 /v1/iTest
接口方法 Post
接口Header header里的Accept Content-Type 都不需要設(shè)置,代碼已經(jīng)寫死為application/json
設(shè)置為其他值不會生效
接口參數(shù) 是否必填 解釋
url 必填 準(zhǔn)備請求哪個接口
method 必填 接口的方法(因為有些接口url一樣,但是method不一樣),
大小寫不敏感, get Get GET 都行
userId 非必填 設(shè)置登錄態(tài),即想用哪個用戶請求接口
param 非必填 請求接口的請求參數(shù),比如對于接口 /v1/xxx/me/list?unitId=2&nextid=&scope=2
那么param就是?后面的字符串,且需要進行url編碼
也就是unitId%3D2%26nextid%3D%26scope%3D2

BUG 修復(fù) 2019年2月20日 10:31:35

修復(fù)了 param 里面 參數(shù)值為空拋異常的問題
代碼已在此博客里更新

功能新增:把異常輸出到瀏覽器上,省去看日志的步驟

一旦代碼拋了異常,瀏覽器調(diào)用接口的時候,看不到信息
于是突發(fā)奇想,把 Exception 信息 參考 Logger 那種方式輸出到屏幕上,多方便
于是寫了一下,代碼在下面,沒有更新到 文章開始的那個代碼塊里

public static final String CHANGE_LINE = "\n".intern();
public static final String TAB_INDENT = "    at ";

...
catch (Exception e) {
            LOGGER.error("error, ", e);
            
            StringBuilder sb = new StringBuilder();
            
            sb.append(e.toString()).append(CHANGE_LINE);
            
            for(StackTraceElement st: e.getStackTrace()) {
                sb.append(TAB_INDENT)
                    .append(st.toString())
                    .append(CHANGE_LINE);
            }
            
            byte[] bytes = sb.toString().getBytes();
            HttpEncryptService.setResponseData(bytes, response, false);
        }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容