生成客户端异常
这部分功能其实和ORM没有关系。
然而,既然提供了前后端API免对接,那么就必须提供这部分功能,否则就不是一套完整的方案。
异常簇
我们需要定义特别的Java/Kotlin业务异常,它们能够被直接翻译成客户端可以理解的数据。
该如何定义这种特别的业务异常类型呢?有两种极端的选择
-
整个应用程序共享一个业务异常类,并内置客户端可以理解的error code
-
每个业务错误都定义一个业务异常类
很明显,上述两种方案都不合适。第一种方案粒度太粗,全局的error code难以维护;第二种方案粒度太细,需要定义的异常类是在太多了。
所以,Jimmer选择折中方案,把业务异常分为多个簇,每个簇用一套Error Code。
用户可以采用两种方法定义异常簇
-
自动生成
-
纯手写
自动生成异常簇
Jimmer采用枚举来定义异常簇,枚举也是这簇异常的错误码,例如
定义异常簇
- Java
- Kotlin
package com.example.business.error;
import org.babyfish.jimmer.error.ErrorFamily;
@ErrorFamily
public enum UserInfoErrorCode {
ILLEGAL_USER_NAME,
PASSWORD_TOO_SHORT,
PASSWORDS_NOT_SAME
}
package com.example.business.error
import org.babyfish.jimmer.error.ErrorFamily
@ErrorFamily
enum class UserInfoErrorCode {
ILLEGAL_USER_NAME,
PASSWORD_TOO_SHORT,
PASSWORDS_NOT_SAME
}
@org.babyfish.jimmer.error.ErrorFamily
表示该枚举表示一簇业务异常,该枚举也是这簇异常的ErrorCode.
注解@ErrorFamily
会被Jimmer的预编译器处理
这里的预编译器,对于Java而言就是Annotation Processor;对于Kotlin而言就是KSP。
这部分类型已经在概述/快速上手/生成代码一节中讨论过,此处不在重复。
预编译器会会根据此枚举生成如下异常类
用 作声明异常簇的枚举可以选择用ErrorCode
或Error
结尾。
-
如果有这样的特殊结尾,异常类名 = 枚举名去掉这样的结尾并加 +
Exception
-
否则,异常类名 = 枚举名 +
Exception
所以,这里生成的异常名为UserInfoException
经过Jimmer的编译,将会生成如下的异常类。
- Java
- Kotlin
public abstract class UserInfoException
extends CodeBasedRuntimeException { ❶
private UserInfoException(String message, Throwable cause) {
super(message, cause);
}
@Override
public abstract UserInfoErrorCode getCode(); ❷
public static IllegalUserName illegalUserName(@NotNull String message) { ❸
return new IllegalUserName(
message,
null
);
}
public static IllegalUserName illegalUserName( ❹
@NotNull String message,
@Nullable Throwable cause
) {
return new IllegalUserName(
message,
cause
);
}
public static PasswordTooShort passwordTooShort(@NotNull String message) { ❺
return new PasswordTooShort(
message,
null
);
}
public static PasswordTooShort passwordTooShort( ❻
@NotNull String message,
@Nullable Throwable cause
) {
return new PasswordTooShort(
message,
cause
);
}
public static PasswordsNotSame passwordsNotSame(@NotNull String message) { ❼
return new PasswordsNotSame(
message,
null
);
}
public static passwordsNotSame passwordsNotSame( ❽
@NotNull String message,
@Nullable Throwable cause
) {
return new PasswordsNotSame(
message,
cause
);
}
public static class IllegalUserName extends UserInfoException {
public IllegalUserName(String message, Throwable cause) {
super(message, cause);
}
@Override
public UserInfoErrorCode getCode() {
return UserInfoErrorCode.ILLEGAL_USER_NAME; ❾
}
@Override
public Map<String, Object> getFields() {
return Collections.emptyMap();
}
}
public static class PasswordTooShort extends UserInfoException {
public PasswordTooShort(String message, Throwable cause) {
super(message, cause);
}
@Override
public UserInfoErrorCode getCode() {
return UserInfoErrorCode.PASSWORD_TOO_SHORT; ❿
}
@Override
public Map<String, Object> getFields() {
return Collections.emptyMap();
}
}
public static class PasswordsNotSame extends UserInfoException {
public PasswordsNotSame(String message, Throwable cause) {
super(message, cause);
}
@Override
public UserInfoErrorCode getCode() {
return UserInfoErrorCode.PASSWORDS_NOT_SAME; ⓫
}
@Override
public Map<String, Object> getFields() {
return Collections.emptyMap();
}
}
}
public abstract class UserInfoException private constructor(
message: String,
cause: Throwable? = null,
) : CodeBasedRuntimeException(message, cause) { ❶
public abstract override val code: UserInfoErrorCode ❷
public companion object {
@JvmStatic
public fun illegalUserName( ❸❹
message: String,
cause: Throwable? = null
): IllegalUserName =
IllegalUserName(
message,
cause
)
@JvmStatic
public fun passwordTooShort( ❺❻
message: String,
cause: Throwable? = null
): PasswordTooShort =
PasswordTooShort(
message,
cause
)
@JvmStatic
public fun passwordsNotSame( ❼❽
message: String,
cause: Throwable? = null
): PasswordsNotSame =
PasswordsNotSame(
message,
cause
)
}
public class IllegalUserName(
message: String,
cause: Throwable? = null,
) : UserInfoException(message, cause) {
public override val code: UserInfoErrorCode
get() = UserInfoErrorCode.ILLEGAL_USER_NAME ❾
public override val fields: Map<String, Any?>
get() = emptyMap()
}
public class PasswordTooShort(
message: String,
cause: Throwable? = null,
) : UserInfoException(message, cause) {
public override val code: UserInfoErrorCode
get() = UserInfoErrorCode.PASSWORD_TOO_SHORT ❿
public override val fields: Map<String, Any?>
get() = emptyMap()
}
public class PasswordsNotSame(
message: String,
cause: Throwable? = null,
) : UserInfoException(message, cause) {
public override val code: UserInfoErrorCode
get() = UserInfoErrorCode.PASSWORDS_NOT_SAME ⓫
public override val fields: Map<String, Any?>
get() = emptyMap()
}
}
-
❶ 基于枚举错误码的异常必然继承
org.babyfish.jimmer.error.CodeBasedRuntimeException
-
❷ 这一簇异常的错误码的类型为
UserInfoErrorCode
-
❸❹ 构建错误码为
ILLEGAL_USER_NAME
的异常的静态方法 -
❺❻ 构建错误码为
PASSWORD_TOO_SHORT
的异常的静态方法 -
❼❽ 构建错误码为
PASSWORDS_NOT_SAME
的异常的静态方法 -
❾ 异常类
UserInfoException.IllegalUserName
的错误码为ILLEGAL_USER_NAME
-
❿ 异常类
UserInfoException.PasswordTooShort
的错误码为PASSWORD_TOO_SHORT
-
⓫ 异常类
UserInfoException.PasswordsNotSame
的错误码为PASSWORDS_NOT_SAME
为异常码添加字段
可以为任何一个错误码添加附加字段。
比如,ILLEGAL_USER_NAME
表示非法的用户名,即用户名包含了非法字符。我们可以为其添加字段illegalChars
- Java
- Kotlin
@ErrorFamily
public enum UserInfoErrorCode {
@ErrorField(name = "illegalChars", type = Character.class, list = true)
ILLEGAL_USER_NAME,
PASSWORD_TOO_SHORT,
PASSWORDS_NOT_SAME
}
@ErrorFamily
enum class UserInfoErrorCode {
@ErrorField(name = "illegalChars", type = Char::class, list = true)
ILLEGAL_USER_NAME,
PASSWORD_TOO_SHORT,
PASSWORDS_NOT_SAME
}
生成的代码的如下
- Java
- Kotlin
public abstract class UserInfoException extends CodeBasedRuntimeException {
public static IllegalUserName illegalUserName(
@NotNull String message,
@NotNull List<Character> illegalChars
) {
...省略代码...
}
public static IllegalUserName illegalUserName(
@NotNull String message,
@Nullable Throwable cause,
@NotNull List<Character> illegalChars
) {
...省略代码...
}
public static class IllegalUserName extends UserInfoException {
@NotNull
private final List<Character> illegalChars;
public IllegalUserName(
String message,
Throwable cause,
@NotNull List<Character> illegalChars
) {
super(message, cause);
this.illegalChars = illegalChars;
}
@Override
public Map<String, Object> getFields() {
return Collections.singletonMap("illegalChars", illegalChars);
}
@NotNull
public List<Character> getIllegalChars() {
return illegalChars;
}
...省略其他代码...
}
...省略其他代码...
}
public abstract class UserInfoException private constructor(
message: String,
cause: Throwable? = null,
) : CodeBasedRuntimeException(message, cause) { ❶
public abstract override val code: UserInfoErrorCode ❷
public companion object {
@JvmStatic
public fun illegalUserName(
message: String,
cause: Throwable? = null,
illegalChars: List<Char>,
): IllegalUserName =
...省略代码...
...省略其他代码...
}
public class IllegalUserName(
message: String,
cause: Throwable? = null,
public val illegalChars: List<Char>,
) : UserInfoException(message, cause) {
public override val fields: Map<String, Any?>
get() = mapOf(
"illegalChars" to illegalChars
)
...省略其他代码...
}
}
手写异常簇
【TODO】
为REST API声明异常
允许抛出整个异常簇
所谓抛出异常簇中部分异常,就是抛出抽象的异常类型 (例如:throws UserInfoException)。
Jimmer允许任何HTTP服务方法抛出异常
-
Java: 使用throws语法
-
Kotlin:使用
@kotlin.Throws
注解
- Java
- Kotlin
package com.example.service;
import org.babyfish.jimmer.client.ThrowsAll;
@PostMapping("/user")
public void registerUser(
@RequestBody RegisterUserInput input
) throws UserInfoException {
...省略代码...
}
package com.example.service
import import org.babyfish.jimmer.client.ThrowsAll
@PostMapping("/user")
@Throws(UserInfoException::class)
fun registerUser(@RequestBody input: RegisterUserInput) {
...省略代码...
}
允许抛出异常簇中部分异常
所谓抛出异常簇中部分异常,就是抛出具体的异常类型 (例如:throws UserInfoException.PasswordTooShort)。
- Java
- Kotlin
@PostMapping("/user")
public void registerUser(
@RequestBody RegisterUserInput input
) throws UserInfoException.PasswordTooShort {
...省略代码...
}
@PostMapping("/user")
@Throws(UserInfoException.PasswordsNotSame::class)
fun registerUser(@RequestBody input: RegisterUserInput) {
...省略代码...
}
比较和建议
其实允许抛出整个异常簇就是抛出簇内所有异常。
即,抛出整个异常簇throws UserInfoException
,其实和抛出具体异常类型列表throws UserInfoException.IllegalUserName, UserInfoException.PasswordTooShort, UserInfoException.PasswordsNotSame
等价。
建议尽量抛出具体的异常类型列表,以减少客户端需要处理的异常的种类。
导出服务端异常
前面的工作只是声明REST API有可能抛出何种异常。接下来我们讨论在服务端真正 抛出异常
抛出异常
- Java
- Kotlin
@PostMapping("/user")
public void registerUser(
@RequestBody RegisterUserInput input
) throws UserInfoException.IllegalUserName {
if (...某些条件...) {
List<Character> illegalChars = ...略...
throw UserInfoException.illegalUserName(
"The user name is invalid",
illegalChars
);
}
...省略其他代码...
}
@PostMapping("/user")
@Throws(UserInfoException.IllegalUserName::class)
fun registerUser(@RequestBody input: RegisterUserInput) {
if (...某些条件...) {
val illegalChars: List<Char> = ...略...
throw UserInfoException.illegalUserName(
"The user name is invalid",
illegalChars
)
}
...省略其他代码...
}
内部代码抛出的异常的种类,不得超过对外声明的种类