Recently in Netty development, need to provide an HTTP Web server, for the caller to call. The HttpServerCodec handler provided by Netty is used to parse the Http protocol, but it needs to provide its own route.

We start by routing to the real Controller class with a multi-layer if else nested judgment on Http methods and URIs:

String uri = request.uri();
HttpMethod method = request.method();
if (method == HttpMethod.POST) {
    if (uri.startsWith("/login")) {
        // call the controller method
    } else if (uri.startsWith("/logout")) {
        / / same as above}}else if (method == HttpMethod.GET) {
    if (uri.startsWith("/")) {}else if (uri.startsWith("/status")) {}}Copy the code

When only login and Logout apis are provided, the code can do the job, but as the number of apis increases, the number of methods and URIs that need to be supported increases, and the number of else Ifs increases, the code becomes more complex.

It’s time to think about refactoring

It is also mentioned in the Ali Development manual:

Refactor multiple else Ifs

Therefore, state design mode and strategy design mode are considered first.

The state pattern

State mode roles:

  • State represents state and defines an interface for handling different states. The interface is a collection of methods that handle content dependent on state, corresponding to the state class of the instance
  • The specific state implements the state interface, corresponding to dayState and nightState
  • Context Context holds an instance of a specific state of the current state, and it defines an interface to the state pattern for use by external callers.

First of all, we know that each HTTP request is uniquely identified by a method and uri. The route is located to a method in the Controller class by this unique identifier.

So HttpLabel as the state

@Data
@AllArgsConstructor
public class HttpLabel {
    private String uri;
    private HttpMethod method;
}

Copy the code

Status interface:

public interface Route {
    /** * route **@param request
     * @return* /
    GeneralResponse call(FullHttpRequest request);
}
Copy the code

Add state implementations for each state:

public void route(a) {
    // Singleton Controller class
    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);
}
Copy the code

Upon receiving the request, judge the status and call different interfaces:

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, "Please check your request method and URL.".null); ResponseUtil.response(ctx, request, generalResponse); }}}Copy the code

Refactoring code using state design mode requires only a put value in the netmap to add urls.

Similar to SpringMVC routing functionality

Later, I looked at JAVA reflection + runtime annotations to implement URL routing and found that reflection + annotations are very elegant, the code is not complex.

Netty uses reflection to implement URL routing.

Routing comments:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    /** * Route URI **@return* /
    String uri(a);

    /** * Route method **@return* /
    String method(a);
}
Copy the code

Scan the classpath for methods annotated with @requestMapping and place the method in a routing Map: Map

> httpRouterAction, key is the Http unique identifier mentioned above, value is the method called by reflection:
,>

@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
    protectedClass<? > 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();
                        // Save the Bean singleton
                        if(! controllerBeans.containsKey(cls.getName())) { controllerBeans.put(cls.getName(), cls.newInstance()); } Action action =new Action(controllerBeans.get(cls.getName()), invokeMethod);
                        // If you need FullHttpRequest, inject the FullHttpRequest object
                        Class[] params = invokeMethod.getParameterTypes();
                        if (params.length == 1 && params[0] == FullHttpRequest.class) {
                            action.setInjectionFullhttprequest(true);
                        }
                        // Save the mapping
                        httpRouterAction.put(new HttpLabel(uri, newHttpMethod(httpMethod)), action); }}}}catch (Exception e) {
            log.warn("{}", e); }}public Action getRoute(HttpLabel httpLabel) {
        returnhttpRouterAction.get(httpLabel); }}Copy the code

Call a method in the Controller class via reflection:

@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;
    }
Copy the code

Serverhandler.java handles the following:

 // Do different processing according to different request API (route distribution)
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 {
    // Error handling
    generalResponse = new GeneralResponse(HttpResponseStatus.BAD_REQUEST, "Please check your request method and URL.".null);
    ResponseUtil.response(ctx, request, generalResponse);
}
Copy the code

DemoController method configuration:

@RequestMapping(uri = "/login", method = "POST")
public GeneralResponse login(FullHttpRequest request) {
    User user = JsonUtil.fromJson(request, User.class);
    log.info("/login called,user: {}", user);
    return new GeneralResponse(null);
}
Copy the code

The test results are as follows:

The test results

Complete code at github.com/morethink/N…