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")) {

    }
}

在只需提供loginlogoutAPI時,代碼可以完成功能,可是隨着API的數量越來越多,需要支持的方法及uri越來越多,else if 越來越多,代碼越來越復雜。

time-for-change

在阿里開發手冊中也提到過:

因此首先考慮采用狀態設計模式及策略設計模式重構。

狀態模式

狀態模式的角色:

  • 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一個值就行了。

Netty實現類似SpringMVC路由

后來看了 JAVA反射+運行時注解實現URL路由 發現反射+注解的方式很優雅,代碼也不復雜。

下面介紹Netty使用反射實現URL路由。

路由注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    /**
     * 路由的uri
     *
     * @return
     */
    String uri();

    /**
     * 路由的方法
     *
     * @return
     */
    String method();
}

json格式的body

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestBody {

}

異常類(用於全局異常處理,實現 @ControllerAdvice 異常處理)

@Data
public class MyRuntimeException extends RuntimeException {

    private GeneralResponse generalResponse;

    public MyRuntimeException(String message) {
        generalResponse = new GeneralResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, message);
    }

    public MyRuntimeException(HttpResponseStatus status, String message) {
        generalResponse = new GeneralResponse(status, message);
    }

    public MyRuntimeException(GeneralResponse generalResponse) {
        this.generalResponse = generalResponse;
    }

    @Override
    public synchronized Throwable fillInStackTrace() {
        return this;
    }
}

掃描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 {
    @NonNull
    private Object object;
    @NonNull
    private Method method;

    private List<Class> paramsClassList;

    public GeneralResponse call(Object... args) {
        try {
            return (GeneralResponse) method.invoke(object, args);
        } catch (InvocationTargetException e) {
            Throwable targetException = e.getTargetException();
            //實現 `@ControllerAdvice` 異常處理,直接拋出自定義異常
            if (targetException instanceof MyRuntimeException) {
                return ((MyRuntimeException) targetException).getGeneralResponse();
            }
            log.warn("method invoke error: {}", e);
            return new GeneralResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, String.format("Internal Error: %s", ExceptionUtils.getRootCause(e)), null);
        } catch (IllegalAccessException e) {
            log.warn("method invoke error: {}", e);
            return new GeneralResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, String.format("Internal Error: %s", ExceptionUtils.getRootCause(e)), null);
        }
    }
}

ServerHandler.java處理如下:

public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
    String uri = request.uri();
    GeneralResponse generalResponse;
    if (uri.contains(DELIMITER)) {
        uri = uri.substring(0, uri.indexOf(DELIMITER));
    }
    //根據不同的請求API做不同的處理(路由分發)
    Action action = httpRouter.getRoute(new HttpLabel(uri, request.method()));
    if (action != null) {
        String s = request.uri();
        if (request.headers().get(HttpHeaderNames.CONTENT_TYPE.toString()).equals(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString())) {
            s = s + "&" + request.content().toString(StandardCharsets.UTF_8);
        }
        QueryStringDecoder queryStringDecoder = new QueryStringDecoder(s);
        Map<String, List<String>> parameters = queryStringDecoder.parameters();
        Class[] classes = action.getMethod().getParameterTypes();
        Object[] objects = new Object[classes.length];
        for (int i = 0; i < classes.length; i++) {
            Class c = classes[i];
            //處理@RequestBody注解
            Annotation[] parameterAnnotation = action.getMethod().getParameterAnnotations()[i];
            if (parameterAnnotation.length > 0) {
                for (int j = 0; j < parameterAnnotation.length; j++) {
                    if (parameterAnnotation[j].annotationType() == RequestBody.class &&
                            request.headers().get(HttpHeaderNames.CONTENT_TYPE.toString()).equals(HttpHeaderValues.APPLICATION_JSON.toString())) {
                        objects[i] = JsonUtil.fromJson(request, c);
                    }
                }
                //處理數組類型
            } else if (c.isArray()) {
                String paramName = action.getMethod().getParameters()[i].getName();
                List<String> paramList = parameters.get(paramName);
                if (CollectionUtils.isNotEmpty(paramList)) {
                    objects[i] = ParamParser.INSTANCE.parseArray(c.getComponentType(), paramList);
                }
            } else {
                //處理基本類型和string
                String paramName = action.getMethod().getParameters()[i].getName();
                List<String> paramList = parameters.get(paramName);
                if (CollectionUtils.isNotEmpty(paramList)) {
                    objects[i] = ParamParser.INSTANCE.parseValue(c, paramList.get(0));
                } else {
                    objects[i] = ParamParser.INSTANCE.parseValue(c, null);
                }
            }
        }
        ResponseUtil.response(ctx, HttpUtil.isKeepAlive(request), action.call(objects));
    } else {
        //錯誤處理
        generalResponse = new GeneralResponse(HttpResponseStatus.BAD_REQUEST, "請檢查你的請求方法及url", null);
        ResponseUtil.response(ctx, HttpUtil.isKeepAlive(request), generalResponse);
    }
}

DemoController 方法配置:

@RequestMapping(uri = "/login", method = "POST")
    public GeneralResponse login(@RequestBody User user, FullHttpRequest request,
                                 String test, Integer test1, int test2,
                                 long[] test3, Long test4, String[] test5, int[] test6) {
        System.out.println(test2);
        log.info("/login called,user: {} ,{} ,{} {} {} {} {} {} {} {} ", user, test, test1, test2, test3, test4, test5, test6);
        return new GeneralResponse(null);
    }

測試結果如下:

netty-route 得到結果如下:

user=User(username=hah, password=dd),test=111,test1=null,test2=0,test3=[1],test4=null,test5=[d,a, 1],test6=[1, 2]

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM