天天看點

微信公衆平台接入之簡單任務分發器

  微信公衆号現在影響力有目共睹,是以接入其功能也是很正常的。

  現在的應用中,有很多是基于spring的架構來做的。針對自行開發的系統,我們可以通過任意的自定義 url 來進行業務功能的映射。然而大家知道,微信的回調位址永遠隻有一個,但是其内部的内容則是多樣的。針對不同的内容,咱們做出的響應自然也是不一樣的。咱們可以通過n個if...else 區分出需要處理的業務。但是這樣的代碼會很不清晰,維護起來将是噩夢。是以,咱們有必要根據内容來做一個業務功能的分發,使代碼能夠更清晰。

  隻需要一個簡單的 mapping 就可以了。

比如,我們可以配置一個微信回調位址: http://a.bc.com/xxx/wxcallback

我們需要響應兩種類型的請求,一個是 get 請求,進行服務驗證,一個是 post 請求,進行業務事件通知!demo 如下:

@RequestMapping(value = "/{wxServiceType}/wxcallback", method = RequestMethod.GET, produces = "text/html")
    @ResponseBody
    public String gatewayGet(@ModelAttribute WxTokenVerifyReqModel verifyReqModel, @PathVariable(value = "wxServiceType") String wxServiceType) {
        log.info("請求token:{}", verifyReqModel);
        try {
            String reply = serverValidateService.validServerToken(verifyReqModel, WxGatewayServiceKeyEnum.xxx);
            log.info("成功傳回:{}", reply);
            return reply;
        } catch (WeixinSystemException e) {
            log.warn("驗證失敗, code:{}, msg:{}", e.errCode, e.message);
            return "fail";
        } catch (Exception e) {
            log.error("微信校驗接口異常error=", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 公衆号主要消息請求入口
     *
     * @param req  請求, xml 格式
     * @param resp 響應
     * @return 按格式傳回
     */
    @RequestMapping(value = "/{wxServiceType}/wxcallback", method = RequestMethod.POST, produces = "application/xml; charset=utf-8")
    @ResponseBody
    public String gatewayPost(HttpServletRequest req, HttpServletResponse resp, @PathVariable(value = "wxServiceType") String wxServiceType) throws IOException {

        try {
            // 交給内部分發器處理,傳回處理結果
            Object retMessage = wxMessageHandleDispatcher.handle(req, wxServiceType);
            if (null != retMessage) {
                return retMessage.toString();
            }
        }
        catch (DocumentException | IOException e) {
            log.error("參數解析異常", e);
        } catch (Exception e) {
            log.error("其他異常,", e);
        }
        return "success";
    }      

  第一個token驗證,與springmvc的普通模式一樣,直接通過 ServletModelAttributeMethodProcessor 參數解析器給解析了。

  是以,咱們隻需處理 xml 的正文請求即可!即如下分發:

Object retMessage = wxMessageHandleDispatcher.handle(req, wxServiceType);

分發入口類:
      
@Component
public class WxMessageHandleDispatcher {

    @Resource
    private WxMessageHandlerMappingBean wxMessageHandlerMappingBean;

    /**
     * 請求編号,可用于統計當日通路量
     */
    private AtomicInteger requestSeqNum = new AtomicInteger(0);

    /**
     * 服務辨別
     */
    private static final ThreadLocal<WxGatewayServiceKeyEnum> gatewayServiceKeyHolder = new ThreadLocal<>();

    /**
     * 處理微信消息響應入口
     *
     * @param request xml
     * @return 傳回結果
     * @throws RuntimeException
     * @apiNote {@link WxMessageBaseReqModel}
     */
    public Object handle(HttpServletRequest request, WxGatewayServiceKeyEnum serviceKey)
            throws DocumentException, IOException, RuntimeException {
        Long startTime = System.currentTimeMillis();
        // 處理參數
        Map<String, String> parameters = extractRequestParams(request);

        // 初始化基礎環境
        prepareGatewayServiceHandle(serviceKey);

        // 轉換為 uri
        String handleUri = exchangeHandleUri(parameters, serviceKey);
        log.info("【微信消息處理】enter {} method, params: {}, serviceKey:{}, seqNum:{}", handleUri, JSONObject.toJSONString(parameters), serviceKey, requestSeqNum.get());

        Object retMessage = null;

        try {
            // 調用 handleMethod
            retMessage = invokeHandleMethod(parameters, handleUri);
        }
        // 業務異常,看情況捕獲
        catch (WeixinSystemException e) {
            log.warn("@{} 發生業務異常:code:{}, msg:{}", handleUri, e.errCode, e.message);
            throw e;
        }
        // 其他異常
        catch (Exception e) {
            log.error("處理方法" + handleUri + " 發生異常", e);
        }
        // 最終列印
        finally {
            log.info("exit {} method, params: {}, result:{}, serviceKey:{}, seqNum:{}, cost:{}ms",
                    handleUri, JSONObject.toJSONString(parameters), retMessage, serviceKey,
                    requestSeqNum.get(), (System.currentTimeMillis() - startTime));
            finishGatewayServiceHandle();
        }

        return retMessage;
    }

    /**
     * 解析請求參數
     *
     * @param request 原始請求
     * @return k-v
     * @throws IOException
     * @throws DocumentException
     */
    private Map<String, String> extractRequestParams(HttpServletRequest request) throws IOException, DocumentException {
        Map<String, String> parameters = WechatMessageUtil.xmlToMap(request);
        String requestIp = NetworkUtil.getIpAddress(request);
        parameters.put("requestIp", requestIp);
        return parameters;
    }

    /**
     * 初始化 serviceKey, 供全局調用
     *
     * @param serviceKey gateway 傳入 服務辨別
     */
    private void prepareGatewayServiceHandle(WxGatewayServiceKeyEnum serviceKey) {
        requestSeqNum.incrementAndGet();
        gatewayServiceKeyHolder.set(serviceKey);
    }

    /**
     * 轉換需要處理的 uri 資源請求
     *
     * @param parameters 原始參數
     * @param serviceKey serviceKey gateway 傳入 服務辨別
     * @return 如 /xxx/text
     */
    private String exchangeHandleUri(Map<String, String> parameters, WxGatewayServiceKeyEnum serviceKey) {
        String msgType = parameters.get("MsgType");
        String eventType = parameters.get("Event");
        String handleUri = serviceKey.getAlias() + "/" + msgType;
        if(eventType != null) {
            handleUri = handleUri + "/" + eventType;
        }
        return handleUri;
    }

    /**
     * 調用處理方法
     *
     * @param parameters 參數請求
     * @param handleUri uri
     * @return 處理結果
     * @throws NoSuchMethodException
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     */
    private Object invokeHandleMethod(Map<String, String> parameters, String handleUri)
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        Object retMessage = "";     // 回複null會有bug

        // 擷取handleMethod
        WxMessageRequestMappingInfo methodConfig = wxMessageHandlerMappingBean.getHandlerConfig(handleUri);

        if(methodConfig != null) {
            WxMessageHandleService handleService = (WxMessageHandleService) SpringContextsUtil.getBean(methodConfig.getHandlerClz());
            WxMessageBaseReqModel paramsToHandle = (WxMessageBaseReqModel) JSONObject.parseObject(JSONObject.toJSONString(parameters), methodConfig.getParamClz());
            if(StringUtils.isBlank(methodConfig.getMethodName())) {
                retMessage = handleService.handle(paramsToHandle);
            }
            else {
                String methodName = methodConfig.getMethodName();
                retMessage = MethodUtils.invokeExactMethod(handleService, methodName, paramsToHandle);
            }
        }
        else {
            log.info("【微信消息處理】no handler found for {}", handleUri);
        }
        return retMessage;
    }

    /**
     * 擷取gateway 進來的 serviceKey
     *
     * @return 自擁有的 serviceKey 服務辨別
     */
    public WxGatewayServiceKeyEnum getGatewayServiceKey() {
        return gatewayServiceKeyHolder.get();
    }

    /**
     * 操作完成後,重置 serviceKey
     */
    private void finishGatewayServiceHandle() {
        gatewayServiceKeyHolder.remove();
    }
}      
如上分發類,主要做一主體的操作,如參數解析,uri 重新擷取,實際業務方法的調用等;
是以,關鍵點還是在于怎麼調用業務方法?
首先,看一下業務方法配置的擷取:
      
WxMessageHandlerMethodEnum methodConfig = wxMessageHandlerMappingBean.getHandlerConfig(handleUri);      
@Component
@Slf4j
public class WxMessageHandlerMappingBean implements InitializingBean, BeanNameAware,
        ApplicationContextAware {

    /**
     * 直接比對的mapping
     */
    private final Map<String, WxMessageRequestMappingInfo> directHandlerMappings = new ConcurrentHashMap<>();

    /**
     * 使用正則比對的mapping
     */
    private final Map<String, WxMessageRequestMappingInfo> patternHandlerMappings = new HashMap<>();

    // spring 式的 mapping
    private final Map<WxMessageRequestMappingInfo, HandlerMethod> springPatternMethodMappings = new ConcurrentHashMap<>();

    private transient String beanName;

    private transient ApplicationContext applicationContext;

    public void setBeanName(String name) {
        this.beanName = name;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
    /**
     * 擷取處理方法配置,主要為處理類
     *     先精确比對,不行再用正則比對一次
     *
     * @param messageHandleUri uri,如 xxx/text
     * @return 處理類,一般需繼承 {@link com.mobanker.weixin.dae.service.WxMessageHandleService}
     */
    public final WxMessageRequestMappingInfo getHandlerConfig(String messageHandleUri) {
        // 直接比對
        WxMessageRequestMappingInfo handlerMethodConfig = directHandlerMappings.get(messageHandleUri);
        if(handlerMethodConfig != null) {
            return handlerMethodConfig;
        }
        // 正則比對
        handlerMethodConfig = getPatternHandlerMapping(messageHandleUri);
        if(handlerMethodConfig != null) {
            return handlerMethodConfig;
        }
        return null;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // 掃描處理方法
        initHandlerMethods();
    }

    /**
     * 正則路由注冊
     *
     * @param methodConfig 路由配置
     */
    private void registerPatternHandlerMapping(WxMessageRequestMappingInfo methodConfig) {
        Pattern normalUriPattern = Pattern.compile("^[a-zA-Z0-9/\\-_:\\$]+$");
        if(!normalUriPattern.matcher(methodConfig.getLookup()).matches()) {
            String patternedUri = methodConfig.getLookup().replace("*", ".*");
            patternHandlerMappings.put(patternedUri, methodConfig);
        }
    }

    /**
     * 正則路由比對
     *
     * @param messageHandleUri 如 xxx/event/(VIEW|CLICK)
     * @return 比對到的方法或者 null
     */
    private WxMessageRequestMappingInfo getPatternHandlerMapping(String messageHandleUri) {
        for (Map.Entry<String, WxMessageRequestMappingInfo> config1 : patternHandlerMappings.entrySet()) {
            Pattern uriPattern = Pattern.compile(config1.getKey());
            if(uriPattern.matcher(messageHandleUri).matches()) {
                return config1.getValue();
            }
        }
        return null;
    }
    protected ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * Scan beans in the ApplicationContext, detect and register handler methods.
     * @see #isWxMessageHandler(Class)
     * @see #getMappingForMethod(Method, Class)
     */
    protected void initHandlerMethods() {
        if (log.isDebugEnabled()) {
            log.debug("Looking for request mappings in application context: " + getApplicationContext());
        }

        String[] beanNames =
                getApplicationContext().getBeanNamesForType(Object.class);

        for (String beanName : beanNames) {
            if (isWxMessageHandler(getApplicationContext().getType(beanName))){
                detectHandlerMethods(beanName);
            }
        }

    }

    protected boolean isWxMessageHandler(Class<?> beanType) {
        return ((AnnotationUtils.findAnnotation(beanType, WxMessageHandler.class) != null));
    }

    /**
     * Look for handler methods in a handler.
     * @param handler the bean name of a handler or a handler instance
     */
    protected void detectHandlerMethods(final Object handler) {
        Class<?> handlerType =
                (handler instanceof String ? getApplicationContext().getType((String) handler) : handler.getClass());

        // Avoid repeated calls to getMappingForMethod which would rebuild RequestMappingInfo instances
        final Map<Method, WxMessageRequestMappingInfo> mappings = new IdentityHashMap<>();
        final Class<?> userType = ClassUtils.getUserClass(handlerType);

        Set<Method> methods = MethodIntrospector.selectMethods(userType, new ReflectionUtils.MethodFilter() {
            @Override
            public boolean matches(Method method) {
                WxMessageRequestMappingInfo mapping = getMappingForMethod(method, userType);
                if (mapping != null) {
                    mappings.put(method, mapping);
                    return true;
                }
                else {
                    return false;
                }
            }
        });

        for (Method method : methods) {
            registerHandlerMethod(handler, method, mappings.get(method));
        }
    }

    /**
     * Uses method and type-level @{@link RequestMapping} annotations to create
     * the RequestMappingInfo.
     * @return the created RequestMappingInfo, or {@code null} if the method
     * does not have a {@code @RequestMapping} annotation.
     */
    protected WxMessageRequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        WxMessageRequestMappingInfo info = null;
        WxMessageRequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, WxMessageRequestMapping.class);
        if (methodAnnotation != null) {
            info = createRequestMappingInfo(methodAnnotation, handlerType, method);
        }
        return info;
    }

    /**
     * Created a RequestMappingInfo from a RequestMapping annotation.
     */
    protected WxMessageRequestMappingInfo createRequestMappingInfo(WxMessageRequestMapping annotation,
                                                                   Class<?> handlerType, Method method) {
        String uri = annotation.value();
        return new WxMessageRequestMappingInfo(
                uri,
                handlerType,
                method.getName(),
                method.getParameterTypes()[0],
                "auto gen"
        );
    }


    /**
     * Register a handler method and its unique mapping.
     * @param handler the bean name of the handler or the handler instance
     * @param method the method to register
     * @param mapping the mapping conditions associated with the handler method
     * @throws IllegalStateException if another method was already registered
     * under the same mapping
     */
    protected void registerHandlerMethod(Object handler, Method method, WxMessageRequestMappingInfo mapping) {
        // old gen
        this.directHandlerMappings.put(mapping.getLookup(), mapping);
        registerPatternHandlerMapping(mapping);

        // new gen
        HandlerMethod handlerMethod = createHandlerMethod(handler, method);
        springPatternMethodMappings.put(mapping, handlerMethod);

        log.info("Mapped WxMessageHandler {}", mapping);
    }


    protected HandlerMethod createHandlerMethod(Object handler, Method method) {
        HandlerMethod handlerMethod;
        if (handler instanceof String) {
            String beanName = (String) handler;
            handlerMethod = new HandlerMethod(beanName,
                    getApplicationContext().getAutowireCapableBeanFactory(), method);
        }
        else {
            handlerMethod = new HandlerMethod(handler, method);
        }
        return handlerMethod;
    }

}      

handler 的注解配置如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface WxMessageHandler {


    /**
     * 路由映射 url
     *
     * @return 如: xxx/text
     */
    String value() default "";

}      

路由配置 requestMapping 如下:

public class WxMessageRequestMappingInfo {

    /**
     * 消息處理路由
     */
    private String lookup;

    /**
     * 處理類,調用期 handle 方法*/
    private Class<?> handlerClz;

    /**
     * 處理方法名稱
     */
    private String methodName;

    /**
     * 方法參數
     */
    private Class<?> paramClz;

    /**
     * 備注
     */
    private String remark;


    public WxMessageRequestMappingInfo(String lookup, @NotNull Class<?> handlerClz, String methodName,
                                       Class<?> paramClz, String remark) {
        this.lookup = lookup;
        this.handlerClz = handlerClz;
        this.methodName = methodName;
        this.paramClz = paramClz;
        this.remark = remark;
    }

    public String getLookup() {
        return lookup;
    }

    public Class<?> getHandlerClz() {
        return handlerClz;
    }

    public String getMethodName() {
        return methodName;
    }

    public Class<?> getParamClz() {
        return paramClz;
    }

    public String getRemark() {
        return remark;
    }

    @Override
    public String toString() {
        return "\"{[" + lookup + "]}\" onto @" + handlerClz.getName() + "#" + methodName + " : " + remark;
    }
}      

使用時,隻需在類上添加注解 @WxMessageHandler 即可:

@WxMessageHandler      

在方法上加上 路由注解即可: 

@WxMessageRequestMapping(value = "xxx/event/subscribe")      

如:

@WxMessageHandler
public class SxkEventPushServiceImpl implements WxEventPushHandleService {

    @WxMessageRequestMapping(value = "xxx/event/subscribe")
    @Override
    public Object subscribe(WxEventPushSubscribeReqModel reqModel) {
        System.out.println("hello, welcome.")
        return "hello, welcome.";
    }
}      

  方法配置做兩件事:

  1. 在啟動時,将hander 添加的 mappings 中;

  2. 在使用時,從mappings 中擷取 handler 資訊;

其實作原理與 spring 的 handlerMappings 類似!

  這裡的參數解析,隻處理了第一個參數,假設方法隻能使用一個參數!

  找到處理方法後,就可以進行反射調用了!

不要害怕今日的苦,你要相信明天,更苦!

繼續閱讀