😀 事件经过
有一天,一如既往的在公司摸鱼,突然,某位好基友在基友群十分认真了问了句:安仔,我有好多的 if 判断,有没有其他办法解决呀?
这能难倒身经百战的安仔么?我不加思索立马回他:switch 啊。
没办法,乐于助人毕竟是我行走江湖的招牌啊。😉
😀 思考
不过说真的,安仔在很多上线的项目中发现也有海量的 if 判断,如下形式:
if(type == 1) {
业务1();
}
if(type == 2) {
业务2();
}
if(type == 3) {
业务3();
}
......
可是安仔思前想后,觉得 switch 也并不是最好的解决办法呀,毕竟代码量还是在那,各位平时如何解决大量 if 的判断呢?(不是真用的 switch 吧??)
😀 调查
有个特别牛逼的开源框架叫做 SpringMVC 给了我极大的灵感,传说它有个叫 HandlerMapping 的家伙能帮助我们找到匹配 Http Method 和 URI 的方法逻辑。也就是 @RequestMapping 注解里面的 method 和 value 属性。
那么我们在 Controller 中写了那么多方法,这个 HandlerMapping 家伙是怎么通过一个 Http 请求找到那个符合的方法呢?不会是通过 if 一个一个判断吧?
安仔决定给大家破这个案。苦苦翻完了 SpringMVC 全部代码,可是并没见着大量 的 if 代码。果然,这案子还是有点难度,暴力破解失败!!😭
难倒还有什么玄机不成,让我们采访一下老大哥 DispatcherServlet , 听说他是 SpingMVC 的核心。
在我们的严刑逼问下,发现一切怀疑都指向了DispatcherServlet 的 doDispatch() 方法,并在这个方法中我们发现了 getHandler() 方法充满了嫌疑。
// 这个方法可以说是 DispatcherServlet 的核心,看明白了有助于理解 SpringMVC
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);
// 获取Handler 重点
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 以下代码省略
通过对 getHandler() 的走访,发现了重要的蛛丝马迹,它通过 mapping 对象的 getHandler() 方法获取了一个处理器链。这个 mapping 对象就是本案的的核心 HandlerMapping*,真相也许马上水落石出。让我们继续调查下去。
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request); // 重点这里
if (handler != null) {
return handler;
}
}
}
return null;
}
遗憾的是 HandlerMapping 是一个接口,我们需要在运行时找到它的实现类对象,才能继续排查。经过断点运行,一切指向了 HandlerMapping 的间接子类 RequestMappingHandlerMapping 。但在 RequestMappingHandlerMapping 中没有找到 getHandler() 方法的实现,而在它的间接父抽象类 AbstractHandlerMapping 找到了 getHandler() 方法的实现:
@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
// 获取 handler **重点**
Object handler = getHandlerInternal(request);
if (handler == null) {
handler = getDefaultHandler();
}
if (handler == null) {
return null;
}
// Bean name or resolved handler?
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
// 获取执行链 (HandlerExecutionChain 里面包含了 handler 和 拦截器 list )
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
if (logger.isTraceEnabled()) {
logger.trace("Mapped to " + handler);
}
else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {
logger.debug("Mapped to " + executionChain.getHandler());
}
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null);
CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
config = (config != null ? config.combine(handlerConfig) : handlerConfig);
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;
}
我们知道一般 HandlerMapping 会返回一个 HandlerExecutionChain 执行链 ,里面包含我们需要的 handler 和一群拦截器(包含我们自己定义的拦截器),之后 HandlerAdapter 执行 Handler 时会帮你处理拦截器的代码。所以接下来就是探秘 **getHandlerInternal(request)**方法了。
getHandlerInternal(request) 方法在 AbstractUrlHandlerMapping 和 AbstractHandlerMethodMapping 中各有实现,顾名思义,一个是针对 URL 的,一个是针对方法的。我们康康针对 URL 的吧!
@Override
@Nullable
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
// 获取 path
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
request.setAttribute(LOOKUP_PATH, lookupPath);
// 找 handler ,【重点】
Object handler = lookupHandler(lookupPath, request);
if (handler == null) {
// We need to care for the default handler directly, since we need to
// expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well.
Object rawHandler = null;
if ("/".equals(lookupPath)) {
rawHandler = getRootHandler();
}
if (rawHandler == null) {
rawHandler = getDefaultHandler();
}
if (rawHandler != null) {
// Bean name or resolved handler?
if (rawHandler instanceof String) {
String handlerName = (String) rawHandler;
rawHandler = obtainApplicationContext().getBean(handlerName);
}
validateHandler(rawHandler, request);
handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);
}
}
return handler;
}
接下来就是继续看 lookupHandler(lookupPath, request) 方法了。方法有点长,截取了一部分
@Nullable
protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
// Direct match? 原注释:直接匹配? 从 handlerMap 中直接获取
Object handler = this.handlerMap.get(urlPath);
if (handler != null) {
// Bean name or resolved handler?
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
validateHandler(handler, request);
return buildPathExposingHandler(handler, urlPath, urlPath, null);
}
// Pattern match? 原注释:匹配匹配? 从 handlerMap 中匹配获取
List<String> matchingPatterns = new ArrayList<>();
for (String registeredPattern : this.handlerMap.keySet()) {
if (getPathMatcher().match(registeredPattern, urlPath)) {
matchingPatterns.add(registeredPattern);
}
else if (useTrailingSlashMatch()) {
if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) {
matchingPatterns.add(registeredPattern + "/");
}
}
}
......
看到这里,相信你明白一些了,SpringMVC 就是把 URL 和 Handler 在初始化时放在了一个 Map(handlerMap) 中,获取的时候直接从这个 Map 中获取。那么,SpringMVC 是如何放入这个 Map 的呢?让我们康康初始化的过程。这次我直接放核心代码吧,源码粘贴多了容易晕。
protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
// 上面省略一下代码
Object mappedHandler = this.handlerMap.get(urlPath);
if (mappedHandler != null) {
if (mappedHandler != resolvedHandler) {
throw new IllegalStateException(
"Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
"]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
}
}
else {
if (urlPath.equals("/")) {
if (logger.isTraceEnabled()) {
logger.trace("Root mapping to " + getHandlerDescription(handler));
}
setRootHandler(resolvedHandler);
}
else if (urlPath.equals("/*")) {
if (logger.isTraceEnabled()) {
logger.trace("Default mapping to " + getHandlerDescription(handler));
}
setDefaultHandler(resolvedHandler);
}
else {
// 康康这里哦,放进去了吧
this.handlerMap.put(urlPath, resolvedHandler);
if (logger.isTraceEnabled()) {
logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler));
}
}
}
}
SpringMVC 在初始化时通过 反射 获取到我们写的 Controller 中的方法,把合理的(在调用这个方法的方法里进行了判断)放入上面提到的 Map 中,为了证明这个过程,我追踪到 AbstractHandlerMethodMapping 的下面这个方法:
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
// 下面这句代码就是注册了
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
不仅初始化放入 Map 时使用了反射获取每个 Handler,被 Adapter 调用时也使用反射调用的,最终追踪到了InvocableHandlerMethod中 的 doInvoke() :
@Nullable
protected Object doInvoke(Object... args) throws Exception {
ReflectionUtils.makeAccessible(getBridgedMethod());
try {
// 这里反射调了我们自己写的方法逻辑 即 method.invoke()
return getBridgedMethod().invoke(getBean(), args);
}
// 以下代码省略
}
😀 破案
总的的来说,就是 SpringMVC 的 HandlerMapping 在初始化时会通过反射拿到我们的个Handler(也就是我们 Controller 中的自己写的方法 Method ),放到一个 handlerMap 中进行,URL 为 key,handler 为 value,在获取时直接从 Map 中获取 Method,在 Adapter 调用时通过 method.invoke() 方法反射调用。
😀 总结
SpingMVC 避免大量 if 判断的原理在于提前通过反射建立 条件 => 方法 的规则,并将这个规则用 map 存起来,用 map 存主要因为反射的效率偏低(特别是 setAccessible(true) 即开启安全检查,效率更低),核心在于反射来动态做判断可以避免大量 if 。
😀 实践
前段时间,我自己在写一个聊天的应用,后端使用 netty 来处理 socket 的连接,前端使用了 websocket 通信,对于每一个来自前端的消息,我需要先判断是正常聊天消息还是其他消息(比如心跳,添加好友,同意好友等),我为消息设置了 type 字段来区分消息类型,可是我需要进行消息类型的判断,如果我使用 if 来判断,那将是个灾难,因为我无法确定有多少种类型。
于是我效仿 SpingMVC 这种 Mapping 反射动态判断的思想,完成了判断,以下是部分代码逻辑(伪代码不可运行):
<1> 设计一个注解用于标注到自定义的方法上:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited // 子类可以继承
public @interface Handle {
// 通过这个类型来判断
String type() default "default";
}
<2> 业务使用该注解:
public class MessageHandler {
// 处理聊天消息的逻辑
@Handle(type = "chat_message")
@Override
public void ChatMessage() {
// 省略具体逻辑
}
// 验证token的逻辑
@Handle(type = "token_verify")
@Override
public void tokenVerify() {
// 省略具体逻辑
}
// 获取好友的逻辑
@Handle(type = "get_friends")
@Override
public void getFriends() {
// 省略具体逻辑
}
...
}
<2> 设计 Mapping 来动态判断和调用:
@Component
@RequiredArgsConstructor
public class MessageTypeHandlerMapping implements HandlerMapping {
// 注入业务类
private final MessageHandler messageHandler;
// 省略部分代码
/**
* mapping 处理
* @param webSocketFrame
* @param channel
*/
@Override
public void handle(Message message) {
String type = message.getType();
// getClass() 拿到的是代理类 getSuperclass() 能获取真实的类
for (Method method : MessageHandler.getClass().getSuperclass().getMethods()) {
// 判断这个方法上是否有 Handle 注解
if(method.isAnnotationPresent(Handle.class)) {
Handle annotation = method.getAnnotation(Handle.class);
// 判断注解的类型是否一致
if (annotation.type().value().equals(type)) {
method.setAccessible(true);
// 反射调用
method.invoke(messageHandler,null);
break;
}
}
}
}
}
当然,安仔这个代码省去了 SpringMVC 中 HandlerMap 和 HandlerAdapter 的设计,大家明白思想就行,不必较真哈 ~
原文: 【好基友问:if 判断太多,不想写重复代码怎么办?】