最近了解到了vertx這個異步框架,但平時用的比較多的仍是spring,出於好奇,嘗試基於vertx web去實現spring mvc風格註解。java
最終效果以下所示git
@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: veladdergithub
如今主流spring mvc的控制器通常是使用@RestController
註解,它的主要做用是告訴框架這個類是請求控制器,讓框架主動掃描並加載這個類。web
這個註解本質是個標記註解,因此目前不須要其餘字段。spring
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface RestController { }
因爲目前沒有實現任何DI(依賴注入),爲了方便咱們直接在代碼裏面手動建立實例並加載。json
@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
註解的做用是把請求路徑和咱們的處理邏輯關聯在一塊兒。mvc
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface RequestMapping { String value() default ""; HttpMethod[] method() default {}; }
value對應的path,而method對應http的請求方法,如GET、POST等。使用效果以下所示:app
@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
。
返回值通常有多種狀況:
處理返回值代碼以下所示:
// 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
)支持簡單類型簡單類型處理只須要把基礎類型都轉換爲包裝類型,再反射調用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
的功能已經實現了。
默認狀況下用戶進行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註解挺多的,此次就先實現這個幾個吧,剩下的後面再寫。