基于VertxWeb的SrpingMVC风格注解实现一

45次阅读

共计 9381 个字符,预计需要花费 24 分钟才能阅读完成。

前言

最近了解到了 vertx 这个异步框架,但平时用的比较多的还是 spring,出于好奇,尝试基于 vertx web 去实现 spring mvc 风格注解。

最终效果如下所示

@Slf4j
@RestController
public class HelloController {@RequestMapping("hello/world")
    public String helloWorld() {return "Hello world and nintha veladder";}

    @RequestMapping("echo")
    public Map<String, Object> echo(String message, Long token, int code, RoutingContext ctx) {log.info("uri={}", ctx.request().absoluteURI());

        log.info("message={}, token={}, code={}", message, token, code);
        HashMap<String, Object> map = new HashMap<>();
        map.put("message", message);
        map.put("token", token);
        map.put("code", code);
        return map;
    }

    @RequestMapping(value = "hello/array")
    public List<Map.Entry<String, String>> helloArray(long[] ids, String[] names, RoutingContext ctx) {log.info("ids={}", Arrays.toString(ids));
        log.info("names={}", Arrays.toString(names));
        return ctx.request().params().entries();}

    @RequestMapping("query/list")
    public List<Map.Entry<String, String>> queryArray(List<Long> ids, TreeSet<String> names, LinkedList rawList, RoutingContext ctx) {log.info("ids={}", ids);
        log.info("names={}", names);
        log.info("rawList={}", rawList);
        return ctx.request().params().entries();}

    @RequestMapping("query/bean")
    public BeanReq queryBean(BeanReq req) {log.info("req={}", req);
        return req;
    }

    @RequestMapping(value = "post/body", method = HttpMethod.POST)
    public BeanReq postRequestBody(@RequestBody BeanReq req) {log.info("req={}", req);
        return req;
    }
}

完整实现见 github repo: veladder

@RestContoller

现在主流 spring mvc 的控制器一般是使用 @RestController 注解,它的主要作用是告诉框架这个类是请求控制器,让框架主动扫描并加载这个类。

这个注解本质是个标记注解,所以目前不需要其他字段。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RestController {}

由于目前没有实现任何 DI(依赖注入),为了方便我们直接在代码里面手动创建实例并加载。

@Override
public void start() throws Exception {HttpServer server = vertx.createHttpServer();

    Router router = Router.router(vertx);
    routerMapping(new HelloController(), router);

    server.requestHandler(router).listen(port, ar -> {if (ar.succeeded()) {log.info("HTTP Server is listening on {}", port);
        } else {log.error("Failed to run HTTP Server", ar.cause());
        }
    });
}

上面这段代码是在 Verticle 里面启动一个 HTTP 服务器,并进行路由构建。里面的 routerMapping 方法我们下面实现一下

private <ControllerType> void routerMapping(ControllerType annotatedBean, Router router) throws NotFoundException {Class<ControllerType> clazz = (Class<ControllerType>) annotatedBean.getClass();
    if (!clazz.isAnnotationPresent(RestController.class)) {return;}
    
    // other code
}

先简单判断下这个加载的类是否为带 @RestController 注解。好了,@RestContoller注解的任务已经完成了。

@RequestMapping

@RequestMapping注解的作用是把请求路径和我们的处理逻辑关联在一起。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequestMapping {String value() default "";

    HttpMethod[] method() default {};}

value 对应的 path,而 method 对应 http 的请求方法,如 GET、POST 等。使用效果如下所示:

@RequestMapping(value = "post/body", method = HttpMethod.POST)

我们继续上一节的路由解析。

首先我们要获取到控制器类中被该注解标记的方法

Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {if (!method.isAnnotationPresent(RequestMapping.class)) continue;
    
    // ... handle method code
}

这里使用了反射,并且只处理了带 @RequestMapping 注解的方法,然后从注解中获取请求路径和请求方法,具体代码如下所示

// ... handle method code
RequestMapping methodAnno = method.getAnnotation(RequestMapping.class);
String requestPath = methodAnno.value();

