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.IllegalUserName
has error codeILLEGAL_USER_NAME
-
❿ Exception class
UserInfoException.PasswordTooShort
has error codePASSWORD_TOO_SHORT
-
⓫ Exception class
UserInfoException.PasswordsNotSame
has 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:
-
Retention
is configured asRUNTIME
, i.e. reflectable at runtime -
Target
is configured asMETHOD
orFUNCTION
(kotlin), can be used to decorate methods -
Has a property named
value
-
The
value
property is of array type -
The array element type of
value
is 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...
};
-
❶
AllErrors
defines 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_DENIED
andPlatformErrorCode.DATA_IS_FROZEN
, but they have not yet been declared as throwable by any REST API using@ThrowsAll
or custom@ThrowsPlatformError
. So they will not be defined inAllErrors
. -
❷ The enum name decorated with
@ErrorFamily
is thefamily
field in client code, used to identify which family the error belongs to -
❸ The enum variable name is the
code
field 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❶
AllErrors
defines all errors, never duplicated.ApiErrors
at ❺ 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 reasonAllErrors
andApiErrors
are separated. -
❺ Errors that each REST API may throw
-
❻ Errors that
userController
module may throw -
❼ Errors that
userController.registerUser
API 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:userController
andbookController
-
ApiErrors["userController"]
has three options:login
,logout
andregisterUser
-
For
ApiErrors["userController"]["registerUser"]
, thefamily
field has two options:USER_INFO_ERROR_CODE
andPLATFORM_ERROR_CODE
-
Once
family
is determined asUserInfoErrorCode
, thecode
field has three options:ILLEGAL_USER_NAME
,PASSWORD_TOO_SHORT
andPASSWORDS_NOT_SAME
-
Once
code
is determined asILLEGAL_USER_NAME
, the additionalillegalChars
field can be used