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 is also mentioned in the Ali Development manual:
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:
Complete code at github.com/morethink/N…