本篇文章目的是将 Springboot 和 Vertx 进行简单整合。整合目的仅仅是为了整活,因为两个不同的东西整合在一起提升的性能并没有只使用 Vertx 性能高,因此追求高性能的话这是在我来说不推荐。而且他们不仅没有提高很多性能甚至增加了学习成本
一、整合流程
首先呢目标是将Vertx 最终整合成和使用Springboot web 一样简单的 httpserver。
步骤:
- 获取Springboot 所有的Bean
- 注册路由: 检查Bean 中是否是存在实现了 Router 的方法,并交给 router
- 开启服务器,等待请求
二、扫描 Bean
最终目标呢,是实现和使用Springboot 一样简便,所以就需要注解来对需要的方法进行标注
最终效果预览
kotlin
@RouterController
class HelloRouter(
val test :PlayerUnlockTechService
) {
/**
* 注册路由
* 正则路由以 ^ 开始
*
* 方法参数可以是 routingContext 或者 router 或者 routingContext 内的任何东西。以及其他的任何东西,或者 bean
*
*/
@Rout("/hello")
fun hello(response: HttpServerResponse, request: HttpServerRequest) {
request.bodyHandler {
response.end(test.getPlayerUnlockTechsByBuilding("BD12DC34624208045CCA1ECE32071F20").toString())
}
}
- 创建注解
主要注解有:
- RouterController 标注类中有 Router 需要的路由实现
- Rout 标注方法是个路由实现
- AutoClose 标注方法执行完成后自动关闭连接
kotlin
/**
*
* @author : zimo
* @date : 2025/01/03
*/
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Component
annotation class RouterController
/**
*
* @author : zimo
* @date : 2025/01/03
*/
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class Router(
val path: String,
val method: String = ""
)
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class Rout(
val path: String,
val method: String = ""
)
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class RouterGet(
val path: String,
val method: String = "GET"
)
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class RouterPost(
val path: String,
val method: String = "POST"
)
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class RouterPut(
val path: String,
val method: String = "PUT"
)
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class RouterDelete(
val path: String,
val method: String = "DELETE"
)
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class RouterPatch(
val path: String,
val method: String = "PATCH"
)
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class RouterHead(
val path: String,
val method: String = "HEAD"
)
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class RouterOptions(
val path: String,
val method: String = "OPTIONS"
)
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class AutoClose
- 获取 Beans
在当前类中注入applicationContext
并通过applicationContext.beanDefinitionNames
获取所有的 Bean
kotlin
@Component
class RouterProcessor(val applicationContext: ApplicationContext) {
/**
* 初始化完成后的 bean 列表
*/
private val initializedBeanInstances by lazy {
applicationContext.beanDefinitionNames.filter {
it != this.javaClass.simpleName && it != Connection::class.simpleName
}.mapNotNull {
applicationContext.getBean(it)
}
}
}
- 检测出所有的Router 方法
检测出是否被标注为了一个 Router 方法,并注册到 router
kotlin
for (method in clazz.methods) {
if (method.isAnnotationPresent(io.github.zimoyin.ra3.annotations.Router::class.java)) {
val path = method.getAnnotation(io.github.zimoyin.ra3.annotations.Router::class.java).path
router.let {
if (path.startsWith("^")) it.routeWithRegex(path.replaceFirst("^", ""))
else it.route(path)
// order 可以放到注解里,这样可以动态设置了
}.order(0).handler {
// 执行方法的封装
invoke(method, bean, it)
}
return
}
// ... 其他注解处理
}
三、执行Router方法
这里只有两个重点,一个是自动关闭,一个是执行方法传入的参数实例
- 自动关闭,如果方法上存在
AutoClose
注解就在执行方法结束后尝试关闭连接 - 方法参数,从Bean、Context 中获取。如果没有则通过上下文创建 Bean
kotlin
fun invoke(method: Method, bean: Any, routingContext: RoutingContext) {
val args = arrayListOf<Any>()
val isHasAutoClose = method.isAnnotationPresent(AutoClose::class.java)
// 获取方法需要的参数
method.parameters.forEach {
val bean0 = kotlin.runCatching { applicationContext.getBean(it.name, it.type) }.getOrNull()
?: kotlin.runCatching { applicationContext.getBean(it.type) }.getOrNull()
if (bean0 != null) {
args.add(bean0)
} else {
args.add(createParameter(it, routingContext))
}
}
//执行方法
try {
routingContext.request().paramsCharset = "UTF-8"
val result = method.invoke(bean, *args.toTypedArray())
kotlin.runCatching {
// 自动关闭,如果方法上存在 `AutoClose` 注解就在执行方法结束后尝试关闭连接
// 获取方法的返回值,并以方法的返回值作为自动关闭的参数
if (isHasAutoClose) {
val response = routingContext.response()
response.putHeader("content-type", "application/json")
if (method.returnType == Unit::class.java) {
response.end()
}
if (result == null) {
response.end()
}
if (result is String) {
response.end(result)
} else if (result is Number || result is Comparable<*>) {
response.end(result.toString())
} else {
kotlin.runCatching {
response.end(result.toJsonObject().toString())
}.onFailure {
response.end()
logger.debug("自动关闭连接失败", it)
}
}
}
}
} catch (e: InvocationTargetException) {
kotlin.runCatching { routingContext.response().end("Server Error!!!!") }
logger.error("路由执行失败, $method 方法内部存在错误逻辑导致方法执行失败", e)
} catch (e: Exception) {
kotlin.runCatching { routingContext.response().end("Server Error!!!!") }
logger.error("路由执行失败", e)
}
}
获取 routingContext 中的参数,或者创建一个参数
kotlin
private fun createParameter(value: Parameter, routingContext: RoutingContext): Any {
val name = value.name
val type = value.type
when (name) {
"res", "response", "resp" -> return routingContext.response()
"req", "request", "requ" -> return routingContext.request()
"body", "reqBody", "requestBody" -> return routingContext.body()
"headers", "header", "reqHeader", "requestHeader", "reqHeaders", "requestHeaders" -> return routingContext
.request()
.headers()
"query", "reqQuery", "requestQuery", "reqQueries", "requestQueries" -> return routingContext.queryParams()
"data", "reqData", "requestData" -> return routingContext.data()
"params", "reqParams", "requestParams" -> return routingContext.pathParams()
"cookie", "reqCookie", "requestCookie" -> return routingContext.cookieMap()
"session", "reqSession", "requestSession" -> return routingContext.session()
"user", "reqUser", "requestUser" -> return routingContext.user()
"bodyAsString", "reqBodyAsString", "requestBodyAsString" -> return routingContext.bodyAsString
"bodyAsJson", "reqBodyAsJson", "requestBodyAsJson" -> return routingContext.bodyAsJson
"bodyAsBuffer", "reqBodyAsBuffer", "requestBodyAsBuffer" -> return routingContext.body().buffer()
"routingContext", "context", "routerContext", "routContext" -> return routingContext
"rout", "router" -> return routingContext.currentRoute()
"vertx", "vertxContext" -> return routingContext.vertx()
"responseHeaders", "responseHeader" -> return routingContext.response().headers()
"uri" -> return routingContext.request().uri()
"absoluteURI" -> return routingContext.request().absoluteURI()
"authority" -> return routingContext.request().authority()
"isSSL", "ssl", "isSsl", "isSSl", "isssl", "SSL", "Ssl" -> return routingContext.request().isSSL
}
// 如果都不是以上的参数则创建一个
kotlin.runCatching {
applicationContext.autowireCapableBeanFactory.createBean(type)
}.onSuccess {
return it
}
throw IllegalArgumentException("Unable to parse parameters:$name")
}
四、全部代码
通过 @EventListener(ApplicationReadyEvent::class)
注解来确保,该初始化方法可以在Springboot 启动完成后执行
注意: 需要提前将 Router 注册到Springboot
kotlin
@Component
class RouterProcessor(val applicationContext: ApplicationContext) {
private lateinit var router: io.vertx.ext.web.Router
private val logger = LoggerFactory.getLogger(RouterProcessor::class.java)
@EventListener(ApplicationReadyEvent::class)
fun init(event: ApplicationReadyEvent) {
kotlin.runCatching {
router = applicationContext.getBeanByName("router")
for (bean in initializedBeanInstances) {
registerBean(bean)
}
}.onFailure {
logger.error(" Vertx WebRouter 初始化失败: ${it.message}", it)
}
}
/**
* 初始化完成后的 bean 列表
*/
private val initializedBeanInstances by lazy {
applicationContext.beanDefinitionNames.filter {
it != this.javaClass.simpleName &&
it != Connection::class.simpleName
}.mapNotNull {
applicationContext.getBean(it)
}
}
fun registerBean(bean: Any) {
val clazz = bean.javaClass
for (method in clazz.methods) {
runCatch {
registerMethod(method, bean)
}
}
}
fun registerMethod(method: Method, bean: Any) {
if (method.isAnnotationPresent(io.github.zimoyin.ra3.annotations.Router::class.java)) {
val path = method.getAnnotation(io.github.zimoyin.ra3.annotations.Router::class.java).path
router.let {
if (path.startsWith("^")) it.routeWithRegex(path.replaceFirst("^", ""))
else it.route(path)
}.order(0).handler {
invoke(method, bean, it)
}
return
}
if (method.isAnnotationPresent(Rout::class.java)) {
val path = method.getAnnotation(Rout::class.java).path
router.let {
if (path.startsWith("^")) it.routeWithRegex(path.replaceFirst("^", ""))
else it.route(path)
}.order(0).handler {
invoke(method, bean, it)
}
return
}
if (method.isAnnotationPresent(RouterPost::class.java)) {
val path = method.getAnnotation(RouterPost::class.java).path
router.let {
if (path.startsWith("^")) it.postWithRegex(path.replaceFirst("^", ""))
else it.post(path)
}.order(0).handler {
invoke(method, bean, it)
}
return
}
if (method.isAnnotationPresent(RouterGet::class.java)) {
val path = method.getAnnotation(RouterGet::class.java).path
router.let {
if (path.startsWith("^")) it.getWithRegex(path.replaceFirst("^", ""))
else it.get(path)
}.order(0).handler {
invoke(method, bean, it)
}
return
}
if (method.isAnnotationPresent(RouterPut::class.java)) {
val path = method.getAnnotation(RouterPut::class.java).path
router.let {
if (path.startsWith("^")) it.putWithRegex(path.replaceFirst("^", ""))
else it.put(path)
}.order(0).handler {
invoke(method, bean, it)
}
return
}
if (method.isAnnotationPresent(RouterPatch::class.java)) {
val path = method.getAnnotation(RouterPatch::class.java).path
router.let {
if (path.startsWith("^")) it.patchWithRegex(path.replaceFirst("^", ""))
else it.patch(path)
}.order(0).handler {
invoke(method, bean, it)
}
return
}
if (method.isAnnotationPresent(RouterPatch::class.java)) {
val path = method.getAnnotation(RouterDelete::class.java).path
router.let {
if (path.startsWith("^")) it.deleteWithRegex(path.replaceFirst("^", ""))
else it.delete(path)
}.order(0).handler {
invoke(method, bean, it)
}
return
}
if (method.isAnnotationPresent(RouterHead::class.java)) {
val path = method.getAnnotation(RouterHead::class.java).path
router.let {
if (path.startsWith("^")) it.headWithRegex(path.replaceFirst("^", ""))
else it.head(path)
}.order(0).handler {
invoke(method, bean, it)
}
return
}
if (method.isAnnotationPresent(RouterOptions::class.java)) {
val path = method.getAnnotation(RouterOptions::class.java).path
router.let {
if (path.startsWith("^")) it.optionsWithRegex(path.replaceFirst("^", ""))
else it.options(path)
}.order(0).handler {
invoke(method, bean, it)
}
return
}
}
fun invoke(method: Method, bean: Any, routingContext: RoutingContext) {
val args = arrayListOf<Any>()
val isHasAutoClose = method.isAnnotationPresent(AutoClose::class.java)
method.parameters.forEach {
val bean0 = kotlin.runCatching { applicationContext.getBean(it.name, it.type) }.getOrNull()
?: kotlin.runCatching { applicationContext.getBean(it.type) }.getOrNull()
if (bean0 != null) {
args.add(bean0)
} else {
args.add(createParameter(it, routingContext))
}
}
//执行方法
try {
routingContext.request().paramsCharset = "UTF-8"
val result = method.invoke(bean, *args.toTypedArray())
kotlin.runCatching {
if (isHasAutoClose) {
val response = routingContext.response()
response.putHeader("content-type", "application/json")
if (method.returnType == Unit::class.java) {
response.end()
}
if (result == null) {
response.end()
}
if (result is String) {
response.end(result)
} else if (result is Number || result is Comparable<*>) {
response.end(result.toString())
} else {
kotlin.runCatching {
response.end(result.toJsonObject().toString())
}.onFailure {
response.end()
logger.debug("自动关闭连接失败", it)
}
}
}
}
} catch (e: InvocationTargetException) {
kotlin.runCatching { routingContext.response().end("Server Error!!!!") }
logger.error("路由执行失败, $method 方法内部存在错误逻辑导致方法执行失败", e)
} catch (e: Exception) {
kotlin.runCatching { routingContext.response().end("Server Error!!!!") }
logger.error("路由执行失败", e)
}
}
private fun createParameter(value: Parameter, routingContext: RoutingContext): Any {
val name = value.name
val type = value.type
when (name) {
"res", "response", "resp" -> return routingContext.response()
"req", "request", "requ" -> return routingContext.request()
"body", "reqBody", "requestBody" -> return routingContext.body()
"headers", "header", "reqHeader", "requestHeader", "reqHeaders", "requestHeaders" -> return routingContext
.request()
.headers()
"query", "reqQuery", "requestQuery", "reqQueries", "requestQueries" -> return routingContext.queryParams()
"data", "reqData", "requestData" -> return routingContext.data()
"params", "reqParams", "requestParams" -> return routingContext.pathParams()
"cookie", "reqCookie", "requestCookie" -> return routingContext.cookieMap()
"session", "reqSession", "requestSession" -> return routingContext.session()
"user", "reqUser", "requestUser" -> return routingContext.user()
"bodyAsString", "reqBodyAsString", "requestBodyAsString" -> return routingContext.bodyAsString
"bodyAsJson", "reqBodyAsJson", "requestBodyAsJson" -> return routingContext.bodyAsJson
"bodyAsBuffer", "reqBodyAsBuffer", "requestBodyAsBuffer" -> return routingContext.body().buffer()
"routingContext", "context", "routerContext", "routContext" -> return routingContext
"rout", "router" -> return routingContext.currentRoute()
"vertx", "vertxContext" -> return routingContext.vertx()
"responseHeaders", "responseHeader" -> return routingContext.response().headers()
"uri" -> return routingContext.request().uri()
"absoluteURI" -> return routingContext.request().absoluteURI()
"authority" -> return routingContext.request().authority()
"isSSL", "ssl", "isSsl", "isSSl", "isssl", "SSL", "Ssl" -> return routingContext.request().isSSL
}
kotlin.runCatching {
applicationContext.autowireCapableBeanFactory.createBean(type)
}.onSuccess {
return it
}
throw IllegalArgumentException("Unable to parse parameters:$name")
}
fun <T : Any> runCatch(block: () -> T): T? {
try {
return block()
} catch (e: Exception) {
logger.error("路由捕获到异常", e)
}
return null
}
}
使用示例
kotlin
/**
*
* @author : zimo
* @date : 2025/01/04
*/
@RouterController
class HelloRouter(
val test :PlayerUnlockTechService
) {
/**
* 注册路由
* 正则路由以 ^ 开始
* 请求处理方法: 所有
* 方法参数可以是 routingContext 或者 router 或者 routingContext 内的任何东西。以及其他的任何东西,或者 bean
*
*/
@Rout("/hello")
// @RouterGet
fun hello(response: HttpServerResponse, request: HttpServerRequest) {
request.bodyHandler {
response.end(test.getPlayerUnlockTechsByBuilding("BD12DC34624208045CCA1ECE32071F20").toString())
}
}
/**
* 注册路由(并自动关闭,将返回值发送会前端)
* 正则路由以 ^ 开始
* 请求处理方法: GET
* 方法参数可以是 routingContext 或者 router 或者 routingContext 内的任何东西。以及其他的任何东西,或者 bean
*
*/
@RouterGet("/test/send_message/:message")
@AutoClose
fun send(response: HttpServerResponse, request: HttpServerRequest):String {
return "你好"
}
}
}
Main 方法
kotlin
/**
*
* @author : zimo
* @date : 2025/01/03
*/
@SpringBootApplication
@EnableCaching
@MapperScan(basePackages = ["io.github.zimoyin.ra3.mapper"])
class ApplicationStart2(
val vertx: Vertx,
val router: Router
) {
companion object {
@JvmStatic
fun main(args: Array<String>) {
runApplication<ApplicationStart2>(*args)
}
}
@PostConstruct
fun onStartVertxWeb() {
vertx.createHttpServer().requestHandler(router).listen(8090).onSuccess {
println("启动成功")
}
}
}
@Configuration
class VertxConfig {
@Bean("vertx")
fun vertx(): Vertx {
return Vertx.vertx()
}
@Bean("router")
fun router(vertx: Vertx): Router? {
return Router.router(vertx)
}
}