SpringBoot

SpringBoot声称可以很简单地创建独立的生产级的直接运行的Spring应用,那我们就来手撕一个试试。

创建项目

打开IntelliJ IDEA,新建项目:Spring Initializr。

配置:
Name: InitProj
Language: Kotlin
Type: Gradle
Sdk: jbr-11
Java: 11
SpringBoot: 2.7.3
Dependences: Web>SpringWeb

start.spring.io 打不开?你可能需要一点上网技术。

配置阿里云仓库

如果你想依赖包下载快一点,建议配置阿里云仓库。
打开 build.gradle.kts 在 repositories 中添加两具仓库:

maven {
        setUrl("https://maven.aliyun.com/repository/public/")
    }
    maven {
        setUrl("https://maven.aliyun.com/repository/spring/")
    }

HelloWord

创建Kolin类HelloController

@RestController
class HelloController {
    @GetMapping("/hello")
    fun hello(): String{
        return "hello"
    }
}

运行启动项目
浏览器输入
http://localhost:8080/hello Ok,小试水果刀,前菜结束。

响应数据结构化

新需求:返回数据是这样 json

{"code":0,"message":"success","data":"hello"}

我们需要:
一个定义错误信息的 Data Class

data class AError(val code: Int, val msg: String)

一些常见的错误信息

inline val SUCCESS
    get() = AError(0, "success")
inline val ERROR_UNKNOWN
    get() = AError(-1, "unknown")
inline val PARAMS_NULL
    get() = AError(-2, "缺少必填参数")
inline val SESSION_ERROR
    get() = AError(-3, "session失效")

一个存储响应结果的 Data Class

data class Result(
    var code: Int = 0,
    var message: String = "",
    var data: Any? = null
)

一些常见的结果

fun succeed(data: Any?) = Result(SUCCESS.code, SUCCESS.msg, data)
fun succeed() = succeed(null)
fun failed(err: AError) = Result(err.code, err.msg, null)

然后修改一下 hello 处理函数

@GetMapping("/hello")
    fun hello(): Result{
        return succeed("hello")
    }

再次运行一下,结果是我们想要的。SpringBoot 帮我们做了比较多的处理,这里不作细论。

异常处理

如果程序发生错误,前端会得到这样的结果:

{
    "timestamp": "2022-08-26T14:34:48.440+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/hello"
}

我们希望得到这样的结果:

{"code":-1,"message":"unknown","data":null}

前端的格式比较统一,方便处理。
首先自定义一个异常

class MyException(val err: AError) : RuntimeException(err.msg)

然后添加一个异常处理组件,SpringBoot 会在异常发生时调用它来处理。

@ControllerAdvice
class ExceptionHandler {

    companion object {
        private const val TAG = "ExceptionHandler"
    }

    @ExceptionHandler(Exception::class)
    @ResponseBody
    fun handle(e: Exception): Result {
        val result = if (e is MyException) {
            failed(e.err)
        } else {
            failed(ERROR_UNKNOWN)
        }
        Log.e(TAG, e)
        return result
    }

}

这样如果发生我们定义的错误,会返回对应信息;其他错误则返回未知。
测试一下Hello:

@GetMapping("/hello")
    fun hello(): Int{
        // return 2/0
        return failed(PARAMS_NULL)
    }

结果是我们想要的。

参数校验

请求参数需要先过滤一遍,防止引入未知错误。
虽然 Spring 有@Valid 注解可以很方便地验证请求参数的合法性,但是我更倾向于自定义验证内容。
首先定义一个抽象类 ARequest,作为所有请求参数的父类。

abstract class ARequest {
    // 验证参数
    abstract fun validate()

    // 检验参数是否为空
    fun paramsNotBlank(vararg params: String){
        params.forEach {
            if(it.isBlank()){
                throw MyException(PARAMS_NULL)
            }
        }
    }
}

然后定义一个切面,在特定函数执行前进行参数校验。

@Aspect
@Component
class RequestAspect {

    companion object{
        private const val TAG = "RequestAspect"
    }

    @Pointcut("execution(public * *(ARequest+,..))")
    fun point(){}

    @Before("point()")
    fun checkRequest(join: JoinPoint){
        join.args.forEach {
            if(it is ARequest){
                it.validate()
            }
        }
    }

}

面向切面编程需要引入依赖
implementation(“org.springframework.boot:spring-boot-starter-aop:2.7.3”)

切点"execution(public * *(ARequest+,…))"的意思是:在任何类型为public,第一个参数是ARequest的子类的函数处作切点。

本例中如果需要指定打招呼的人名,可以这样定义请求参数:

data class HelloRequest(
    var name: String = "",
): ARequest(){
    override fun validate() {
        paramsNotBlank(name)
    }
}

修改 Hello 函数:

@GetMapping("/hello")
    fun hello(request: HelloRequest): Result{
        return succeed("Hello ${request.name}")
    }

不指定name参数时,返回:

{"code":-2,"message":"缺少必填参数","data":null}

指定name 为 Tom , 返回:

{"code":0,"message":"success","data":"Hello Tom"}

总结

SpringBoot 为我们做了相当多的配置工作,使用起来很便捷。本篇文章没有原理解释,更像一道练习题,用来熟悉 SpringBoot 的起手式,更多细节以后探讨。