Netty URL路由方案探討
最近在用Netty做開發,需要提供一個http web server,供呼叫方呼叫。採用Netty本身提供的HttpServerCodec
handler進行Http協議的解析,但是需要自己提供路由。
最開始是通過對Http method及uri 採用多層if else 巢狀判斷的方法路由到真正的controller類:
String uri = request.uri(); HttpMethod method = request.method(); if (method == HttpMethod.POST) { if (uri.startsWith("/login")) { //url引數解析,呼叫controller的方法 } else if (uri.startsWith("/logout")) { //同上 } } else if (method == HttpMethod.GET) { if (uri.startsWith("/")) { } else if (uri.startsWith("/status")) { } }
在只需提供login
及logout
API時,程式碼可以完成功能,可是隨著API的數量越來越多,需要支援的方法及uri越來越多,else if
越來越多,程式碼越來越複雜。
在阿里開發手冊中也提到過:
因此首先考慮採用狀態設計模式及策略設計模式重構。
狀態模式
狀態模式的角色:
-
state狀態
表示狀態,定義了根據不同狀態進行不同處理的介面,該介面是那些處理內容依賴於狀態的方法集合,對應例項的state類 -
具體的狀態
實現了state介面,對應daystate和nightstate -
context
context持有當前狀態的具體狀態的例項,此外,他還定義了供外部呼叫者使用的狀態模式的介面。
首先我們知道每個http請求都是由method及uri來唯一標識的,所謂路由就是通過這個唯一標識定位到controller類的中的某個方法。
因此把HttpLabel作為狀態
@Data @AllArgsConstructor public class HttpLabel { private String uri; private HttpMethod method; }
狀態介面:
public interface Route { /** * 路由 * * @param request * @return */ GeneralResponse call(FullHttpRequest request); }
為每個狀態新增狀態實現:
public void route() { //單例controller類 final DemoController demoController = DemoController.getInstance(); Map<HttpLabel, Route> map = new HashMap<>(); map.put(new HttpLabel("/login", HttpMethod.POST), demoController::login); map.put(new HttpLabel("/logout", HttpMethod.POST), demoController::login); }
接到請求,判斷狀態,呼叫不同介面:
public class ServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> { @Override public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) { String uri = request.uri(); GeneralResponse generalResponse; if (uri.contains("?")) { uri = uri.substring(0, uri.indexOf("?")); } Route route = map.get(new HttpLabel(uri, request.method())); if (route != null) { ResponseUtil.response(ctx, request, route.call(request)); } else { generalResponse = new GeneralResponse(HttpResponseStatus.BAD_REQUEST, "請檢查你的請求方法及url", null); ResponseUtil.response(ctx, request, generalResponse); } } }
使用狀態設計模式重構程式碼,在增加url時只需要網map裡面put一個值就行了。
類似SpringMVC路由功能
後來看了ofollow,noindex" target="_blank">JAVA反射+執行時註解實現URL路由 發現反射+註解的方式很優雅,程式碼也不復雜。
下面介紹Netty使用反射實現URL路由。
路由註解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequestMapping { /** * 路由的uri * * @return */ String uri(); /** * 路由的方法 * * @return */ String method(); }
掃描classpath下帶有@RequestMapping
註解的方法,將這個方法放進一個路由Map:Map<HttpLabel, Action<GeneralResponse>> httpRouterAction
,key為上面提到過的Http唯一標識HttpLabel
,value為通過反射呼叫的方法:
@Slf4j public class HttpRouter extends ClassLoader { private Map<HttpLabel, Action<GeneralResponse>> httpRouterAction = new HashMap<>(); private String classpath = this.getClass().getResource("").getPath(); private Map<String, Object> controllerBeans = new HashMap<>(); @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String path = classpath + name.replaceAll("\\.", "/"); byte[] bytes; try (InputStream ins = new FileInputStream(path)) { try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { byte[] buffer = new byte[1024 * 5]; int b = 0; while ((b = ins.read(buffer)) != -1) { out.write(buffer, 0, b); } bytes = out.toByteArray(); } } catch (Exception e) { throw new ClassNotFoundException(); } return defineClass(name, bytes, 0, bytes.length); } public void addRouter(String controllerClass) { try { Class<?> cls = loadClass(controllerClass); Method[] methods = cls.getDeclaredMethods(); for (Method invokeMethod : methods) { Annotation[] annotations = invokeMethod.getAnnotations(); for (Annotation annotation : annotations) { if (annotation.annotationType() == RequestMapping.class) { RequestMapping requestMapping = (RequestMapping) annotation; String uri = requestMapping.uri(); String httpMethod = requestMapping.method().toUpperCase(); // 儲存Bean單例 if (!controllerBeans.containsKey(cls.getName())) { controllerBeans.put(cls.getName(), cls.newInstance()); } Action action = new Action(controllerBeans.get(cls.getName()), invokeMethod); //如果需要FullHttpRequest,就注入FullHttpRequest物件 Class[] params = invokeMethod.getParameterTypes(); if (params.length == 1 && params[0] == FullHttpRequest.class) { action.setInjectionFullhttprequest(true); } // 儲存對映關係 httpRouterAction.put(new HttpLabel(uri, new HttpMethod(httpMethod)), action); } } } } catch (Exception e) { log.warn("{}", e); } } public Action getRoute(HttpLabel httpLabel) { return httpRouterAction.get(httpLabel); } }
通過反射呼叫controller
類中的方法:
@Data @RequiredArgsConstructor @Slf4j public class Action<T> { @NonNull private Object object; @NonNull private Method method; private boolean injectionFullhttprequest; public T call(Object... args) { try { return (T) method.invoke(object, args); } catch (IllegalAccessException | InvocationTargetException e) { log.warn("{}", e); } return null; }
ServerHandler.java
處理如下:
//根據不同的請求API做不同的處理(路由分發) Action<GeneralResponse> action = httpRouter.getRoute(new HttpLabel(uri, request.method())); if (action != null) { if (action.isInjectionFullhttprequest()) { ResponseUtil.response(ctx, request, action.call(request)); } else { ResponseUtil.response(ctx, request, action.call()); } } else { //錯誤處理 generalResponse = new GeneralResponse(HttpResponseStatus.BAD_REQUEST, "請檢查你的請求方法及url", null); ResponseUtil.response(ctx, request, generalResponse); }
測試結果如下:

完整程式碼在 https://github.com/morethink/Netty-Route