Handler<RoutingContext> requestHandler = ctx -> {// ... call annotated method and inject parameters};

// bind handler to router
HttpMethod[] httpMethods = methodAnno.method();
if (httpMethods.length == 0) {
    // 默认绑定全部 HttpMethod
    router.route(formatPath).handler(BodyHandler.create()).handler(requestHandler);
} else {for (HttpMethod httpMethod : httpMethods) {
        router
            .route(httpMethod, formatPath)
            .handler(BodyHandler.create())
            .handler(requestHandler);
    }
}

httpMethods 这个参数我们采用了数组类型,这样一个处理函数可以绑定到多个 HttpMethod 中。这里还做了一个特殊处理,当用户省略这个参数时,将绑定全部 HttpMethod,毕竟不绑定 HttpMethod 的处理函数没有意义。

上面代码中的 requestHandler 的功能是调用被注解的处理函数并注入所需要的参数,并将函数返回值转换为合适的格式再返回给请求者。个人感觉强大的参数注入功能可以极大的提升开发者的编码体验。接下来我们实现下requestHandler

返回值处理

返回值一般有多种情况:

  1. void 类型,这种是无返回值,一般是特殊情况会使用。比如下载文件,无法以 JSON 形式返回,需要调用 RoutingContext 进行处理。
  2. 基础类型和字符串,直接转换为 JSON 中的数字和字符串就好。
  3. 其他类型,当 POJO 处理,直接序列化成 JSON

处理返回值代码如下所示:

// Write to the response and end it
Consumer<Object> responseEnd = x -> {if (method.getReturnType() == void.class) return;

    HttpServerResponse response = ctx.response();
    response.putHeader(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
    response.end(x instanceof CharSequence ? x.toString() : Json.encode(x));
};

Void 类型就直接 return,不做处理。

再判断是否为字符串类型,字符串类型可以直接作为返回内容,其余类型走 JSON 序列化。

参数处理

vertx 请求参数的内容都可以通过 RoutingContext 对象获取到,无论是在 URI 里的 query 还是 body 里面的 formdata 或者 JSON 形式数据,都是可以的。

MultiMap params = ctx.request().params();

一个典型的请求处理函数如下所示:

@RequestMapping("echo")
public Map<String, Object> echo(String message, Long token, int code, RoutingContext ctx) {log.info("uri={}", ctx.request().absoluteURI());

    log.info("message={}, token={}, code={}", message, token, code);
    HashMap<String, Object> map = new HashMap<>();
    map.put("message", message);
    map.put("token", token);
    map.put("code", code);
    return map;
}

echo 函数有 4 个参数,我们需要对它进行反射获取 4 个参数的类型和名字。获取类型可以方便我们对数据进行类型转换,对于一些特殊的类型还有进行专门的处理。参数类型比较好获取,反射 API 可以直接获取到

Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {Class<?>[] paramTypes = method.getParameterTypes();}

参数名字就相对复杂一点,虽然 JDK8 里面提供了对方法形参名反射的 API,但它存在限制

Parameter[] parameters = method.getParameters();
for (Parameter p: Parameters){
    String name = p.getName;
    // ...
}

如果在编译代码的时候没有加上 –parameters 参数,那么 Parameter#getName 拿到的是 arg0 这样的占位符,毕竟 java 还是要向前兼容的嘛。

正常途径还真不好拿到形参名,所以像 MyBatis 这类的框架是让用户通过注解进行形参名标注

public interface DemoMapper {List<Card> getCardList(@Param("cardIds") List<Integer> cardIds);
    Card getCard(@Param("cardId") int cardId);
}

@Param允许用户使用和形参名不一样的值,类似别名,功能上更加灵活,但是大部分情况下,我们就希望直接使用形参名,重复的内容写两遍还是比较不友好的。

但是 spring 就支持直接获取方法形参名。在强大的搜索引擎帮助下,可以发现 spring 里面使用了字节码方式去获取方法形参(具体内容请自行 google),这里我们使用 javassist 来实现这个功能,这样就不需要自己去处理字节码了。

// javassist 获取反射类
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(clazz));
CtClass cc = classPool.get(clazz.getName());

// 反射获取方法实体
CtMethod ctMethod = cc.getDeclaredMethod(method.getName());
MethodInfo methodInfo = ctMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
// 获取本地变量表,里面有形参信息
LocalVariableAttribute attribute = 
    (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);

Class<?>[] paramTypes = method.getParameterTypes();
String[] paramNames = new String[ctMethod.getParameterTypes().length];
if (attribute != null) {
    // 通过 javassist 获取方法形参,成员方法 0 位变量是 this
    int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1;
    for (int i = 0; i < paramNames.length; i++) {paramNames[i] = attribute.variableName(i + pos);
    }
}

javassist可以获取到方法的LocalVariableAttribute,这里面有我们需要的形参信息,包括形参名。

获取到对应的形参名后,我们可以靠这个获取请求的参数:

// 单个值
String value = ctx.request().params().get(paramName);
// 数组或集合
List<String> values = ctx.request().params().getAll(paramName);

接着我们来注入参数值,需要处理的参数类型可以分为下列几种:

  1. 简单类型,基础类型和它们的包装类,再加上字符串
  2. 集合类型,java.util.Collection<T>及其子类,泛型参数 T 支持简单类型
  3. 数组类型,数组的组件类型 (ComponentType) 支持简单类型
  4. 数据类型,类似 POJO 的数据载体类,里面可以再嵌套数据类型或其他类型
  5. 特殊类型,比如 FileUpload 和 RoutingContext,需要特殊处理

简单类型处理只需要把基础类型都转换为包装类型,再反射调用 valueOf 方法就可以了

 private <T> T parseSimpleType(String value, Class<T> targetClass) throws Throwable {if (StringUtils.isBlank(value)) return null;

     Class<?> wrapType = Primitives.wrap(targetClass);
     if (Primitives.allWrapperTypes().contains(wrapType)) {MethodHandle valueOf = MethodHandles.lookup().unreflect(wrapType.getMethod("valueOf", String.class));
         return (T) valueOf.invoke(value);
     } else if (targetClass == String.class) {return (T) value;
     }

     return null;
 }

集合类型,反射获取到泛型类型后按简单类型处理

private Collection parseCollectionType(List<String> values, Type genericParameterType) throws Throwable {
    Class<?> actualTypeArgument = String.class; // 无泛型参数默认用 String 类型
    Class<?> rawType;
    // 参数带泛型
    if (genericParameterType instanceof ParameterizedType) {ParameterizedType parameterType = (ParameterizedType) genericParameterType;
        actualTypeArgument = (Class<?>) parameterType.getActualTypeArguments()[0];
        rawType = (Class<?>) parameterType.getRawType();} else {rawType = (Class<?>) genericParameterType;
    }

    Collection coll;
    if (rawType == List.class) {coll = new ArrayList<>();
    } else if (rawType == Set.class) {coll = new HashSet<>();
    } else {coll = (Collection) rawType.newInstance();}

    for (String value : values) {coll.add(parseSimpleType(value, actualTypeArgument));
    }
    return coll;
}

数组类型,反射获取到组件类型后按简单类型处理

if (paramType.isArray()) {
    // 数组元素类型
    Class<?> componentType = paramType.getComponentType();

    List<String> values = allParams.getAll(paramName);
    Object array = Array.newInstance(componentType, values.size());
    for (int j = 0; j < values.size(); j++) {Array.set(array, j, parseSimpleType(values.get(j), componentType));
    }
    return array;
}

数据类型,反射获取类型中字段的类型,递归处理

private Object parseBeanType(MultiMap allParams, Class<?> paramType) throws Throwable {Object bean = paramType.newInstance();
    Field[] fields = paramType.getDeclaredFields();
    for (Field field : fields) {
        Object value = parseSimpleTypeOrArrayOrCollection(allParams, field.getType(), field.getName(), field.getGenericType());

        field.setAccessible(true);
        field.set(bean, value);
    }
    return bean;
}

特殊类型,其中 RoutingContext 类型不会作为请求参数,而是由 route 中获取并注入。

if (paramType == RoutingContext.class) {return ctx;} else if (paramType == FileUpload.class) {Set<FileUpload> uploads = ctx.fileUploads();
    Map<String, FileUpload> uploadMap = uploads
        .stream()
        .collect(Collectors.toMap(FileUpload::name, x -> x));
    return uploadMap.get(paramNames[i]);
}

好了,现在 @RequestMapping 的功能已经实现了。

@RequestBody

默认情况下用户进行 POST 请求,数据是以表单形式提交的,有些时候我们需要以 JSON 形式提交,那么本注解就是实现这样的功能。

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

这里处理方式比较简单,把整个 body 以字符串类型读取并进行反序列化

List<? extends Class<? extends Annotation>> parameterAnnotation = Arrays
    .stream(parameterAnnotations[i])
    .map(Annotation::annotationType)
    .collect(Collectors.toList());

if (parameterAnnotation.contains(RequestBody.class)) {String bodyAsString = ctx.getBodyAsString();
    argValues[i] = Json.decodeValue(bodyAsString, paramType);
}

Spring 注解挺多的,这次就先实现这个几个吧,剩下的后面再写。

正文完
 0