Generate Client Errors
This part is actually unrelated to ORM.
However, since generate client api is provided, this part of the functionality must also be provided to make it a complete solution.
Exception Families
We need to define special Java/Kotlin business exceptions that can be directly translated into data that the client can understand.
How do we define this special type of business exception? There are two extreme choices:
-
Share one business exception class across the entire application, with built-in error codes that the client can understand
-
Define a business exception class for each business error
Obviously, both of the above solutions are unsuitable. The first option is too coarse-grained, with globally maintained error codes that are difficult to maintain. The second option is too fine-grained, requiring the definition of too many exception classes.
So, Jimmer chooses a compromise: dividing business exceptions into multiple families, with each family using one set of Error Codes.
Jimmer uses enums to define exception families, where the enum also serves as the error code for that family, e.g.
Define Exception Families
- 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
}
The @org.babyfish.jimmer.error.ErrorFamily annotation indicates that this enum represents a family of business exceptions, and the enum itself serves as the error code for this family.
The @ErrorFamily annotation will be processed by Jimmer's precompiler
Here, the precompiler refers to Annotation Processor for Java, and KSP for Kotlin.
This part of the code has been discussed in Overview/Get Started/Generate Code, so it won't be repeated here.
The precompiler will generate the following exception class based on this enum:
For enums used to declare exception families, you can choose to use ErrorCode or Error as a special suffix.
-
If there is such a special suffix, the exception class name = enum name without the suffix +
Exception -
Otherwise, exception class name = enum name +
Exception
So here the generated exception name is UserInfoException
After compilation by Jimmer, the following exception class will be generated.
- Java
- Kotlin
public abstract class UserInfoException
extends CodeBasedException { ❶
private UserInfoException(String message, Throwable cause) {
super(message, cause);
}
@Override
public abstract UserInfoErrorCode getCode(); ❷
public static UserInfoException illegalUserName(@NotNull String message) { ❸
return new IllegalUserName(
message,
null
);
}
public static UserInfoException illegalUserName( ❹
@NotNull String message,
@Nullable Throwable cause
) {
return new IllegalUserName(
message,
cause
);
}
public static UserInfoException passwordTooShort(@NotNull String message) { ❺
return new PasswordTooShort(
message,
null
);
}
public static UserInfoException passwordTooShort( ❻
@NotNull String message,
@Nullable Throwable cause
) {
return new PasswordTooShort(
message,
cause
);
}
public static UserInfoException passwordsNotSame(@NotNull String message) { ❼
return new PasswordsNotSame(
message,
null
);
}
public static UserInfoException 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,
) : CodeBasedException(message, cause) { ❶
public abstract override val code: UserInfoErrorCode ❷
public companion object {
@JvmStatic
public fun illegalUserName( ❸❹
message: String,
cause: Throwable? = null
): UserInfoException =
IllegalUserName(
message,
cause
)
@JvmStatic
public fun passwordTooShort( ❺❻
message: String,
cause: Throwable? = null
): UserInfoException =
PasswordTooShort(
message,
cause
)
@JvmStatic
public fun passwordsNotSame( ❼❽
message: String,
cause: Throwable? = null
): UserInfoException =
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()
}
}
-
❶ Exceptions based on enum error codes must inherit from
org.babyfish.jimmer.error.CodeBasedException -
❷ The error code type for this family of exceptions is
UserInfoErrorCode -
❸❹ Static method to construct exception with error code
ILLEGAL_USER_NAME -
❺❻ Static method to construct exception with error code
PASSWORD_TOO_SHORT -
❼❽ Static method to construct exception with error code
PASSWORDS_NOT_SAME -
❾ Exception class
UserInfoException.IllegalUserNamehas error codeILLEGAL_USER_NAME -
❿ Exception class
UserInfoException.PasswordTooShorthas error codePASSWORD_TOO_SHORT -
⓫ Exception class
UserInfoException.PasswordsNotSamehas error codePASSWORDS_NOT_SAME
Add Fields to Error Codes
Additional fields can be added to any error code.
For example, ILLEGAL_USER_NAME indicates an illegal username, i.e. the username contains illegal characters. We can add the field illegalChars for it.
- 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
}
The generated code is as follows:
- Java
- Kotlin
public abstract class UserInfoException extends CodeBasedException {
public static UserInfoException illegalUserName(
@NotNull String message,
@NotNull List<Character> illegalChars
) {
...omit code...
}
public static UserInfoException illegalUserName(
@NotNull String message,
@Nullable Throwable cause,
@NotNull List<Character> illegalChars
) {
...omit code...
}
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;
}
...omit other code...
}
...omit other code...
}
public abstract class UserInfoException private constructor(
message: String,
cause: Throwable? = null,
) : CodeBasedException(message, cause) { ❶
public abstract override val code: UserInfoErrorCode ❷
public companion object {
@JvmStatic
public fun illegalUserName(
message: String,
cause: Throwable? = null,
illegalChars: List<Char>,
): UserInfoException =
...omit code...
...omit other code...
}
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
)
...omit other code...
}
}
Declare Exceptions for REST APIs
Allow To Throw All Exceptions Of Family
- Java
- Kotlin
package com.example.service;
import org.babyfish.jimmer.client.ThrowsAll;
@PostMapping("/user")
@ThrowsAll(UserInfoErrorCode.class)
public void registerUser(@RequestBody RegisterUserInput input) {
...omit code...
}
package com.example.service
import import org.babyfish.jimmer.client.ThrowsAll
@PostMapping("/user")
@ThrowsAll(UserInfoErrorCode::class)
fun registerUser(@RequestBody input: RegisterUserInput) {
...omit code...
}
The @org.babyfish.jimmer.client.ThrowsAll annotation allows the REST API to throw all exceptions in the family.
Allow To Throw Some Exceptions Of Family
Let's locate another business exception family:
- Java
- Kotlin
@ErrorFamily
public enum PlatformErrorCode {
PERMISSION_DENIED,
DATA_IS_FROZEN,
SERVICE_IS_SUSPENDED,
}
@ErrorFamily
enum class PlatformErrorCode {
PERMISSION_DENIED,
DATA_IS_FROZEN,
SERVICE_IS_SUSPENDED
}
To be able to declare that a REST API can only throw some of the exceptions in this family, define the following annotation:
- Java
- Kotlin
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ThrowsPlatformError {
PlatformErrorCode[] value();
}
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class ThrowsPlatformError(
val value: Array<PlatformErrorCode>
)
This annotation satisfies the following 5 conditions:
-
Retentionis configured asRUNTIME, i.e. reflectable at runtime -
Targetis configured asMETHODorFUNCTION(kotlin), can be used to decorate methods -
Has a property named
value -
The
valueproperty is of array type -
The array element type of
valueis an enum decorated with@ErrorFamily.
If an annotation satisfies all the above conditions, it can be used to decorate REST APIs to declare the exceptions that can be thrown. Just like how we use @ThrowsAll.
- Java
- Kotlin
@PostMapping("/user")
@ThrowsAll(UserInfoErrorCode.class)
@ThrowsPlatformError({PlatformErrorCode.SERVICE_IS_SUSPENDED})
public void registerUser(@RequestBody RegisterUserInput input) {
...omit code...
}
@PostMapping("/user")
@ThrowsAll(UserInfoErrorCode::class)
@ThrowsPlatformError([PlatformErrorCode.SERVICE_IS_SUSPENDED])
fun registerUser(@RequestBody input: RegisterUserInput) {
...omit code...
}
Export Server Side Exceptions
Throw Exceptions
- Java
- Kotlin
@PostMapping("/user")
@ThrowsAll(UserInfoErrorCode.class)
@ThrowsPlatformError({PlatformErrorCode.SERVICE_IS_SUSPENDED})
public void registerUser(@RequestBody RegisterUserInput input) {
if (...some condition...) {
throw PlatformException.serviceIsSuspended(
"The service is suspended"
);
}
...omit other code...
}
@PostMapping("/user")
@ThrowsAll(UserInfoErrorCode::class)
@ThrowsPlatformError([PlatformErrorCode.SERVICE_IS_SUSPENDED])
fun registerUser(@RequestBody input: RegisterUserInput) {
if (...some condition...) {
throw PlatformException.serviceIsSuspended(
"The service is suspended"
)
}
...omit other code...
}
The kinds of exceptions thrown internally must not exceed those declared externally.
Write Exception Messages to HTTP Responses
As long as Jimmer's Spring Boot Starter is used, no extra work is required. Any exception inherited from CodeBasedException thrown by the service will be automatically translated.
The translation result is:
{
"family":"USER_INFO_ERROR_CODE",
"code":"ILLEGAL_USER_NAME",
"illegalChars": ["&", "`", "@"]
}
To facilitate development and testing, you can configure in application.yml or application.properties:
jimmer:
error-translator:
debug-info-supported: true
This configuration will include information useful for locating issues during development and testing in the HTTP response. Since the content is quite long, please click the button below to view the result.
This configuration is only for assisting development and testing, and must NOT be enabled in production environments!
For example, enable this switch in application-dev.yml and application-test.yml, but absolutely do NOT enable it in application-prod.yml.
Client
Generated TypeScript Code
export type AllErrors = ❶
{
readonly family: "PLATFORM_ERROR_CODE", ❷
readonly code: "SERVICE_IS_SUSPENDED" ❸
} |
{
readonly family: "USER_INFO_ERROR_CODE",
readonly code: "ILLEGAL_USER_NAME",
readonly "illegalChars": ReadonlyArray<number> ❹
} |
{
readonly family: "USER_INFO_ERROR_CODE",
readonly code: "PASSWORDS_NOT_SAME"
} |
{
readonly family: "USER_INFO_ERROR_CODE",
readonly code: "PASSWORD_TOO_SHORT"
} |
...omit other code...
;
export type ApiErrors = { ❺
"userController": { ❻
"registerUser": AllErrors & ( ❼
{
readonly family: 'USER_INFO_ERROR_CODE',
readonly code: 'ILLEGAL_USER_NAME',
readonly [key:string]: any
} |
{
readonly family: 'USER_INFO_ERROR_CODE',
readonly code: 'PASSWORD_TOO_SHORT',
readonly [key:string]: any
} |
{
readonly family: 'USER_INFO_ERROR_CODE',
readonly code: 'PASSWORDS_NOT_SAME',
readonly [key:string]: any
} |
{
readonly family: 'PLATFORM_ERROR_CODE',
readonly code: 'SERVICE_IS_SUSPENDED',
readonly [key:string]: any
}
),
...omit other code...
},
"bookController": {
...omit code...
},
...omit other code...
};
-
❶
AllErrorsdefines all error typescautionOnly REST APIs that explicitly declare exceptions that may be thrown via annotations in Java/Kotlin code will be used to generate TypeScript code.
For example, in the Java/Kotlin code above, we defined the error codes
PlatformError.PERMISSION_DENIEDandPlatformErrorCode.DATA_IS_FROZEN, but they have not yet been declared as throwable by any REST API using@ThrowsAllor custom@ThrowsPlatformError. So they will not be defined inAllErrors. -
❷ The enum name decorated with
@ErrorFamilyis thefamilyfield in client code, used to identify which family the error belongs to -
❸ The enum variable name is the
codefield in client code, used to identify which specific error it is -
❹ The server uses
@ErrorField(can repeat) to declare arbitrary additional fields for an error code (only one here), so all additional fields are generated heretip❶
AllErrorsdefines all errors, never duplicated.ApiErrorsat ❺ refers to errors, may have duplicates. Because different REST APIs may declare throwing the same error code.So additional fields are always declared in
AllErrors, not inApiErrors. This is the fundamental reasonAllErrorsandApiErrorsare separated. -
❺ Errors that each REST API may throw
-
❻ Errors that
userControllermodule may throw -
❼ Errors that
userController.registerUserAPI may throw
TypeScript IDE Effects
The above TypeScript code is quite lengthy, and considering some readers may not have TypeScript background knowledge, here are some IDE auto-complete screenshots:
-
Under global type
ApiErrors, there are two options:userControllerandbookController -
ApiErrors["userController"]has three options:login,logoutandregisterUser
-
For
ApiErrors["userController"]["registerUser"], thefamilyfield has two options:USER_INFO_ERROR_CODEandPLATFORM_ERROR_CODE
-
Once
familyis determined asUserInfoErrorCode, thecodefield has three options:ILLEGAL_USER_NAME,PASSWORD_TOO_SHORTandPASSWORDS_NOT_SAME
-
Once
codeis determined asILLEGAL_USER_NAME, the additionalillegalCharsfield can be used