前言
最近瞭解到了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;
}
}
@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
。
返回值處理
返回值一般有多種情況:
- void類型,這種是無返回值,一般是特殊情況會使用。比如下載文件,無法以JSON形式返回,需要調用RoutingContext進行處理。
- 基礎類型和字符串,直接轉換爲JSON中的數字和字符串就好。
- 其他類型,當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);
接着我們來注入參數值,需要處理的參數類型可以分爲下列幾種:
- 簡單類型,基礎類型和它們的包裝類,再加上字符串
- 集合類型,
java.util.Collection<T>
及其子類,泛型參數T支持簡單類型 - 數組類型,數組的組件類型(
ComponentType
)支持簡單類型 - 數據類型,類似POJO的數據載體類,裏面可以再嵌套數據類型或其他類型
- 特殊類型,比如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註解挺多的,這次就先實現這個幾個吧,剩下的後面再寫。