微信公眾號現在影響力有目共睹,所以接入其功能也是很正常的。
現在的應用中,有很多是基於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 類似!
這里的參數解析,只處理了第一個參數,假設方法只能使用一個參數!
找到處理方法后,就可以進行反射調用了!