├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── top │ │ └── nintha │ │ └── veladder │ │ ├── AppLauncher.java │ │ ├── annotations │ │ ├── BlockingService.java │ │ ├── RequestBody.java │ │ ├── RequestMapping.java │ │ └── RestController.java │ │ ├── controller │ │ ├── HelloController.java │ │ └── HelloRxController.java │ │ ├── dao │ │ └── MockUserDao.java │ │ ├── entity │ │ └── MockUser.java │ │ └── utils │ │ ├── ClassScanUtil.java │ │ └── Singles.java └── resources │ ├── assert │ └── pic.jpg │ ├── log4j.properties │ └── logback.xml └── test └── java └── top └── nintha └── veladder ├── controller └── HelloControllerTest.java └── utils └── ClassScanUtilTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | **/.gradle 2 | **/build/ 3 | **/out/ 4 | *.zip 5 | *.csv 6 | !gradle/wrapper/gradle-wrapper.jar 7 | **/file-uploads/ 8 | 9 | ### STS ### 10 | .apt_generated 11 | .classpath 12 | .factorypath 13 | .project 14 | .settings 15 | .springBeans 16 | .sts4-cache 17 | 18 | ### IntelliJ IDEA ### 19 | **/.idea 20 | *.iws 21 | *.iml 22 | *.ipr 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | /.gradle/ 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # veladder 2 | 3 | A ladder for helping springer use vertx web 4 | 5 | spring风格的vertx web框架 6 | 7 | ## Get start 8 | 9 | veladder可以使用类似spring mvc的注解来构建web服务,大部分功能是通过注解扫描进行实现的。 10 | 11 | ``` java 12 | @Slf4j 13 | @RestController 14 | public class HelloController { 15 | 16 | @RequestMapping("hello/world") 17 | public String helloWorld() { 18 | return "Hello world"; 19 | } 20 | 21 | @RequestMapping("echo") 22 | public Map echo(String message, Long token, int code, RoutingContext ctx) { 23 | log.info("uri={}", ctx.request().absoluteURI()); 24 | 25 | log.info("message={}, token={}, code={}", message, token, code); 26 | HashMap map = new HashMap<>(); 27 | map.put("message", message); 28 | map.put("token", token); 29 | map.put("code", code); 30 | return map; 31 | } 32 | 33 | @RequestMapping(value = "hello/array") 34 | public List> helloArray(long[] ids, String[] names, RoutingContext ctx) { 35 | log.info("ids={}", Arrays.toString(ids)); 36 | log.info("names={}", Arrays.toString(names)); 37 | return ctx.request().params().entries(); 38 | } 39 | 40 | @RequestMapping("query/list") 41 | public List> queryArray(List ids, TreeSet names, LinkedList rawList, RoutingContext ctx) { 42 | log.info("ids={}", ids); 43 | log.info("names={}", names); 44 | log.info("rawList={}", rawList); 45 | return ctx.request().params().entries(); 46 | } 47 | 48 | @RequestMapping("query/bean") 49 | public BeanReq queryBean(BeanReq req) { 50 | log.info("req={}", req); 51 | return req; 52 | } 53 | 54 | @RequestMapping(value = "post/body", method = HttpMethod.POST) 55 | public BeanReq postRequestBody(@RequestBody BeanReq req) { 56 | log.info("req={}", req); 57 | return req; 58 | } 59 | } 60 | ``` 61 | 62 | ## Spring-like Annotations 63 | - [x] RestController 64 | - [x] RequestMapping (目前不支持类上使用) 65 | - [x] RequestBody 66 | - [ ] GetMapping 67 | - [ ] PostMapping 68 | - [ ] PutMapping 69 | - [ ] DeleteMapping 70 | - [ ] PatchMapping 71 | 72 | ## RxJava2 73 | - [x] upload file 74 | - [x] download file 75 | 76 | ## Database Access 77 | - [ ] mysql(jpa/mybatis) 78 | - [ ] mongodb 79 | 80 | ## Other 81 | - [x] package scan 82 | 83 | 84 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'top.nintha' 6 | version '0.0.1' 7 | 8 | sourceCompatibility = 11 9 | targetCompatibility = 11 10 | compileJava.options.encoding = 'UTF-8' 11 | 12 | repositories { 13 | maven {url 'http://maven.aliyun.com/nexus/content/groups/public/'} 14 | mavenCentral() 15 | } 16 | 17 | ext { 18 | vertxVersion = '4.0.2' 19 | lombokVersion = '1.18.18' 20 | } 21 | 22 | dependencies { 23 | testImplementation(platform('org.junit:junit-bom:5.7.1')) 24 | testImplementation('org.junit.jupiter:junit-jupiter') 25 | 26 | implementation "io.vertx:vertx-core:$vertxVersion" 27 | implementation "io.vertx:vertx-web:$vertxVersion" 28 | implementation "io.vertx:vertx-web-client:$vertxVersion" 29 | implementation "io.vertx:vertx-rx-java2:$vertxVersion" 30 | implementation "io.vertx:vertx-junit5:$vertxVersion" 31 | 32 | implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.8.1' 33 | implementation "org.slf4j:slf4j-api:1.8.0-beta4" 34 | implementation "ch.qos.logback:logback-core:1.3.0-alpha5" 35 | implementation "ch.qos.logback:logback-classic:1.3.0-alpha5" 36 | 37 | implementation group: 'org.javassist', name: 'javassist', version: '3.25.0-GA' 38 | implementation group: 'com.google.guava', name: 'guava', version: '30.1-jre' 39 | 40 | implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.12.2' 41 | 42 | compileOnly "org.projectlombok:lombok:$lombokVersion" 43 | annotationProcessor "org.projectlombok:lombok:$lombokVersion" 44 | 45 | testCompileOnly "org.projectlombok:lombok:$lombokVersion" 46 | testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" 47 | } 48 | 49 | jar { 50 | from { 51 | configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } 52 | } 53 | exclude "META-INF/*.SF" 54 | exclude "META-INF/*.DSA" 55 | exclude "META-INF/*.RSA" 56 | manifest { 57 | attributes 'Main-Class': 'top.nintha.veladder.AppLauncher' 58 | } 59 | } 60 | 61 | 62 | test { 63 | useJUnitPlatform() 64 | testLogging { 65 | events "passed", "skipped", "failed" 66 | } 67 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nintha/veladder/3cc59e8c5de84b9f7707a0f1bb22490196a3aa07/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'veladder' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/top/nintha/veladder/AppLauncher.java: -------------------------------------------------------------------------------- 1 | package top.nintha.veladder; 2 | 3 | import com.google.common.primitives.Primitives; 4 | import io.reactivex.Flowable; 5 | import io.reactivex.Single; 6 | import io.reactivex.functions.Consumer; 7 | import io.vertx.core.AbstractVerticle; 8 | import io.vertx.core.Handler; 9 | import io.vertx.core.MultiMap; 10 | import io.vertx.core.Vertx; 11 | import io.vertx.core.http.HttpHeaders; 12 | import io.vertx.core.http.HttpMethod; 13 | import io.vertx.core.http.HttpServer; 14 | import io.vertx.core.http.HttpServerResponse; 15 | import io.vertx.core.json.Json; 16 | import io.vertx.ext.web.FileUpload; 17 | import io.vertx.ext.web.Router; 18 | import io.vertx.ext.web.RoutingContext; 19 | import io.vertx.ext.web.handler.BodyHandler; 20 | import javassist.Modifier; 21 | import javassist.*; 22 | import javassist.bytecode.CodeAttribute; 23 | import javassist.bytecode.LocalVariableAttribute; 24 | import javassist.bytecode.MethodInfo; 25 | import lombok.extern.slf4j.Slf4j; 26 | import org.apache.commons.lang3.StringUtils; 27 | import top.nintha.veladder.annotations.RequestBody; 28 | import top.nintha.veladder.annotations.RequestMapping; 29 | import top.nintha.veladder.annotations.RestController; 30 | import top.nintha.veladder.utils.ClassScanUtil; 31 | 32 | import java.lang.annotation.Annotation; 33 | import java.lang.invoke.MethodHandle; 34 | import java.lang.invoke.MethodHandles; 35 | import java.lang.reflect.*; 36 | import java.util.*; 37 | import java.util.stream.Collectors; 38 | 39 | @Slf4j 40 | public class AppLauncher extends AbstractVerticle { 41 | private final static String SCAN_PACKAGE = "top.nintha.veladder.controller"; 42 | private final int port; 43 | 44 | public AppLauncher(int port) { 45 | this.port = port; 46 | } 47 | 48 | @Override 49 | public void start() throws Exception { 50 | HttpServer server = vertx.createHttpServer(); 51 | 52 | Router router = Router.router(vertx); 53 | router.errorHandler(500, rc -> { 54 | Throwable failure = rc.failure(); 55 | if (failure != null) { 56 | log.error("[Router Error Handler]", failure); 57 | } 58 | }); 59 | 60 | Set> classes = ClassScanUtil.scanByAnnotation(SCAN_PACKAGE, RestController.class); 61 | for (Class cls : classes) { 62 | Object controller = cls.getConstructor().newInstance(); 63 | routerMapping(controller, router); 64 | } 65 | 66 | server.requestHandler(router).listen(port, ar -> { 67 | if (ar.succeeded()) { 68 | log.info("HTTP Server is listening on {}", port); 69 | } else { 70 | log.error("Failed to run HTTP Server", ar.cause()); 71 | } 72 | }); 73 | } 74 | 75 | 76 | /** 77 | * buildRouteFromAnnotatedClass 78 | * 79 | * @param annotatedBean 80 | * @param router 81 | * @param 82 | * @throws NotFoundException 83 | */ 84 | private void routerMapping(ControllerType annotatedBean, Router router) throws NotFoundException { 85 | Class clazz = (Class) annotatedBean.getClass(); 86 | if (!clazz.isAnnotationPresent(RestController.class)) { 87 | return; 88 | } 89 | 90 | ClassPool classPool = ClassPool.getDefault(); 91 | classPool.insertClassPath(new ClassClassPath(clazz)); 92 | CtClass cc = classPool.get(clazz.getName()); 93 | Method[] methods = clazz.getDeclaredMethods(); 94 | for (Method method : methods) { 95 | if (!method.isAnnotationPresent(RequestMapping.class)) { 96 | continue; 97 | } 98 | 99 | RequestMapping methodAnno = method.getAnnotation(RequestMapping.class); 100 | String requestPath = methodAnno.value(); 101 | 102 | CtMethod ctMethod = cc.getDeclaredMethod(method.getName()); 103 | MethodInfo methodInfo = ctMethod.getMethodInfo(); 104 | CodeAttribute codeAttribute = methodInfo.getCodeAttribute(); 105 | LocalVariableAttribute attribute = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag); 106 | 107 | Class[] paramTypes = method.getParameterTypes(); 108 | String[] paramNames = new String[ctMethod.getParameterTypes().length]; 109 | if (attribute != null) { 110 | // 通过javassist获取方法形参,成员方法 0位变量是this 111 | int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1; 112 | for (int i = 0; i < paramNames.length; i++) { 113 | paramNames[i] = attribute.variableName(i + pos); 114 | } 115 | } 116 | String formatPath = requestPath.startsWith("/") ? requestPath : "/" + requestPath; 117 | log.info("[Router Mapping] {}({}) > {}, {}", method.getName(), formatPath, Arrays.toString(paramNames), Arrays.toString(paramTypes)); 118 | 119 | 120 | Handler requestHandler = ctx -> { 121 | try { 122 | Object[] argValues = new Object[ctMethod.getParameterTypes().length]; 123 | MultiMap params = ctx.request().params(); 124 | 125 | Annotation[][] parameterAnnotations = method.getParameterAnnotations(); 126 | Set uploads = ctx.fileUploads(); 127 | Map uploadMap = uploads.stream().collect(Collectors.toMap(FileUpload::name, x -> x)); 128 | for (int i = 0; i < argValues.length; i++) { 129 | Class paramType = paramTypes[i]; 130 | // RequestBody数据解析 131 | List> parameterAnnotation = Arrays.stream(parameterAnnotations[i]).map(Annotation::annotationType).collect(Collectors.toList()); 132 | if (parameterAnnotation.contains(RequestBody.class)) { 133 | String bodyAsString = ctx.getBodyAsString(); 134 | argValues[i] = Json.decodeValue(bodyAsString, paramType); 135 | } 136 | // special type 137 | else if (paramType == RoutingContext.class) { 138 | argValues[i] = ctx; 139 | } else if (paramType == FileUpload.class) { 140 | argValues[i] = uploadMap.get(paramNames[i]); 141 | } 142 | // Normal Type 143 | else if (paramType.isArray() || Collection.class.isAssignableFrom(paramType) || isStringOrPrimitiveType(paramType)) { 144 | Type[] genericParameterTypes = method.getGenericParameterTypes(); 145 | argValues[i] = parseSimpleTypeOrArrayOrCollection(params, paramType, paramNames[i], genericParameterTypes[i]); 146 | } 147 | // POJO Bean 148 | else { 149 | argValues[i] = parseBeanType(params, paramType); 150 | } 151 | } 152 | HttpServerResponse response = ctx.response(); 153 | Object result = MethodHandles.lookup().unreflect(method).bindTo(annotatedBean).invokeWithArguments(argValues); 154 | if (!response.headWritten()) { 155 | response.putHeader(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8"); 156 | } 157 | 158 | // Write to the response and end it 159 | Consumer responseEnd = x -> { 160 | if (method.getReturnType() == void.class) { 161 | response.end(); 162 | } else { 163 | response.end(x instanceof CharSequence ? x.toString() : Json.encode(x)); 164 | } 165 | }; 166 | Consumer onError = err -> { 167 | log.error("request error, {}::{}", clazz.getName(), method.getName(), err); 168 | HashMap map = new HashMap<>(); 169 | map.put("message", "system error"); 170 | ctx.response().end(Json.encode(map)); 171 | }; 172 | if (result instanceof Single) { 173 | ((Single) result).subscribe(responseEnd, onError); 174 | } else if (result instanceof Flowable) { 175 | throw new UnsupportedOperationException("not support Flowable, maybe use Single instead"); 176 | } else { 177 | responseEnd.accept(result); 178 | } 179 | 180 | } catch (Throwable e) { 181 | log.error("request error, {}::{} ", clazz.getName(), method.getName(), e); 182 | HashMap result = new HashMap<>(); 183 | result.put("message", "system error"); 184 | ctx.response().end(Json.encode(result)); 185 | } 186 | }; 187 | 188 | // bind handler to router 189 | if (methodAnno.method().length == 0) { 190 | // 默认绑定全部HttpMethod 191 | router.route(formatPath).handler(BodyHandler.create()).handler(requestHandler); 192 | } else { 193 | for (String m : methodAnno.method()) { 194 | router.route(HttpMethod.valueOf(m), formatPath).handler(BodyHandler.create()).handler(requestHandler); 195 | } 196 | } 197 | } 198 | } 199 | 200 | /** 201 | * 解析简单类型以及对应的集合或数组类型 202 | * 203 | * @param allParams 所有请求参数 204 | * @param paramType 参数类型 205 | * @param paramName 参数名称 206 | * @param genericParameterTypes 泛型化参数类型 207 | * @return 208 | * @throws Throwable 209 | */ 210 | private Object parseSimpleTypeOrArrayOrCollection(MultiMap allParams, Class paramType, String paramName, Type genericParameterTypes) throws Throwable { 211 | // Array type 212 | if (paramType.isArray()) { 213 | // 数组元素类型 214 | Class componentType = paramType.getComponentType(); 215 | 216 | List values = allParams.getAll(paramName); 217 | Object array = Array.newInstance(componentType, values.size()); 218 | for (int j = 0; j < values.size(); j++) { 219 | Array.set(array, j, parseSimpleType(values.get(j), componentType)); 220 | } 221 | return array; 222 | } 223 | // Collection type 224 | else if (Collection.class.isAssignableFrom(paramType)) { 225 | return parseCollectionType(allParams.getAll(paramName), genericParameterTypes); 226 | } 227 | // String and primitive type 228 | else if (isStringOrPrimitiveType(paramType)) { 229 | return parseSimpleType(allParams.get(paramName), paramType); 230 | } 231 | 232 | return null; 233 | } 234 | 235 | /** 236 | * 判断是否为字符串或基础类型以及对应的包装类型 237 | */ 238 | private boolean isStringOrPrimitiveType(Class targetClass) { 239 | return targetClass == String.class || Primitives.allWrapperTypes().contains(Primitives.wrap(targetClass)); 240 | } 241 | 242 | /** 243 | * 处理字符串,基础类型以及对应的包装类型 244 | */ 245 | @SuppressWarnings("unchecked") 246 | private T parseSimpleType(String value, Class targetClass) throws Throwable { 247 | if (StringUtils.isBlank(value)) { 248 | return null; 249 | } 250 | 251 | Class wrapType = Primitives.wrap(targetClass); 252 | if (Primitives.allWrapperTypes().contains(wrapType)) { 253 | MethodHandle valueOf = MethodHandles.lookup().unreflect(wrapType.getMethod("valueOf", String.class)); 254 | return (T) valueOf.invoke(value); 255 | } else if (targetClass == String.class) { 256 | return (T) value; 257 | } 258 | 259 | return null; 260 | } 261 | 262 | /** 263 | * 解析集合类型 264 | * 265 | * @param values 请求参数值 266 | * @param genericParameterType from Method::getGenericParameterTypes 267 | */ 268 | private Collection parseCollectionType(List values, Type genericParameterType) throws Throwable { 269 | Class actualTypeArgument = String.class; // 无泛型参数默认用String类型 270 | Class rawType; 271 | // 参数带泛型 272 | if (genericParameterType instanceof ParameterizedType) { 273 | ParameterizedType parameterType = (ParameterizedType) genericParameterType; 274 | actualTypeArgument = (Class) parameterType.getActualTypeArguments()[0]; 275 | rawType = (Class) parameterType.getRawType(); 276 | } else { 277 | rawType = (Class) genericParameterType; 278 | } 279 | 280 | Collection coll; 281 | if (rawType == List.class) { 282 | coll = new ArrayList<>(); 283 | } else if (rawType == Set.class) { 284 | coll = new HashSet<>(); 285 | } else { 286 | coll = (Collection) rawType.newInstance(); 287 | } 288 | 289 | for (String value : values) { 290 | coll.add(parseSimpleType(value, actualTypeArgument)); 291 | } 292 | return coll; 293 | } 294 | 295 | /** 296 | * 解析实体对象 297 | * 298 | * @param allParams 所有参数 299 | * @param paramType 实体参数类型 300 | * @return 已经注入字段的实体对象 301 | */ 302 | private Object parseBeanType(MultiMap allParams, Class paramType) throws Throwable { 303 | Object bean = paramType.newInstance(); 304 | Field[] fields = paramType.getDeclaredFields(); 305 | for (Field field : fields) { 306 | Object value = parseSimpleTypeOrArrayOrCollection(allParams, field.getType(), field.getName(), field.getGenericType()); 307 | 308 | field.setAccessible(true); 309 | field.set(bean, value); 310 | } 311 | return bean; 312 | } 313 | 314 | public static void main(String[] args) { 315 | Vertx vertx = Vertx.vertx(); 316 | vertx.deployVerticle(new AppLauncher(8080)); 317 | log.info("Deploy Verticle...."); 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/main/java/top/nintha/veladder/annotations/BlockingService.java: -------------------------------------------------------------------------------- 1 | package top.nintha.veladder.annotations; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Documented 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Target({ElementType.TYPE}) 8 | public @interface BlockingService { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/top/nintha/veladder/annotations/RequestBody.java: -------------------------------------------------------------------------------- 1 | package top.nintha.veladder.annotations; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Documented 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Target({ElementType.PARAMETER}) 8 | public @interface RequestBody { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/top/nintha/veladder/annotations/RequestMapping.java: -------------------------------------------------------------------------------- 1 | package top.nintha.veladder.annotations; 2 | 3 | import io.vertx.core.http.HttpMethod; 4 | 5 | import java.lang.annotation.*; 6 | 7 | @Documented 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target({ElementType.METHOD, ElementType.TYPE}) 10 | public @interface RequestMapping { 11 | String value() default ""; 12 | 13 | String[] method() default {}; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/top/nintha/veladder/annotations/RestController.java: -------------------------------------------------------------------------------- 1 | package top.nintha.veladder.annotations; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Documented 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Target({ElementType.METHOD, ElementType.TYPE}) 8 | public @interface RestController { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/top/nintha/veladder/controller/HelloController.java: -------------------------------------------------------------------------------- 1 | package top.nintha.veladder.controller; 2 | 3 | import io.vertx.ext.web.RoutingContext; 4 | import lombok.extern.slf4j.Slf4j; 5 | import top.nintha.veladder.annotations.RequestBody; 6 | import top.nintha.veladder.annotations.RequestMapping; 7 | import top.nintha.veladder.annotations.RestController; 8 | import top.nintha.veladder.entity.MockUser; 9 | 10 | import java.util.*; 11 | 12 | @Slf4j 13 | @RestController 14 | public class HelloController { 15 | 16 | @RequestMapping("hello/world") 17 | public String helloWorld() { 18 | return "Hello world"; 19 | } 20 | 21 | @RequestMapping("hello/void") 22 | public void helloVoid() { 23 | log.info("call void"); 24 | } 25 | 26 | @RequestMapping("echo/text") 27 | public String echoText(String text) { 28 | return text; 29 | } 30 | 31 | @RequestMapping("echo/object") 32 | public Map echoObject(String text, Long number, int code, RoutingContext ctx) { 33 | log.info("uri={}", ctx.request().absoluteURI()); 34 | 35 | log.info("text={}, number={}, code={}", text, number, code); 36 | HashMap map = new HashMap<>(); 37 | map.put("text", text); 38 | map.put("number", number); 39 | map.put("code", code); 40 | return map; 41 | } 42 | 43 | @RequestMapping(value = "hello/array") 44 | public HashMap helloArray(long[] ids, String[] names, RoutingContext ctx) { 45 | log.info("ids={}", Arrays.toString(ids)); 46 | log.info("names={}", Arrays.toString(names)); 47 | 48 | HashMap map = new HashMap<>(); 49 | map.put("ids", ids); 50 | map.put("names", names); 51 | return map; 52 | } 53 | 54 | @RequestMapping("hello/list") 55 | public HashMap helloList(List ids, TreeSet names, LinkedList rawList, RoutingContext ctx) { 56 | log.info("ids={}", ids); 57 | log.info("names={}", names); 58 | log.info("rawList={}", rawList); 59 | 60 | HashMap map = new HashMap<>(); 61 | map.put("ids", ids); 62 | map.put("names", names); 63 | map.put("rawList", rawList); 64 | return map; 65 | } 66 | 67 | @RequestMapping("query/bean") 68 | public MockUser queryBean(MockUser req) { 69 | log.info("req={}", req); 70 | return req; 71 | } 72 | 73 | @RequestMapping(value = "post/body", method = "POST") 74 | public MockUser postRequestBody(@RequestBody MockUser req) { 75 | log.info("req={}", req); 76 | return req; 77 | } 78 | 79 | @RequestMapping("hello/path/variable/:token/:id") 80 | public String helloPathVariable(String token, Long id, RoutingContext ctx) { 81 | log.info("token={}", token); 82 | log.info("id={}", id); 83 | return token + id; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/top/nintha/veladder/controller/HelloRxController.java: -------------------------------------------------------------------------------- 1 | package top.nintha.veladder.controller; 2 | 3 | import io.reactivex.Single; 4 | import io.vertx.core.file.FileSystem; 5 | import io.vertx.core.http.HttpHeaders; 6 | import io.vertx.core.http.HttpMethod; 7 | import io.vertx.ext.web.FileUpload; 8 | import io.vertx.ext.web.RoutingContext; 9 | import lombok.extern.slf4j.Slf4j; 10 | import top.nintha.veladder.annotations.RequestMapping; 11 | import top.nintha.veladder.annotations.RestController; 12 | import top.nintha.veladder.dao.MockUserDao; 13 | import top.nintha.veladder.entity.MockUser; 14 | import top.nintha.veladder.utils.Singles; 15 | 16 | import java.util.concurrent.CompletableFuture; 17 | 18 | @Slf4j 19 | @RestController 20 | public class HelloRxController { 21 | private static final MockUserDao MOCK_USER_DAO = new MockUserDao(); 22 | 23 | @RequestMapping("rx/hello/world") 24 | public Single helloWorld() { 25 | return Single.just("Hello world"); 26 | } 27 | 28 | @RequestMapping(value = "rx/users/default", method = "GET") 29 | public Single findDefaultUser() { 30 | return Singles.supplyAsync(MOCK_USER_DAO::findDefaultUser); 31 | } 32 | 33 | @RequestMapping(value = "rx/users/exception", method = "GET") 34 | public Single exceptionAction() { 35 | return Singles.supplyAsync(MOCK_USER_DAO::blockingActionWithException); 36 | } 37 | 38 | @RequestMapping(value = "rx/file/upload", method = "GET") 39 | public Single uploadFile(FileUpload file, RoutingContext ctx){ 40 | if(file == null){ 41 | log.error("upload failed"); 42 | return Single.just(""); 43 | } 44 | 45 | FileSystem fileSystem = ctx.vertx().fileSystem(); 46 | fileSystem.readFile(file.uploadedFileName(), ar -> { 47 | if(ar.succeeded()){ 48 | fileSystem.writeFile(file.fileName(), ar.result(), writeAr -> { 49 | if(writeAr.succeeded()){ 50 | log.info("upload ok."); 51 | }else { 52 | log.error("error", ar.cause()); 53 | } 54 | }); 55 | }else{ 56 | log.error("error", ar.cause()); 57 | } 58 | }); 59 | log.info("{}", file.uploadedFileName()); 60 | return Single.just(file.fileName()); 61 | } 62 | 63 | @RequestMapping(value = "rx/file/download", method = "GET") 64 | public void downloadFile(RoutingContext ctx){ 65 | ctx.response() 66 | .putHeader(HttpHeaders.CONTENT_TYPE, "application/octet-stream") 67 | .putHeader("Content-Disposition", "attachment; filename=\"pic.jpg\"") 68 | .putHeader(HttpHeaders.TRANSFER_ENCODING, "chunked") 69 | .sendFile("src/main/resources/assert/pic.jpg"); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/top/nintha/veladder/dao/MockUserDao.java: -------------------------------------------------------------------------------- 1 | package top.nintha.veladder.dao; 2 | 3 | import top.nintha.veladder.annotations.BlockingService; 4 | import top.nintha.veladder.entity.MockUser; 5 | 6 | @BlockingService 7 | public class MockUserDao { 8 | 9 | public MockUser findDefaultUser(){ 10 | try { 11 | Thread.sleep(5000); 12 | } catch (InterruptedException e) { 13 | e.printStackTrace(); 14 | } 15 | return MockUser.defaultUser(); 16 | } 17 | 18 | public String blockingActionWithException(){ 19 | try { 20 | Thread.sleep(5000); 21 | } catch (InterruptedException e) { 22 | e.printStackTrace(); 23 | } 24 | throw new RuntimeException("test throw error"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/top/nintha/veladder/entity/MockUser.java: -------------------------------------------------------------------------------- 1 | package top.nintha.veladder.entity; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | @Data 9 | public class MockUser { 10 | private Long id; 11 | private String name; 12 | private List tags; 13 | 14 | public static MockUser defaultUser(){ 15 | MockUser user = new MockUser(); 16 | user.id = 1L; 17 | user.name = "default"; 18 | user.tags = Collections.singletonList("TAG"); 19 | return user; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/top/nintha/veladder/utils/ClassScanUtil.java: -------------------------------------------------------------------------------- 1 | package top.nintha.veladder.utils; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.lang.annotation.Annotation; 8 | import java.net.JarURLConnection; 9 | import java.net.URL; 10 | import java.net.URLDecoder; 11 | import java.nio.charset.StandardCharsets; 12 | import java.util.*; 13 | import java.util.jar.JarEntry; 14 | import java.util.jar.JarFile; 15 | 16 | @Slf4j 17 | public class ClassScanUtil { 18 | private static final String CLASS_FILE_SUFFIX = ".class"; 19 | 20 | /** 21 | * 扫描指定包路径下所有包含指定注解的类 22 | * 23 | * @param packageName 包名 24 | * @param annotation 指定的注解 25 | * @return Set 26 | */ 27 | public static Set> scanByAnnotation(String packageName, Class annotation) { 28 | final Set> classSet = new HashSet<>(); 29 | String packageDirName = packageName.replace('.', '/'); 30 | final Enumeration dirs; 31 | try { 32 | dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName); 33 | } catch (IOException e) { 34 | log.warn("[ClassScan] failed to get package resources, packageDirName={}", packageDirName, e); 35 | return classSet; 36 | } 37 | while (dirs.hasMoreElements()) { 38 | // 获取下一个元素 39 | URL url = dirs.nextElement(); 40 | // 得到协议的名称 41 | String protocol = url.getProtocol(); 42 | Set classNames = Collections.emptySet(); 43 | // 如果是以文件的形式保存在服务器上 44 | if ("file".equals(protocol)) { 45 | classNames = scanFile(url, packageName); 46 | } 47 | // 如果是jar包文件 48 | else if ("jar".equals(protocol)) { 49 | classNames = scanJar(url, packageName, packageDirName); 50 | } 51 | for (String cls : classNames) { 52 | Class clazz = null; 53 | try { 54 | clazz = Class.forName(cls); 55 | } catch (ClassNotFoundException e) { 56 | log.warn("[ClassScan] failed to load class '{}'", cls, e); 57 | } 58 | if (clazz != null && null != clazz.getAnnotation(annotation)) { 59 | classSet.add(clazz); 60 | } 61 | } 62 | 63 | } 64 | return classSet; 65 | } 66 | 67 | private static Set scanFile(URL url, String packageName) { 68 | // 获取包的物理路径 69 | String filePath = URLDecoder.decode(url.getFile(), StandardCharsets.UTF_8); 70 | // 以文件的方式扫描整个包下的文件 并添加到集合中 71 | File dir = new File(filePath); 72 | Set packages = new HashSet<>(); 73 | fetchFilePackages(dir, packageName, packages); 74 | return packages; 75 | } 76 | 77 | private static Set scanJar(URL url, String packageName, String packageDirName) { 78 | JarFile jar; 79 | try { 80 | JarURLConnection urlConnection = (JarURLConnection) url.openConnection(); 81 | jar = urlConnection.getJarFile(); 82 | } catch (IOException e) { 83 | log.warn("[ClassScan] failed to resolve jar entry", e); 84 | return Set.of(); 85 | } 86 | 87 | Set classNames = new HashSet<>(); 88 | Enumeration entries = jar.entries(); 89 | while (entries.hasMoreElements()) { 90 | // 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件 91 | JarEntry entry = entries.nextElement(); 92 | String name = entry.getName(); 93 | // 如果是以/开头的 94 | if (name.charAt(0) == '/') { 95 | // 获取后面的字符串 96 | name = name.substring(1); 97 | } 98 | // 如果前半部分和定义的包名相同 99 | if (name.startsWith(packageDirName)) { 100 | int idx = name.lastIndexOf('/'); 101 | // 如果以"/"结尾 是一个包 102 | if (idx != -1) { 103 | // 获取包名 把"/"替换成"." 104 | packageName = name.substring(0, idx).replace('/', '.'); 105 | } 106 | // 如果可以迭代下去 并且是一个包 107 | // 如果是一个.class文件 而且不是目录 108 | if (name.endsWith(".class") && !entry.isDirectory()) { 109 | // 去掉后面的".class" 获取真正的类名 110 | String className = name.substring(packageName.length() + 1, name.length() - 6); 111 | classNames.add(packageName + '.' + className); 112 | } 113 | } 114 | } 115 | return classNames; 116 | } 117 | 118 | /** 119 | * 查找所有的文件 120 | */ 121 | private static void fetchFilePackages(File dir, String packageName, Set packages) { 122 | if (dir.isDirectory()) { 123 | for (File f : Objects.requireNonNull(dir.listFiles())) { 124 | fetchFilePackages(f, String.format("%s.%s", packageName, f.getName()), packages); 125 | } 126 | } else if (packageName.endsWith(CLASS_FILE_SUFFIX)) { 127 | packages.add(packageName.substring(0, packageName.length() - CLASS_FILE_SUFFIX.length())); 128 | } 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/top/nintha/veladder/utils/Singles.java: -------------------------------------------------------------------------------- 1 | package top.nintha.veladder.utils; 2 | 3 | import io.reactivex.Single; 4 | import io.vertx.core.Future; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | import java.util.function.Supplier; 8 | 9 | public class Singles { 10 | /** 11 | * async converter, call by CompletableFuture::whenComplete 12 | */ 13 | public static Single fromCompletableFuture(CompletableFuture future) { 14 | return Single.create(emitter -> future.whenComplete((value, error) -> { 15 | if (error == null) { 16 | emitter.onSuccess(value); 17 | } else { 18 | emitter.onError(error); 19 | } 20 | })); 21 | } 22 | 23 | public static Single fromVertxFuture(Future future){ 24 | return Single.create(emitter -> future.onComplete(ar -> { 25 | if(ar.succeeded()){ 26 | emitter.onSuccess(ar.result()); 27 | }else { 28 | emitter.onError(ar.cause()); 29 | } 30 | })); 31 | } 32 | 33 | public static Single supplyAsync(Supplier supplier){ 34 | return fromCompletableFuture(CompletableFuture.supplyAsync(supplier)); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/resources/assert/pic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nintha/veladder/3cc59e8c5de84b9f7707a0f1bb22490196a3aa07/src/main/resources/assert/pic.jpg -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # rootLogger参数分别为:根Logger级别,输出器stdout,输出器log 2 | log4j.rootLogger = info,stdout 3 | 4 | # 输出信息到控制台 5 | log4j.appender.stdout = org.apache.log4j.ConsoleAppender 6 | log4j.appender.stdout.layout = org.apache.log4j.PatternLayout 7 | log4j.appender.stdout.layout.ConversionPattern = %d [%p][%-10.10t] %l: %m%n 8 | 9 | 10 | # ban 11 | log4j.logger.io.vertx.core.net.impl.ConnectionBase=OFF -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%-15.15thread] %36logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/test/java/top/nintha/veladder/controller/HelloControllerTest.java: -------------------------------------------------------------------------------- 1 | package top.nintha.veladder.controller; 2 | 3 | import io.vertx.core.MultiMap; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.core.buffer.Buffer; 6 | import io.vertx.core.json.JsonArray; 7 | import io.vertx.core.json.JsonObject; 8 | import io.vertx.ext.web.client.WebClient; 9 | import io.vertx.junit5.VertxExtension; 10 | import io.vertx.junit5.VertxTestContext; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.junit.jupiter.api.Assertions; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.DisplayName; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.api.extension.ExtendWith; 17 | import top.nintha.veladder.AppLauncher; 18 | import top.nintha.veladder.entity.MockUser; 19 | 20 | import java.util.List; 21 | import java.util.UUID; 22 | import java.util.concurrent.ThreadLocalRandom; 23 | 24 | @Slf4j 25 | @ExtendWith(VertxExtension.class) 26 | class HelloControllerTest { 27 | private final int port = ThreadLocalRandom.current().nextInt(12000, 22000); 28 | 29 | @BeforeEach 30 | @DisplayName("Deploy a verticle") 31 | void prepare(Vertx vertx, VertxTestContext testContext) { 32 | vertx.exceptionHandler(t -> log.error("[VERTX]", t)); 33 | vertx.deployVerticle(new AppLauncher(port), testContext.succeedingThenComplete()); 34 | } 35 | 36 | @Test 37 | void helloWorld(Vertx vertx, VertxTestContext ctx) { 38 | WebClient.create(vertx) 39 | .get(port, "127.0.0.1", "/hello/world") 40 | .send() 41 | .onComplete(ctx.succeeding(buffer -> ctx.verify(() -> { 42 | Assertions.assertEquals("hello world", buffer.bodyAsString().toLowerCase()); 43 | ctx.completeNow(); 44 | }))); 45 | } 46 | 47 | @Test 48 | void helloVoid(Vertx vertx, VertxTestContext ctx) { 49 | WebClient.create(vertx) 50 | .get(port, "127.0.0.1", "/hello/void") 51 | .send() 52 | .onComplete(ctx.succeeding(buffer -> ctx.verify(() -> { 53 | Assertions.assertNull(buffer.body()); 54 | ctx.completeNow(); 55 | }))); 56 | } 57 | 58 | @Test 59 | void echoText(Vertx vertx, VertxTestContext ctx) { 60 | String text = UUID.randomUUID().toString(); 61 | WebClient.create(vertx) 62 | .get(port, "127.0.0.1", "/echo/text") 63 | .addQueryParam("text", text) 64 | .send() 65 | .onComplete(ctx.succeeding(buffer -> ctx.verify(() -> { 66 | Assertions.assertEquals(text, buffer.bodyAsString()); 67 | ctx.completeNow(); 68 | }))); 69 | } 70 | 71 | @Test 72 | void echoObjectWithQuery(Vertx vertx, VertxTestContext ctx) { 73 | String text = UUID.randomUUID().toString(); 74 | Long number = ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE); 75 | int code = ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE); 76 | WebClient.create(vertx) 77 | .get(port, "127.0.0.1", "/echo/object") 78 | .addQueryParam("text", text) 79 | .addQueryParam("number", number.toString()) 80 | .addQueryParam("code", String.valueOf(code)) 81 | .send() 82 | .onComplete(ctx.succeeding(buffer -> ctx.verify(() -> { 83 | JsonObject entries = buffer.bodyAsJsonObject(); 84 | Assertions.assertEquals(text, entries.getString("text")); 85 | Assertions.assertEquals(number, entries.getLong("number")); 86 | Assertions.assertEquals(code, entries.getInteger("code")); 87 | ctx.completeNow(); 88 | }))); 89 | } 90 | 91 | @Test 92 | void echoObjectWithForm(Vertx vertx, VertxTestContext ctx) { 93 | String text = UUID.randomUUID().toString(); 94 | Long number = ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE); 95 | int code = ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE); 96 | 97 | MultiMap form = MultiMap.caseInsensitiveMultiMap(); 98 | form.add("text", text); 99 | form.add("number", String.valueOf(number)); 100 | form.add("code", String.valueOf(code)); 101 | 102 | WebClient.create(vertx) 103 | .post(port, "127.0.0.1", "/echo/object") 104 | .sendForm(form) 105 | .onComplete(ctx.succeeding(buffer -> ctx.verify(() -> { 106 | JsonObject entries = buffer.bodyAsJsonObject(); 107 | Assertions.assertEquals(text, entries.getString("text")); 108 | Assertions.assertEquals(number, entries.getLong("number")); 109 | Assertions.assertEquals(code, entries.getInteger("code")); 110 | ctx.completeNow(); 111 | }))); 112 | } 113 | 114 | @Test 115 | void helloArray(Vertx vertx, VertxTestContext ctx) { 116 | MultiMap form = MultiMap.caseInsensitiveMultiMap(); 117 | form.add("ids", "0"); 118 | form.add("ids", "1"); 119 | form.add("names", "name0"); 120 | form.add("names", "name1"); 121 | 122 | WebClient.create(vertx) 123 | .post(port, "127.0.0.1", "/hello/array") 124 | .sendForm(form) 125 | .onComplete(ctx.succeeding(buffer -> ctx.verify(() -> { 126 | var entries = buffer.bodyAsJsonObject(); 127 | Assertions.assertEquals(new JsonArray().add(0).add(1), entries.getJsonArray("ids")); 128 | Assertions.assertEquals(new JsonArray().add("name0").add("name1"), entries.getJsonArray("names")); 129 | ctx.completeNow(); 130 | }))); 131 | } 132 | 133 | @Test 134 | void helloList(Vertx vertx, VertxTestContext ctx) { 135 | MultiMap form = MultiMap.caseInsensitiveMultiMap(); 136 | form.add("ids", "0"); 137 | form.add("ids", "1"); 138 | form.add("names", "name0"); 139 | form.add("names", "name1"); 140 | form.add("rawList", "raw"); 141 | 142 | WebClient.create(vertx) 143 | .post(port, "127.0.0.1", "/hello/list") 144 | .sendForm(form) 145 | .onComplete(ctx.succeeding(buffer -> ctx.verify(() -> { 146 | var entries = buffer.bodyAsJsonObject(); 147 | Assertions.assertEquals(new JsonArray().add(0).add(1), entries.getJsonArray("ids")); 148 | Assertions.assertEquals(new JsonArray().add("name0").add("name1"), entries.getJsonArray("names")); 149 | Assertions.assertEquals(new JsonArray().add("raw"), entries.getJsonArray("rawList")); 150 | ctx.completeNow(); 151 | }))); 152 | } 153 | 154 | @Test 155 | void queryBean(Vertx vertx, VertxTestContext ctx) { 156 | long id = ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE); 157 | String name = "name" + UUID.randomUUID().toString(); 158 | String tag0 = "tag0" + UUID.randomUUID().toString(); 159 | String tag1 = "tag1" + UUID.randomUUID().toString(); 160 | WebClient.create(vertx) 161 | .get(port, "127.0.0.1", "/query/bean") 162 | .addQueryParam("id", Long.toString(id)) 163 | .addQueryParam("name", name) 164 | .addQueryParam("tags", tag0) 165 | .addQueryParam("tags", tag1) 166 | .send() 167 | .onComplete(ctx.succeeding(buffer -> ctx.verify(() -> { 168 | MockUser user = buffer.bodyAsJson(MockUser.class); 169 | Assertions.assertEquals(id, user.getId()); 170 | Assertions.assertEquals(name, user.getName()); 171 | Assertions.assertEquals(tag0, user.getTags().get(0)); 172 | Assertions.assertEquals(tag1, user.getTags().get(1)); 173 | ctx.completeNow(); 174 | }))); 175 | } 176 | 177 | @Test 178 | void postRequestBody(Vertx vertx, VertxTestContext ctx) { 179 | long id = ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE); 180 | String name = "name" + UUID.randomUUID().toString(); 181 | String tag0 = "tag0" + UUID.randomUUID().toString(); 182 | String tag1 = "tag1" + UUID.randomUUID().toString(); 183 | 184 | MockUser mu = new MockUser(); 185 | mu.setId(id); 186 | mu.setName(name); 187 | mu.setTags(List.of(tag0, tag1)); 188 | 189 | WebClient.create(vertx) 190 | .post(port, "127.0.0.1", "/post/body") 191 | .sendJson(mu) 192 | .onComplete(ctx.succeeding(buffer -> ctx.verify(() -> { 193 | MockUser user = buffer.bodyAsJson(MockUser.class); 194 | Assertions.assertEquals(id, user.getId()); 195 | Assertions.assertEquals(name, user.getName()); 196 | Assertions.assertEquals(tag0, user.getTags().get(0)); 197 | Assertions.assertEquals(tag1, user.getTags().get(1)); 198 | ctx.completeNow(); 199 | }))); 200 | } 201 | 202 | @Test 203 | void helloPathVariable(Vertx vertx, VertxTestContext ctx) { 204 | final String token = UUID.randomUUID().toString(); 205 | final Long id = 1234L; 206 | String uri = String.format("/hello/path/variable/%s/%s", token, id); 207 | 208 | WebClient.create(vertx) 209 | .post(port, "127.0.0.1", uri) 210 | .send() 211 | .onComplete(ctx.succeeding(buffer -> ctx.verify(() -> { 212 | Assertions.assertEquals(token + id, buffer.bodyAsString()); 213 | ctx.completeNow(); 214 | }))); 215 | 216 | } 217 | 218 | } -------------------------------------------------------------------------------- /src/test/java/top/nintha/veladder/utils/ClassScanUtilTest.java: -------------------------------------------------------------------------------- 1 | package top.nintha.veladder.utils; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | import top.nintha.veladder.annotations.RestController; 6 | import top.nintha.veladder.controller.HelloController; 7 | import top.nintha.veladder.controller.HelloRxController; 8 | 9 | import java.util.Set; 10 | 11 | class ClassScanUtilTest { 12 | 13 | @Test 14 | void scanAnnotation() { 15 | String packageName = "top.nintha.veladder"; 16 | Set> classSet = ClassScanUtil.scanByAnnotation(packageName, RestController.class); 17 | Assertions.assertEquals(2, classSet.size()); 18 | Assertions.assertTrue(classSet.contains(HelloRxController.class)); 19 | Assertions.assertTrue(classSet.contains(HelloController.class)); 20 | } 21 | } --------------------------------------------------------------------------------