微信公众号现在影响力有目共睹,所以接入其功能也是很正常的。
现在的应用中,有很多是基于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 类似!
这里的参数解析,只处理了第一个参数,假设方法只能使用一个参数!
找到处理方法后,就可以进行反射调用了!