param-validate
在開發過程中,用戶傳遞的數據不一定合法,雖然可以通過前端進行一些校驗,但是為了確保程序的安全性,保證數據的合法,在后台進行數據校驗也是十分必要的。
后台的參數校驗的解決方案
在controller方法中校驗:
后台的參數是通過controller方法獲取的,所以最簡單的參數校驗的方法,就是在controller方法中進行參數校驗。在controller方法中如果進行參數校驗會有大量重復、沒有太大意義的代碼。
使用攔截器、過濾器校驗
為了保證controller中的代碼有更好的可讀性,可以將參數校驗的工作交由攔截器(Interceptor)或者過濾器(Filter)來完成,但是此時又存在一個問題:非共性的參數需要每個方法都創建一個與之對應的攔截器(或者過濾器)。
實現對Entity的統一校驗
鑒於上述解決方案的缺點,我們可以借助AOP的思想來進行統一的參數校驗。思想是通過自定義注解來完成對實體類屬性的標注,在AOP中掃描加了自定義注解的屬性,對其進行注解屬性標注的校驗。對於不滿足的參數直接拋出自定義異常,交由全局異常處理來處理並返回友好的提示信息。
認識Param-Validate
以上的解決方案各有優劣,param-validate采用了AOP+注解的形式解決了統一參數的校驗。使用四個注解可以解決大部分的參數校驗,有效的簡化了代碼、提高了程序的可讀性和開發效率。
Param-Validate基於SpringBoot2.3.0.RELEASE,所以請保證你的SpringBoot版本和其一致,如果必須使用其他版本,可以將其內包含的SpringBoot依賴排除。
依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.18338862369</groupId>
<artifactId>param-validate</artifactId>
<version>1.0.0</version>
<description>SpringBoot ParamValidate</description>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<aspectjweaver.version>1.9.5</aspectjweaver.version>
<lombok.version>1.18.12</lombok.version>
</properties>
<dependencies>
<!--web內置了jackson-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- aspect -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectjweaver.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- <optional>true</optional>-->
<version>${lombok.version}</version>
</dependency>
</dependencies>
</project>
項目結構
下圖是項目的結構圖。接下來,我們逐個包來介紹一下
response包
此包中定義的內容為用戶相應的類
HttpStatus枚舉
這個枚舉是從Spring源碼中拿到,並且拓展的一個枚舉。其封裝了很多常見的狀態碼和狀態信息。
package cn.rayfoo.validate.response;
/**
* @author rayfoo@qq.com
* @version 1.0
* @date 2020/8/5 11:37
* @description
*/
public enum HttpStatus {
/**
* 登錄成功
* {@code 600 Login Success}.
*/
LOGIN_SUCCESS(600, "Login Success" ),
/**
* 重新登錄
* {@code 605 Login Success}.
*/
LOGIN_AGAIN(605, "Login AGAIN" ),
/**
* 登錄超時
* {@code 601 Login Out Time}.
*/
LOGIN_OUT_TIME(601, "Login Out Time" ),
/**
* 用戶無訪問權限
* {@code 602 Not Roles}.
*/
NOT_ROLES(602, "Not Roles" ),
/**
* 用戶未注冊
* {@code 603 Not Register}.
*/
NOT_REGISTER(603, "Not Register未找到該賬號" ),
/**
* 用戶未注冊
* {@code 604 AuthenticationExcption}.
*/
AUTHENTICATION_EXCPTION(604, "身份認證錯誤" ),
/**
* 未知的賬號異常
* {@code 606 Unknown Account}.
*/
UNKNOWN_ACCOUNT(606, "賬號已注銷" ),
/**
* 請求中的參數有誤
* {@code 705 Parameter Error}.
*/
PARAMETER_ERROR(705, "Parameter Error" ),
/**
* 驗證碼錯誤
* {@code 704 Invalid Captcha}.
*/
INVALID_CAPTCHA(704, "Invalid Captcha驗證碼錯誤" ),
/**
* 郵箱和手機號驗證錯誤
*/
EMAIL_OR_PHONE_ERROR(703, "email or phone error" ),
/**
* 用戶已激活
*/
HAS_BEEN_ACTIVATED(702, "has been activated" ),
/**
* 用戶名或密碼錯誤
*/
USERNAME_OR_PASSWORD_ERROR(700, "username or password error用戶名或密碼錯誤" ),
/**
* 用戶未啟用
*/
USER_NOT_ENABLED(699, "user not enabled" ),
/**
* 驗證碼錯誤
*/
ACTIVATION_CODE_ERROR(698, "Activation code error" ),
/**
* 用戶名被占用
*/
USERNAME_IS_OCCUPIED(697, "Username is occupied" ),
/**
* 返回無數據
* {@code 706 not data}.
*/
NOT_DATA(706, "not data" ),
/**
* 流程操作成功
* {@code 710 Successful operation}.
*/
SUCCESSFUL_OPERATION(710, "Successful operation" ),
/**
* 數據沖突,無法存儲!
*/
DATA_CONFLICT(711, "Data conflict" ),
/**
* 刪除操作失敗
*/
CANNOT_DELETE(712, "Cannot delete" ),
/**
* 操作失敗
* {@code 720 operation failed}.
*/
FAILED_OPERATION(720, "operation failed" ),
NOT_VTASKSTATUS(709, "not vtaskstatus" ),
// 1xx Informational
/**
* {@code 100 Continue}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.1.1">HTTP/1.1</a>
*/
CONTINUE(100, "Continue" ),
/**
* {@code 101 Switching Protocols}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.1.2">HTTP/1.1</a>
*/
SWITCHING_PROTOCOLS(101, "Switching Protocols" ),
/**
* {@code 102 Processing}.
*
* @see <a href="http://tools.ietf.org/html/rfc2518#section-10.1">WebDAV</a>
*/
PROCESSING(102, "Processing" ),
/**
* {@code 103 Checkpoint}.
*
* @see <a href="http://code.google.com/p/gears/wiki/ResumableHttpRequestsProposal">A proposal for supporting
* resumable POST/PUT HTTP requests in HTTP/1.0</a>
*/
CHECKPOINT(103, "Checkpoint" ),
// 2xx Success
/**
* {@code 200 OK}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.2.1">HTTP/1.1</a>
*/
OK(200, "OK" ),
/**
* {@code 201 Created}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.2.2">HTTP/1.1</a>
*/
CREATED(201, "Created" ),
/**
* {@code 202 Accepted}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.2.3">HTTP/1.1</a>
*/
ACCEPTED(202, "Accepted" ),
/**
* {@code 203 Non-Authoritative Information}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.2.4">HTTP/1.1</a>
*/
NON_AUTHORITATIVE_INFORMATION(203, "Non-Authoritative Information" ),
/**
* {@code 204 No Content}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.2.5">HTTP/1.1</a>
*/
NO_CONTENT(204, "No Content" ),
/**
* {@code 205 Reset Content}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.2.6">HTTP/1.1</a>
*/
RESET_CONTENT(205, "Reset Content" ),
/**
* {@code 206 Partial Content}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.2.7">HTTP/1.1</a>
*/
PARTIAL_CONTENT(206, "Partial Content" ),
/**
* {@code 207 Multi-Status}.
*
* @see <a href="http://tools.ietf.org/html/rfc4918#section-13">WebDAV</a>
*/
MULTI_STATUS(207, "Multi-Status" ),
/**
* {@code 208 Already Reported}.
*
* @see <a href="http://tools.ietf.org/html/rfc5842#section-7.1">WebDAV Binding Extensions</a>
*/
ALREADY_REPORTED(208, "Already Reported" ),
/**
* {@code 226 IM Used}.
*
* @see <a href="http://tools.ietf.org/html/rfc3229#section-10.4.1">Delta encoding in HTTP</a>
*/
IM_USED(226, "IM Used" ),
// 3xx Redirection
/**
* {@code 300 Multiple Choices}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3.1">HTTP/1.1</a>
*/
MULTIPLE_CHOICES(300, "Multiple Choices" ),
/**
* {@code 301 Moved Permanently}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3.2">HTTP/1.1</a>
*/
MOVED_PERMANENTLY(301, "Moved Permanently" ),
/**
* {@code 302 Found}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3.3">HTTP/1.1</a>
*/
FOUND(302, "Found" ),
/**
* {@code 302 Moved Temporarily}.
*
* @see <a href="http://tools.ietf.org/html/rfc1945#section-9.3">HTTP/1.0</a>
* @deprecated In favor of {@link #FOUND} which will be returned from {@code HttpStatus.valueOf(302)}
*/
@Deprecated
MOVED_TEMPORARILY(302, "Moved Temporarily" ),
/**
* {@code 303 See Other}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3.4">HTTP/1.1</a>
*/
SEE_OTHER(303, "See Other" ),
/**
* {@code 304 Not Modified}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3.5">HTTP/1.1</a>
*/
NOT_MODIFIED(304, "Not Modified" ),
/**
* {@code 305 Use Proxy}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3.6">HTTP/1.1</a>
*/
USE_PROXY(305, "Use Proxy" ),
/**
* {@code 307 Temporary Redirect}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3.8">HTTP/1.1</a>
*/
TEMPORARY_REDIRECT(307, "Temporary Redirect" ),
/**
* {@code 308 Resume Incomplete}.
*
* @see <a href="http://code.google.com/p/gears/wiki/ResumableHttpRequestsProposal">A proposal for supporting
* resumable POST/PUT HTTP requests in HTTP/1.0</a>
*/
RESUME_INCOMPLETE(308, "Resume Incomplete" ),
// --- 4xx Client Error ---
/**
* {@code 400 Bad Request}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.1">HTTP/1.1</a>
*/
BAD_REQUEST(400, "Bad Request" ),
/**
* {@code 401 Unauthorized}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.2">HTTP/1.1</a>
*/
UNAUTHORIZED(401, "Unauthorized" ),
/**
* {@code 402 Payment Required}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.3">HTTP/1.1</a>
*/
PAYMENT_REQUIRED(402, "Payment Required" ),
/**
* {@code 403 Forbidden}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.4">HTTP/1.1</a>
*/
FORBIDDEN(403, "Forbidden" ),
/**
* {@code 404 Not Found}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.5">HTTP/1.1</a>
*/
NOT_FOUND(404, "Not Found" ),
/**
* {@code 405 Method Not Allowed}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.6">HTTP/1.1</a>
*/
METHOD_NOT_ALLOWED(405, "Method Not Allowed" ),
/**
* {@code 406 Not Acceptable}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.7">HTTP/1.1</a>
*/
NOT_ACCEPTABLE(406, "Not Acceptable" ),
/**
* {@code 407 Proxy Authentication Required}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.8">HTTP/1.1</a>
*/
PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required" ),
/**
* {@code 408 Request Timeout}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.9">HTTP/1.1</a>
*/
REQUEST_TIMEOUT(408, "Request Timeout" ),
/**
* {@code 409 Conflict}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.10">HTTP/1.1</a>
*/
CONFLICT(409, "Conflict" ),
/**
* {@code 410 Gone}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.11">HTTP/1.1</a>
*/
GONE(410, "Gone" ),
/**
* {@code 411 Length Required}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.12">HTTP/1.1</a>
*/
LENGTH_REQUIRED(411, "Length Required" ),
/**
* {@code 412 Precondition failed}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.13">HTTP/1.1</a>
*/
PRECONDITION_FAILED(412, "Precondition Failed" ),
/**
* {@code 413 Request entity Too Large}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.14">HTTP/1.1</a>
*/
REQUEST_ENTITY_TOO_LARGE(413, "Request entity Too Large" ),
/**
* {@code 414 Request-URI Too Long}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.15">HTTP/1.1</a>
*/
REQUEST_URI_TOO_LONG(414, "Request-URI Too Long" ),
/**
* {@code 415 Unsupported Media Type}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.16">HTTP/1.1</a>
*/
UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type" ),
/**
* {@code 416 Requested Range Not Satisfiable}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.17">HTTP/1.1</a>
*/
REQUESTED_RANGE_NOT_SATISFIABLE(416, "Requested range not satisfiable" ),
/**
* {@code 417 Expectation Failed}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.18">HTTP/1.1</a>
*/
EXPECTATION_FAILED(417, "Expectation Failed" ),
/**
* {@code 418 I'm a teapot}.
*
* @see <a href="http://tools.ietf.org/html/rfc2324#section-2.3.2">HTCPCP/1.0</a>
*/
I_AM_A_TEAPOT(418, "I'm a teapot" ),
/**
* @deprecated See <a href="http://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt">WebDAV Draft Changes</a>
*/
@Deprecated INSUFFICIENT_SPACE_ON_RESOURCE(419, "Insufficient Space On Resource" ),
/**
* @deprecated See <a href="http://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt">WebDAV Draft Changes</a>
*/
@Deprecated METHOD_FAILURE(420, "Method Failure" ),
/**
* @deprecated See <a href="http://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt">WebDAV Draft Changes</a>
*/
@Deprecated DESTINATION_LOCKED(421, "Destination Locked" ),
/**
* {@code 422 Unprocessable entity}.
*
* @see <a href="http://tools.ietf.org/html/rfc4918#section-11.2">WebDAV</a>
*/
UNPROCESSABLE_ENTITY(422, "Unprocessable entity" ),
/**
* {@code 423 Locked}.
*
* @see <a href="http://tools.ietf.org/html/rfc4918#section-11.3">WebDAV</a>
*/
LOCKED(423, "帳號已鎖定" ),
/**
* {@code 424 Failed Dependency}.
*
* @see <a href="http://tools.ietf.org/html/rfc4918#section-11.4">WebDAV</a>
*/
FAILED_DEPENDENCY(424, "Failed Dependency" ),
/**
* {@code 426 Upgrade Required}.
*
* @see <a href="http://tools.ietf.org/html/rfc2817#section-6">Upgrading to TLS Within HTTP/1.1</a>
*/
UPGRADE_REQUIRED(426, "Upgrade Required" ),
/**
* {@code 428 Precondition Required}.
*
* @see <a href="http://tools.ietf.org/html/rfc6585#section-3">Additional HTTP Status Codes</a>
*/
PRECONDITION_REQUIRED(428, "Precondition Required" ),
/**
* {@code 429 Too Many Requests}.
*
* @see <a href="http://tools.ietf.org/html/rfc6585#section-4">Additional HTTP Status Codes</a>
*/
TOO_MANY_REQUESTS(429, "Too Many Requests" ),
/**
* {@code 431 Request Header Fields Too Large}.
*
* @see <a href="http://tools.ietf.org/html/rfc6585#section-5">Additional HTTP Status Codes</a>
*/
REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large" ),
// --- 5xx Server Error ---
/**
* {@code 500 Internal Server Error}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.5.1">HTTP/1.1</a>
*/
INTERNAL_SERVER_ERROR(500, "Internal Server Error" ),
/**
* {@code 501 Not Implemented}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.5.2">HTTP/1.1</a>
*/
NOT_IMPLEMENTED(501, "Not Implemented" ),
/**
* {@code 502 Bad Gateway}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.5.3">HTTP/1.1</a>
*/
BAD_GATEWAY(502, "Bad Gateway" ),
/**
* {@code 503 service Unavailable}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.5.4">HTTP/1.1</a>
*/
SERVICE_UNAVAILABLE(503, "service Unavailable" ),
/**
* {@code 504 Gateway Timeout}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.5.5">HTTP/1.1</a>
*/
GATEWAY_TIMEOUT(504, "Gateway Timeout" ),
/**
* {@code 505 HTTP Version Not Supported}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.5.6">HTTP/1.1</a>
*/
HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version not supported" ),
/**
* {@code 506 Variant Also Negotiates}
*
* @see <a href="http://tools.ietf.org/html/rfc2295#section-8.1">Transparent Content Negotiation</a>
*/
VARIANT_ALSO_NEGOTIATES(506, "Variant Also Negotiates" ),
/**
* {@code 507 Insufficient Storage}
*
* @see <a href="http://tools.ietf.org/html/rfc4918#section-11.5">WebDAV</a>
*/
INSUFFICIENT_STORAGE(507, "Insufficient Storage" ),
/**
* {@code 508 Loop Detected}
*
* @see <a href="http://tools.ietf.org/html/rfc5842#section-7.2">WebDAV Binding Extensions</a>
*/
LOOP_DETECTED(508, "Loop Detected" ),
/**
* {@code 509 Bandwidth Limit Exceeded}
*/
BANDWIDTH_LIMIT_EXCEEDED(509, "Bandwidth Limit Exceeded" ),
/**
* {@code 510 Not Extended}
*
* @see <a href="http://tools.ietf.org/html/rfc2774#section-7">HTTP Extension Framework</a>
*/
NOT_EXTENDED(510, "Not Extended" ),
/**
* {@code 511 Network Authentication Required}.
*
* @see <a href="http://tools.ietf.org/html/rfc6585#section-6">Additional HTTP Status Codes</a>
*/
NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required" ),
SMS_CLIENT_EXCEPTION(512, "輸入手機號不合法,請重新輸入" ),
SMS_SERVER_EXCEPTION(513, "發送短信出現異常,請稍后重試~" );
private final int value;
private final String reasonPhrase;
private HttpStatus(int value, String reasonPhrase) {
this.value = value;
this.reasonPhrase = reasonPhrase;
}
/**
* Return the integer value of this status code.
*/
public int value() {
return this.value;
}
/**
* Return the reason phrase of this status code.
*/
public String getReasonPhrase() {
return reasonPhrase;
}
/**
* Returns the HTTP status series of this status code.
*
* @see Series
*/
public Series series() {
return Series.valueOf(this);
}
/**
* Return a string representation of this status code.
*/
@Override
public String toString() {
return Integer.toString(value);
}
/**
* Return the enum constant of this type with the specified numeric value.
*
* @param statusCode the numeric value of the enum to be returned
* @return the enum constant with the specified numeric value
* @throws IllegalArgumentException if this enum has no constant for the specified numeric value
*/
public static HttpStatus valueOf(int statusCode) {
for (HttpStatus status : values()) {
if (status.value == statusCode) {
return status;
}
}
throw new IllegalArgumentException("No matching constant for [" + statusCode + "]" );
}
/**
* Java 5 enumeration of HTTP status series.
* <p>Retrievable via {@link HttpStatus#series()}.
*/
public static enum Series {
INFORMATIONAL(1),
SUCCESSFUL(2),
REDIRECTION(3),
CLIENT_ERROR(4),
SERVER_ERROR(5);
private final int value;
private Series(int value) {
this.value = value;
}
/**
* Return the integer value of this status series. Ranges from 1 to 5.
*/
public int value() {
return this.value;
}
public static Series valueOf(int status) {
int seriesCode = status / 100;
for (Series series : values()) {
if (series.value == seriesCode) {
return series;
}
}
throw new IllegalArgumentException("No matching constant for [" + status + "]" );
}
public static Series valueOf(HttpStatus status) {
return valueOf(status.value);
}
}
}
Result類
此類是一個通用的返回結果類,起初使用了Swagger文檔標注,后期單獨整合為工具包,為了減少jar包的依賴就刪去了Swagger部分。
package cn.rayfoo.validate.response;
import lombok.Builder;
import lombok.Data;
/**
* @author rayfoo@qq.com
* @date 2020年8月6日
*/
@Data@Builder
public class Result<T> {
/**
* 狀態碼
*/
private Integer code;
/**
* 提示信息
*/
private String msg;
/**
* 數據記錄
*/
private T data;
public Result() {
}
public Result(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Result(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}
exception包
此包內封裝了關於異常的定義和處理類
MyAssert類
從類名就可以看出,這個類是進行斷言判斷的類,其內定義了多個重載的斷言判斷方法,斷言方法可以在特定條件下中止方法的運行。
package cn.rayfoo.validate.exception;
import HttpStatus;
import lombok.extern.slf4j.Slf4j;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>斷言類</p>
* @date 2020/8/7 9:43
*/
@Slf4j
public class MyAssert {
/**
* 如果為空直接拋出異常 類似於斷言的思想
* @param status 當status為false 就會拋出異常 不繼續執行后續語句
* @param msg 異常描述
*/
public static void assertMethod(boolean status, String msg) throws Exception {
//為false拋出異常
if (!status) {
//記錄錯誤信息
log.error(msg);
//拋出異常
throw MyException.builder().code(HttpStatus.INTERNAL_SERVER_ERROR.value()).msg(msg).build();
}
}
/**
* 如果為空直接拋出異常 類似於斷言的思想
* @param status 當status為false 就會拋出異常 不繼續執行后續語句
* @param code 狀態碼
* @param msg 異常描述
*/
public static void assertMethod(boolean status,Integer code, String msg) throws Exception {
//為false拋出異常
if (!status) {
//記錄錯誤信息
log.error(msg);
//拋出異常
throw MyException.builder().code(code).msg(msg).build();
}
}
/**
* 如果為空直接拋出異常 類似於斷言的思想
* @param status 當status為false 就會拋出異常 不繼續執行后續語句
*/
public static void assertMethod(boolean status) throws Exception {
//為false拋出異常
if (!status) {
//記錄錯誤信息
log.error(HttpStatus.INTERNAL_SERVER_ERROR.name());
//拋出異常
throw MyException.builder().code(HttpStatus.INTERNAL_SERVER_ERROR.value()).msg(HttpStatus.INTERNAL_SERVER_ERROR.name()).build();
}
}
}
MyException類
這個類是自定義異常的聲明,由於AOP中只有RuntimeException及其子類的異常可以被全局異常處理器處理,所以其繼承了RuntimeException。拓展了code和msg屬性
package cn.rayfoo.validate.exception;
import lombok.*;
/**
* @Author: rayfoo@qq.com
* @Date: 2020/7/20 9:26 下午
* @Description: 自定義的異常...
*/
@Getter@Setter@Builder@NoArgsConstructor@AllArgsConstructor
public class MyException extends RuntimeException{
private int code;
private String msg;
}
ServerExceptionResolver類
此類用來處理手動拋出的自定義異常。
package cn.rayfoo.validate.exception;
import HttpStatus;
import Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author rayfoo@qq.com
* @version 1.0
* @date 2020/8/5 14:55
* @description 全局異常處理
*/
@ControllerAdvice@Slf4j
public class ServerExceptionResolver {
/**
* 處理自定義的異常
* @param ex
* @return
*/
@ExceptionHandler(MyException.class)@ResponseBody
public Result<String> resolveMyException(MyException ex){
//打印完整的異常信息
ex.printStackTrace();
//創建result
Result<String> result = new Result<>();
//設置result屬性
result.setData(ex.getMsg());
result.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
result.setMsg(ex.getMsg());
//保存自定義異常日志
log.error(ex.getMsg());
return result;
}
}
enums包
很明顯這個包中存放的是一個枚舉,這個枚舉是為了@Verify注解創建的,其作為@Verify的屬性提供正則校驗表。
RegexOption枚舉
如果需要新的校驗條件只需要拓展這個枚舉即可。
package cn.rayfoo.validate.enums;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>參數校驗枚舉</p>
* @date 2020/8/7 15:51
*/
public enum RegexOption {
/**
* 缺省,表示不進行正則校驗
*/
DEFAULT(""),
/**
* 郵箱正則
*/
EMAIL_REGEX("^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$"),
/**
* 手機號正則
*/
PHONE_NUMBER_REGEX("^((13[0-9])|(14[0|5|6|7|9])|(15[0-3])|(15[5-9])|(16[6|7])|(17[2|3|5|6|7|8])|(18[0-9])|(19[1|8|9]))\\d{8}$"),
/**
* 身份證正則
*/
IDENTITY_CARD_REGEX("(^\\d{18}$)|(^\\d{15}$)"),
/**
* URL正則
*/
URL_REGEX("http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?"),
/**
* IP地址正則
*/
IP_ADDR_REGEX("(25[0-5]|2[0-4]\\d|[0-1]\\d{2}|[1-9]?\\d)"),
/**
* 用戶名正則
*/
USERNAME_REGEX("^[a-zA-Z]\\w{5,20}$"),
/**
* 密碼正則
*/
PASSWORD_REGEX("^[a-zA-Z0-9]{6,20}$");
/**
* 正則
*/
private String regex;
/**
* 構造方法
*
* @param regex
*/
private RegexOption(String regex) {
this.regex = regex;
}
public String getRegex() {
return regex;
}
public void setRegex(String regex) {
this.regex = regex;
}
}
annotation包
顯而易見,這個包是用來存放注解的。這四個注解可以完成大部分情況下的參數校驗工作。
@Verify注解
標注在參數、字段上,用於標注需要校驗的數據以及其校驗的規則。
- name:描述,當校驗結果為false時,返回用戶提示信息時使用
- maxLength:最大長度,用於判斷字符串類型,如果時默認值代表不進行判斷
- minLength:最小長度,用於判斷字符串類型,如果時默認值代表不進行判斷
- required:是否為必填屬性
- notNull:是否允許為空
- regular:是否需要進行正則校驗,如果需要正則內容是什么,默認為不進行正則校驗
- isEntity:是否是一個entity,如果是,遞歸調用判斷其內的屬性
package com.github.validate.annotation;
import com.github.validate.enums.RegexOption;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/7 15:33
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
public @interface Verify {
/** 參數名稱 */
String name();
/** 參數最大長度 */
int maxLength() default Integer.MAX_VALUE;
/*** 是否是實體,如果是,繼續判斷其內部的值 */
boolean isEntity() default false;
/** 是否必填 這里只是判斷是否為null */
boolean required() default true;
/** 是否為非空 是否為null和空串都判斷 */
boolean notNull() default true;
/** 最小長度 */
int minLength() default Integer.MIN_VALUE;
/** 正則匹配 */
RegexOption regular() default RegexOption.DEFAULT;
}
@RequestMap注解
用於標注Controller方法的參數,用於指定Map類型參數。
- baseEntityList制定Entity的全類名,如果Entity的屬性名和key一致,就進行參數校驗,校驗規則由Entity中加了@Verify注解的屬性來決定
package cn.rayfoo.validate.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>對Map</p>
* @date 2020/8/8 19:50
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestMap {
/**
* 實體類全類名列表
*/
String[] baseEntityList();
}
@RequestEntity注解
用法同上,不過其標注的是Entity類型。
package cn.rayfoo.validate.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/8 22:43
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestEntity {
String value() default "";
}
@EnableVerify注解
用於標注在SpringBoot啟動類
- execution:指定execution表達式,只有execution標識的方法才會進行全局參數校驗
package cn.rayfoo.validate.annotation;
import EnableVerifyConfig;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/9 13:36
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({EnableVerifyConfig.class})
public @interface EnableVerify {
String execution();
}
Config包
EnableVerifyConfig類
@EnableVerify引用了EnableVerifyConfig類,下面我們介紹一下這個類是如何 編寫的:
這個類被@EnableVerify所引用,當讀取到此注解時,就會自動注冊此類registerBeanDefinitions方法內的代碼主要是進行Bean的注冊(由於默認時沒有掃描cn,rayfoo包的,所以需要注冊到Spring中的Bean要手動的放入Bean容器中),所以我們需要手動的注冊好Aspect類,這里借鑒了通用Mapper的源碼。
package cn.rayfoo.validate.config;
import EnableVerify;
import ValidateAdvice;
import org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/9 13:37
*/
public class EnableVerifyConfig implements ImportBeanDefinitionRegistrar{
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
/*
* 注冊Aspect
*/
//獲取啟動器上的注解
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(EnableVerify.class.getName()));
//獲取自定義注解中的屬性
String execution = annoAttrs.getString("execution");
//創建Bean
BeanDefinition validateAdvice = new GenericBeanDefinition();
//指定Bean的字節碼
validateAdvice.setBeanClassName(AspectJExpressionPointcutAdvisor.class.getName());
MutablePropertyValues validateAdviceProp = validateAdvice.getPropertyValues();
//設置Bean的屬性
validateAdviceProp.addPropertyValue("advice", new ValidateAdvice());
validateAdviceProp.addPropertyValue("expression", execution);
//注冊bean
registry.registerBeanDefinition("validateAdvice",validateAdvice);
/*
* 注冊全局異常處理 如果你的包恰好是cn.rayfoo就需要將其注釋
*/
//創建Bean
// BeanDefinition serverExceptionResolver = new GenericBeanDefinition();
// //指定Bean的字節碼
// serverExceptionResolver.setBeanClassName(ServerExceptionResolver.class.getName());
// //注冊bean
// registry.registerBeanDefinition("serverExceptionResolver",serverExceptionResolver);
}
}
aspect包
ValidateAdvice類
最后就是此篇文章的重中之重了,所有的注解解析和參數校驗都是在這個類中完成的,其主要使用了前置通知,在通知中獲取目標方法,通過反射拿到目標方法的參數,根據方法參數的類型再選擇進行何種判斷。
package com.github.validate.aspect;
import com.github.validate.annotation.RequestEntity;
import com.github.validate.annotation.RequestMap;
import com.github.validate.annotation.Verify;
import com.github.validate.exception.MyAssert;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/9 12:42
*/
public class ValidateAdvice implements MethodInterceptor {
/**
* 校驗的類型
*/
private static final String LINK_HASH_MAP_TYPE = "java.util.LinkedHashMap";
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
//獲取方法
Method method = invocation.getMethod();
//獲取參數上的所有注解
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
//獲取參數列表
Object[] args = invocation.getArguments();
//判斷是否加了RequestMap注解
for (Annotation[] parameterAnnotation : parameterAnnotations) {
//獲取當前參數的位置
int index = indexOf(parameterAnnotations, parameterAnnotation);
for (Annotation annotation : parameterAnnotation) {
//獲取此注解修飾的具體的參數
Object param = args[index];
//如果有@RequestEntity注解
hasRequestEntity(annotation, param);
//如果有Verify注解 由於是參數上的注解 注意:此處傳遞的是具體的param 而非args
hasVerify(annotation, param);
//如果有RequestMap注解 由於是參數上的注解 注意:此處傳遞的是具體的param 而非args
hasRequestMap(annotation, param);
}
}
//執行方法
return invocation.proceed();
}
/**
* 如果參數存在RequestEntity注解
*
* @param annotation 參數上的注解
* @param param 具體的參數
*/
private void hasRequestEntity(Annotation annotation, Object param) throws Exception {
//獲取注解的全類名
String requestEntityName = RequestEntity.class.getName();
//獲取當前注解的全類名
String name = annotation.annotationType().getName();
//匹配是否相同
if (requestEntityName.equals(name)) {
//是否是實體類
isEntity(param);
}
}
/**
* 遞歸判斷是否存在實體屬性
* @param entity 判斷的內容
*/
public void isEntity(Object entity) throws Exception {
//獲取其內部的所有屬性
Field[] fields = entity.getClass().getDeclaredFields();
//遍歷屬性
for (Field field : fields) {
//獲取私有屬性值
field.setAccessible(true);
//需要做校驗的參數
if (field.isAnnotationPresent(Verify.class)) {
//獲取注解對象
Verify verify = field.getAnnotation(Verify.class);
//校驗的對象
Object fieldObj = field.get(entity);
//如果這個屬性是一個實體
if (verify.isEntity()) {
//遞歸調用
isEntity(fieldObj);
}
//校驗
validate(verify, fieldObj);
}
}
}
/**
* 如果參數上加的是Verify注解
*
* @param annotation 參數上的注解
* @param param 參數
*/
private void hasVerify(Annotation annotation, Object param) throws Exception {
//獲取注解的全類名
String verifyName = Verify.class.getName();
//獲取當前注解的全類名
String name = annotation.annotationType().getName();
//匹配是否相同
if (verifyName.equals(name)) {
//獲取此注解修飾的具體的參數
//獲取當前注解的具值
Verify verify = (Verify) annotation;
//進行校驗
validate(verify, param);
}
}
/**
* 判斷是否加了@RequestMap注解 加了再進行下一步的操作
*
* @param annotation 所有參數前的注解
* @param param 當前參數
*/
private void hasRequestMap(Annotation annotation, Object param) throws Exception {
//獲取注解的全類名
String RequestMapName = RequestMap.class.getName();
//獲取當前注解的全類名
String name = annotation.annotationType().getName();
//匹配是否相同
if (RequestMapName.equals(name)) {
//如果存在此注解,執行方法
isLinkedHashMap(annotation, param);
}
}
/**
* 判斷是否為LinkedHashMap,如果是,進行進一步的操作
*
* @param annotation 參數上的注解
* @param param 注解所修飾的參數
*/
private void isLinkedHashMap(Annotation annotation, Object param) throws Exception {
//獲取注解
RequestMap RequestMap = (RequestMap) annotation;
//獲取要校驗的所有entity
String[] entitys = RequestMap.baseEntityList();
//如果是map接收參數
if (LINK_HASH_MAP_TYPE.equals(param.getClass().getName())) {
//如果存在Verify注解
hasVerify(entitys, param);
}
}
/**
* 如果EntityList中的實體存在Verify注解
*
* @param entityList 實體列表
* @param param 加入@RequestMap的注解 的參數
*/
private void hasVerify(String[] entityList, Object param) throws Exception {
//迭代entityList
for (String entity : entityList) {
//獲取所有字段
Field[] fields = Class.forName(entity).getDeclaredFields();
//迭代字段
for (Field field : fields) {
field.setAccessible(true);
//判斷是否加入了Verify注解
if (field.isAnnotationPresent(Verify.class)) {
//如果有 獲取注解的實例
Verify verify = field.getAnnotation(Verify.class);
//校驗
fieldIsNeedValidate(param, verify, field.getName());
}
}
}
}
/**
* 字段是否需要校驗
*
* @param param 增加@RequestMap注解的參數
* @param verify Verify注解的實例
* @param fieldName 加了Verify的屬性name值
*/
private void fieldIsNeedValidate(Object param, Verify verify, String fieldName) throws Exception {
//獲取集合
LinkedHashMap map = (LinkedHashMap) param;
//獲取key列表
Set set = map.keySet();
//迭代key
for (Object key : set) {
//如果key和注解的fieldName一致
if (fieldName.equals(key)) {
//當前值
Object fieldObj = map.get(key);
//真正的進行校驗
validate(verify, fieldObj);
}
}
}
/**
* 正則的校驗方法
*
* @param verify 校驗規則
* @param fieldObj 校驗者
*/
private void validate(Verify verify, Object fieldObj) throws Exception {
//獲取verify的name
String name = verify.name();
//是否時必傳 斷言判斷
if (verify.required()) {
MyAssert.assertMethod(fieldObj != null, String.format("【%s】為必傳參數", name));
}
//字符串的 非空校驗
if (verify.notNull()) {
MyAssert.assertMethod(!(fieldObj == null || "".equals(fieldObj)), String.format("【%s】不能為空", name));
}
//是否有最大長度限制 斷言判斷
int maxLength = verify.maxLength();
if (Integer.MAX_VALUE != maxLength) {
MyAssert.assertMethod(maxLength > String.valueOf(fieldObj).length(), String.format("【%s】長度不合理,最大長度為【%s】", name, maxLength));
}
//是否有最小長度限制 斷言判斷
int minLength = verify.minLength();
if (Integer.MIN_VALUE != minLength) {
MyAssert.assertMethod(minLength < String.valueOf(fieldObj).length(), String.format("【%s】長度不合理,最小長度為【%s】", name, minLength));
}
//是否有正則校驗
if (!"".equals(verify.regular().getRegex())) {
//初始化Pattern
Pattern pattern = Pattern.compile(verify.regular().getRegex());
//斷言判斷正則
MyAssert.assertMethod(pattern.matcher(String.valueOf(fieldObj)).matches(), String.format("參數【%s】的請求數據不符合規則", name));
}
}
/**
* hutool中的方法
*/
private <T> int indexOf(T[] array, Object value) {
if (null != array) {
for (int i = 0; i < array.length; ++i) {
if (equal(value, array[i])) {
return i;
}
}
}
return -1;
}
/**
* hutool中的方法
*/
private boolean equal(Object obj1, Object obj2) {
return Objects.equals(obj1, obj2);
}
}
使用Param-Validate
導入依賴
<dependency>
<groupId>com.github.18338862369</groupId>
<artifactId>param-validate</artifactId>
<version>1.0.0</version>
</dependency>
此依賴中包含了下述依賴,如果是其他SpringBoot版本,建議對依賴進行排除,並重新引入依賴。
本地引入依賴
創建一個文件夾將param-validate-1.0.0.jar放入,創建一個名為install.bat的bat文件,執行文件即可將jar安裝到本地maven倉庫。
mvn install:install-file -Dfile=%~dp0param-validate-1.0.0.jar -DgroupId=com.github.18338862369 -DartifactId=param-validate -Dversion=1.0.0 -Dpackaging=jar
使用
在啟動類上加入@EnableVerify(execution = "execution(* cn.rayfoo.web..*(..))")
注解
package cn.rayfoo;
import EnableVerify;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/9 16:05
*/
@SpringBootApplication
@EnableVerify(execution = "execution(* cn.rayfoo.web..*(..))")
public class Runner {
public static void main(String[] args) {
SpringApplication.run(Runner.class, args);
}
}
在Controller測試
package cn.rayfoo.web;
import Verify;
import RegexOption;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/9 16:12
*/
@RestController
public class TestController {
@GetMapping("/test")
public void test(@Verify(name = "用戶名",regular = RegexOption.USERNAME_REGEX)String username){
System.out.println("validate");
}
}
使用postMan測試
如果成功進行了參數校驗,表示jar包執行正常
下載地址:https://github.com/18338862369/param-validate/releases/tag/1.0.0.RELEASE
如果對你有用,歡迎star支持一下~# param-validate
在開發過程中,用戶傳遞的數據不一定合法,雖然可以通過前端進行一些校驗,但是為了確保程序的安全性,保證數據的合法,在后台進行數據校驗也是十分必要的。
后台的參數校驗的解決方案
在controller方法中校驗:
后台的參數是通過controller方法獲取的,所以最簡單的參數校驗的方法,就是在controller方法中進行參數校驗。在controller方法中如果進行參數校驗會有大量重復、沒有太大意義的代碼。
使用攔截器、過濾器校驗
為了保證controller中的代碼有更好的可讀性,可以將參數校驗的工作交由攔截器(Interceptor)或者過濾器(Filter)來完成,但是此時又存在一個問題:非共性的參數需要每個方法都創建一個與之對應的攔截器(或者過濾器)。
實現對Entity的統一校驗
鑒於上述解決方案的缺點,我們可以借助AOP的思想來進行統一的參數校驗。思想是通過自定義注解來完成對實體類屬性的標注,在AOP中掃描加了自定義注解的屬性,對其進行注解屬性標注的校驗。對於不滿足的參數直接拋出自定義異常,交由全局異常處理來處理並返回友好的提示信息。
認識Param-Validate
以上的解決方案各有優劣,param-validate采用了AOP+注解的形式解決了統一參數的校驗。使用四個注解可以解決大部分的參數校驗,有效的簡化了代碼、提高了程序的可讀性和開發效率。
Param-Validate基於SpringBoot2.3.0.RELEASE,所以請保證你的SpringBoot版本和其一致,如果必須使用其他版本,可以將其內包含的SpringBoot依賴排除。
依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.18338862369</groupId>
<artifactId>param-validate</artifactId>
<version>1.0.0</version>
<description>SpringBoot ParamValidate</description>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<aspectjweaver.version>1.9.5</aspectjweaver.version>
<lombok.version>1.18.12</lombok.version>
</properties>
<dependencies>
<!--web內置了jackson-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- aspect -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectjweaver.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- <optional>true</optional>-->
<version>${lombok.version}</version>
</dependency>
</dependencies>
</project>
項目結構
下圖是項目的結構圖。接下來,我們逐個包來介紹一下
response包
此包中定義的內容為用戶相應的類
HttpStatus枚舉
這個枚舉是從Spring源碼中拿到,並且拓展的一個枚舉。其封裝了很多常見的狀態碼和狀態信息。
package cn.rayfoo.validate.response;
/**
* @author rayfoo@qq.com
* @version 1.0
* @date 2020/8/5 11:37
* @description
*/
public enum HttpStatus {
/**
* 登錄成功
* {@code 600 Login Success}.
*/
LOGIN_SUCCESS(600, "Login Success" ),
/**
* 重新登錄
* {@code 605 Login Success}.
*/
LOGIN_AGAIN(605, "Login AGAIN" ),
/**
* 登錄超時
* {@code 601 Login Out Time}.
*/
LOGIN_OUT_TIME(601, "Login Out Time" ),
/**
* 用戶無訪問權限
* {@code 602 Not Roles}.
*/
NOT_ROLES(602, "Not Roles" ),
/**
* 用戶未注冊
* {@code 603 Not Register}.
*/
NOT_REGISTER(603, "Not Register未找到該賬號" ),
/**
* 用戶未注冊
* {@code 604 AuthenticationExcption}.
*/
AUTHENTICATION_EXCPTION(604, "身份認證錯誤" ),
/**
* 未知的賬號異常
* {@code 606 Unknown Account}.
*/
UNKNOWN_ACCOUNT(606, "賬號已注銷" ),
/**
* 請求中的參數有誤
* {@code 705 Parameter Error}.
*/
PARAMETER_ERROR(705, "Parameter Error" ),
/**
* 驗證碼錯誤
* {@code 704 Invalid Captcha}.
*/
INVALID_CAPTCHA(704, "Invalid Captcha驗證碼錯誤" ),
/**
* 郵箱和手機號驗證錯誤
*/
EMAIL_OR_PHONE_ERROR(703, "email or phone error" ),
/**
* 用戶已激活
*/
HAS_BEEN_ACTIVATED(702, "has been activated" ),
/**
* 用戶名或密碼錯誤
*/
USERNAME_OR_PASSWORD_ERROR(700, "username or password error用戶名或密碼錯誤" ),
/**
* 用戶未啟用
*/
USER_NOT_ENABLED(699, "user not enabled" ),
/**
* 驗證碼錯誤
*/
ACTIVATION_CODE_ERROR(698, "Activation code error" ),
/**
* 用戶名被占用
*/
USERNAME_IS_OCCUPIED(697, "Username is occupied" ),
/**
* 返回無數據
* {@code 706 not data}.
*/
NOT_DATA(706, "not data" ),
/**
* 流程操作成功
* {@code 710 Successful operation}.
*/
SUCCESSFUL_OPERATION(710, "Successful operation" ),
/**
* 數據沖突,無法存儲!
*/
DATA_CONFLICT(711, "Data conflict" ),
/**
* 刪除操作失敗
*/
CANNOT_DELETE(712, "Cannot delete" ),
/**
* 操作失敗
* {@code 720 operation failed}.
*/
FAILED_OPERATION(720, "operation failed" ),
NOT_VTASKSTATUS(709, "not vtaskstatus" ),
// 1xx Informational
/**
* {@code 100 Continue}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.1.1">HTTP/1.1</a>
*/
CONTINUE(100, "Continue" ),
/**
* {@code 101 Switching Protocols}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.1.2">HTTP/1.1</a>
*/
SWITCHING_PROTOCOLS(101, "Switching Protocols" ),
/**
* {@code 102 Processing}.
*
* @see <a href="http://tools.ietf.org/html/rfc2518#section-10.1">WebDAV</a>
*/
PROCESSING(102, "Processing" ),
/**
* {@code 103 Checkpoint}.
*
* @see <a href="http://code.google.com/p/gears/wiki/ResumableHttpRequestsProposal">A proposal for supporting
* resumable POST/PUT HTTP requests in HTTP/1.0</a>
*/
CHECKPOINT(103, "Checkpoint" ),
// 2xx Success
/**
* {@code 200 OK}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.2.1">HTTP/1.1</a>
*/
OK(200, "OK" ),
/**
* {@code 201 Created}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.2.2">HTTP/1.1</a>
*/
CREATED(201, "Created" ),
/**
* {@code 202 Accepted}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.2.3">HTTP/1.1</a>
*/
ACCEPTED(202, "Accepted" ),
/**
* {@code 203 Non-Authoritative Information}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.2.4">HTTP/1.1</a>
*/
NON_AUTHORITATIVE_INFORMATION(203, "Non-Authoritative Information" ),
/**
* {@code 204 No Content}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.2.5">HTTP/1.1</a>
*/
NO_CONTENT(204, "No Content" ),
/**
* {@code 205 Reset Content}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.2.6">HTTP/1.1</a>
*/
RESET_CONTENT(205, "Reset Content" ),
/**
* {@code 206 Partial Content}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.2.7">HTTP/1.1</a>
*/
PARTIAL_CONTENT(206, "Partial Content" ),
/**
* {@code 207 Multi-Status}.
*
* @see <a href="http://tools.ietf.org/html/rfc4918#section-13">WebDAV</a>
*/
MULTI_STATUS(207, "Multi-Status" ),
/**
* {@code 208 Already Reported}.
*
* @see <a href="http://tools.ietf.org/html/rfc5842#section-7.1">WebDAV Binding Extensions</a>
*/
ALREADY_REPORTED(208, "Already Reported" ),
/**
* {@code 226 IM Used}.
*
* @see <a href="http://tools.ietf.org/html/rfc3229#section-10.4.1">Delta encoding in HTTP</a>
*/
IM_USED(226, "IM Used" ),
// 3xx Redirection
/**
* {@code 300 Multiple Choices}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3.1">HTTP/1.1</a>
*/
MULTIPLE_CHOICES(300, "Multiple Choices" ),
/**
* {@code 301 Moved Permanently}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3.2">HTTP/1.1</a>
*/
MOVED_PERMANENTLY(301, "Moved Permanently" ),
/**
* {@code 302 Found}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3.3">HTTP/1.1</a>
*/
FOUND(302, "Found" ),
/**
* {@code 302 Moved Temporarily}.
*
* @see <a href="http://tools.ietf.org/html/rfc1945#section-9.3">HTTP/1.0</a>
* @deprecated In favor of {@link #FOUND} which will be returned from {@code HttpStatus.valueOf(302)}
*/
@Deprecated
MOVED_TEMPORARILY(302, "Moved Temporarily" ),
/**
* {@code 303 See Other}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3.4">HTTP/1.1</a>
*/
SEE_OTHER(303, "See Other" ),
/**
* {@code 304 Not Modified}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3.5">HTTP/1.1</a>
*/
NOT_MODIFIED(304, "Not Modified" ),
/**
* {@code 305 Use Proxy}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3.6">HTTP/1.1</a>
*/
USE_PROXY(305, "Use Proxy" ),
/**
* {@code 307 Temporary Redirect}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3.8">HTTP/1.1</a>
*/
TEMPORARY_REDIRECT(307, "Temporary Redirect" ),
/**
* {@code 308 Resume Incomplete}.
*
* @see <a href="http://code.google.com/p/gears/wiki/ResumableHttpRequestsProposal">A proposal for supporting
* resumable POST/PUT HTTP requests in HTTP/1.0</a>
*/
RESUME_INCOMPLETE(308, "Resume Incomplete" ),
// --- 4xx Client Error ---
/**
* {@code 400 Bad Request}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.1">HTTP/1.1</a>
*/
BAD_REQUEST(400, "Bad Request" ),
/**
* {@code 401 Unauthorized}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.2">HTTP/1.1</a>
*/
UNAUTHORIZED(401, "Unauthorized" ),
/**
* {@code 402 Payment Required}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.3">HTTP/1.1</a>
*/
PAYMENT_REQUIRED(402, "Payment Required" ),
/**
* {@code 403 Forbidden}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.4">HTTP/1.1</a>
*/
FORBIDDEN(403, "Forbidden" ),
/**
* {@code 404 Not Found}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.5">HTTP/1.1</a>
*/
NOT_FOUND(404, "Not Found" ),
/**
* {@code 405 Method Not Allowed}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.6">HTTP/1.1</a>
*/
METHOD_NOT_ALLOWED(405, "Method Not Allowed" ),
/**
* {@code 406 Not Acceptable}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.7">HTTP/1.1</a>
*/
NOT_ACCEPTABLE(406, "Not Acceptable" ),
/**
* {@code 407 Proxy Authentication Required}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.8">HTTP/1.1</a>
*/
PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required" ),
/**
* {@code 408 Request Timeout}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.9">HTTP/1.1</a>
*/
REQUEST_TIMEOUT(408, "Request Timeout" ),
/**
* {@code 409 Conflict}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.10">HTTP/1.1</a>
*/
CONFLICT(409, "Conflict" ),
/**
* {@code 410 Gone}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.11">HTTP/1.1</a>
*/
GONE(410, "Gone" ),
/**
* {@code 411 Length Required}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.12">HTTP/1.1</a>
*/
LENGTH_REQUIRED(411, "Length Required" ),
/**
* {@code 412 Precondition failed}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.13">HTTP/1.1</a>
*/
PRECONDITION_FAILED(412, "Precondition Failed" ),
/**
* {@code 413 Request entity Too Large}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.14">HTTP/1.1</a>
*/
REQUEST_ENTITY_TOO_LARGE(413, "Request entity Too Large" ),
/**
* {@code 414 Request-URI Too Long}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.15">HTTP/1.1</a>
*/
REQUEST_URI_TOO_LONG(414, "Request-URI Too Long" ),
/**
* {@code 415 Unsupported Media Type}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.16">HTTP/1.1</a>
*/
UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type" ),
/**
* {@code 416 Requested Range Not Satisfiable}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.17">HTTP/1.1</a>
*/
REQUESTED_RANGE_NOT_SATISFIABLE(416, "Requested range not satisfiable" ),
/**
* {@code 417 Expectation Failed}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.4.18">HTTP/1.1</a>
*/
EXPECTATION_FAILED(417, "Expectation Failed" ),
/**
* {@code 418 I'm a teapot}.
*
* @see <a href="http://tools.ietf.org/html/rfc2324#section-2.3.2">HTCPCP/1.0</a>
*/
I_AM_A_TEAPOT(418, "I'm a teapot" ),
/**
* @deprecated See <a href="http://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt">WebDAV Draft Changes</a>
*/
@Deprecated INSUFFICIENT_SPACE_ON_RESOURCE(419, "Insufficient Space On Resource" ),
/**
* @deprecated See <a href="http://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt">WebDAV Draft Changes</a>
*/
@Deprecated METHOD_FAILURE(420, "Method Failure" ),
/**
* @deprecated See <a href="http://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt">WebDAV Draft Changes</a>
*/
@Deprecated DESTINATION_LOCKED(421, "Destination Locked" ),
/**
* {@code 422 Unprocessable entity}.
*
* @see <a href="http://tools.ietf.org/html/rfc4918#section-11.2">WebDAV</a>
*/
UNPROCESSABLE_ENTITY(422, "Unprocessable entity" ),
/**
* {@code 423 Locked}.
*
* @see <a href="http://tools.ietf.org/html/rfc4918#section-11.3">WebDAV</a>
*/
LOCKED(423, "帳號已鎖定" ),
/**
* {@code 424 Failed Dependency}.
*
* @see <a href="http://tools.ietf.org/html/rfc4918#section-11.4">WebDAV</a>
*/
FAILED_DEPENDENCY(424, "Failed Dependency" ),
/**
* {@code 426 Upgrade Required}.
*
* @see <a href="http://tools.ietf.org/html/rfc2817#section-6">Upgrading to TLS Within HTTP/1.1</a>
*/
UPGRADE_REQUIRED(426, "Upgrade Required" ),
/**
* {@code 428 Precondition Required}.
*
* @see <a href="http://tools.ietf.org/html/rfc6585#section-3">Additional HTTP Status Codes</a>
*/
PRECONDITION_REQUIRED(428, "Precondition Required" ),
/**
* {@code 429 Too Many Requests}.
*
* @see <a href="http://tools.ietf.org/html/rfc6585#section-4">Additional HTTP Status Codes</a>
*/
TOO_MANY_REQUESTS(429, "Too Many Requests" ),
/**
* {@code 431 Request Header Fields Too Large}.
*
* @see <a href="http://tools.ietf.org/html/rfc6585#section-5">Additional HTTP Status Codes</a>
*/
REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large" ),
// --- 5xx Server Error ---
/**
* {@code 500 Internal Server Error}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.5.1">HTTP/1.1</a>
*/
INTERNAL_SERVER_ERROR(500, "Internal Server Error" ),
/**
* {@code 501 Not Implemented}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.5.2">HTTP/1.1</a>
*/
NOT_IMPLEMENTED(501, "Not Implemented" ),
/**
* {@code 502 Bad Gateway}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.5.3">HTTP/1.1</a>
*/
BAD_GATEWAY(502, "Bad Gateway" ),
/**
* {@code 503 service Unavailable}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.5.4">HTTP/1.1</a>
*/
SERVICE_UNAVAILABLE(503, "service Unavailable" ),
/**
* {@code 504 Gateway Timeout}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.5.5">HTTP/1.1</a>
*/
GATEWAY_TIMEOUT(504, "Gateway Timeout" ),
/**
* {@code 505 HTTP Version Not Supported}.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.5.6">HTTP/1.1</a>
*/
HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version not supported" ),
/**
* {@code 506 Variant Also Negotiates}
*
* @see <a href="http://tools.ietf.org/html/rfc2295#section-8.1">Transparent Content Negotiation</a>
*/
VARIANT_ALSO_NEGOTIATES(506, "Variant Also Negotiates" ),
/**
* {@code 507 Insufficient Storage}
*
* @see <a href="http://tools.ietf.org/html/rfc4918#section-11.5">WebDAV</a>
*/
INSUFFICIENT_STORAGE(507, "Insufficient Storage" ),
/**
* {@code 508 Loop Detected}
*
* @see <a href="http://tools.ietf.org/html/rfc5842#section-7.2">WebDAV Binding Extensions</a>
*/
LOOP_DETECTED(508, "Loop Detected" ),
/**
* {@code 509 Bandwidth Limit Exceeded}
*/
BANDWIDTH_LIMIT_EXCEEDED(509, "Bandwidth Limit Exceeded" ),
/**
* {@code 510 Not Extended}
*
* @see <a href="http://tools.ietf.org/html/rfc2774#section-7">HTTP Extension Framework</a>
*/
NOT_EXTENDED(510, "Not Extended" ),
/**
* {@code 511 Network Authentication Required}.
*
* @see <a href="http://tools.ietf.org/html/rfc6585#section-6">Additional HTTP Status Codes</a>
*/
NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required" ),
SMS_CLIENT_EXCEPTION(512, "輸入手機號不合法,請重新輸入" ),
SMS_SERVER_EXCEPTION(513, "發送短信出現異常,請稍后重試~" );
private final int value;
private final String reasonPhrase;
private HttpStatus(int value, String reasonPhrase) {
this.value = value;
this.reasonPhrase = reasonPhrase;
}
/**
* Return the integer value of this status code.
*/
public int value() {
return this.value;
}
/**
* Return the reason phrase of this status code.
*/
public String getReasonPhrase() {
return reasonPhrase;
}
/**
* Returns the HTTP status series of this status code.
*
* @see Series
*/
public Series series() {
return Series.valueOf(this);
}
/**
* Return a string representation of this status code.
*/
@Override
public String toString() {
return Integer.toString(value);
}
/**
* Return the enum constant of this type with the specified numeric value.
*
* @param statusCode the numeric value of the enum to be returned
* @return the enum constant with the specified numeric value
* @throws IllegalArgumentException if this enum has no constant for the specified numeric value
*/
public static HttpStatus valueOf(int statusCode) {
for (HttpStatus status : values()) {
if (status.value == statusCode) {
return status;
}
}
throw new IllegalArgumentException("No matching constant for [" + statusCode + "]" );
}
/**
* Java 5 enumeration of HTTP status series.
* <p>Retrievable via {@link HttpStatus#series()}.
*/
public static enum Series {
INFORMATIONAL(1),
SUCCESSFUL(2),
REDIRECTION(3),
CLIENT_ERROR(4),
SERVER_ERROR(5);
private final int value;
private Series(int value) {
this.value = value;
}
/**
* Return the integer value of this status series. Ranges from 1 to 5.
*/
public int value() {
return this.value;
}
public static Series valueOf(int status) {
int seriesCode = status / 100;
for (Series series : values()) {
if (series.value == seriesCode) {
return series;
}
}
throw new IllegalArgumentException("No matching constant for [" + status + "]" );
}
public static Series valueOf(HttpStatus status) {
return valueOf(status.value);
}
}
}
Result類
此類是一個通用的返回結果類,起初使用了Swagger文檔標注,后期單獨整合為工具包,為了減少jar包的依賴就刪去了Swagger部分。
package cn.rayfoo.validate.response;
import lombok.Builder;
import lombok.Data;
/**
* @author rayfoo@qq.com
* @date 2020年8月6日
*/
@Data@Builder
public class Result<T> {
/**
* 狀態碼
*/
private Integer code;
/**
* 提示信息
*/
private String msg;
/**
* 數據記錄
*/
private T data;
public Result() {
}
public Result(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Result(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}
exception包
此包內封裝了關於異常的定義和處理類
MyAssert類
從類名就可以看出,這個類是進行斷言判斷的類,其內定義了多個重載的斷言判斷方法,斷言方法可以在特定條件下中止方法的運行。
package cn.rayfoo.validate.exception;
import HttpStatus;
import lombok.extern.slf4j.Slf4j;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>斷言類</p>
* @date 2020/8/7 9:43
*/
@Slf4j
public class MyAssert {
/**
* 如果為空直接拋出異常 類似於斷言的思想
* @param status 當status為false 就會拋出異常 不繼續執行后續語句
* @param msg 異常描述
*/
public static void assertMethod(boolean status, String msg) throws Exception {
//為false拋出異常
if (!status) {
//記錄錯誤信息
log.error(msg);
//拋出異常
throw MyException.builder().code(HttpStatus.INTERNAL_SERVER_ERROR.value()).msg(msg).build();
}
}
/**
* 如果為空直接拋出異常 類似於斷言的思想
* @param status 當status為false 就會拋出異常 不繼續執行后續語句
* @param code 狀態碼
* @param msg 異常描述
*/
public static void assertMethod(boolean status,Integer code, String msg) throws Exception {
//為false拋出異常
if (!status) {
//記錄錯誤信息
log.error(msg);
//拋出異常
throw MyException.builder().code(code).msg(msg).build();
}
}
/**
* 如果為空直接拋出異常 類似於斷言的思想
* @param status 當status為false 就會拋出異常 不繼續執行后續語句
*/
public static void assertMethod(boolean status) throws Exception {
//為false拋出異常
if (!status) {
//記錄錯誤信息
log.error(HttpStatus.INTERNAL_SERVER_ERROR.name());
//拋出異常
throw MyException.builder().code(HttpStatus.INTERNAL_SERVER_ERROR.value()).msg(HttpStatus.INTERNAL_SERVER_ERROR.name()).build();
}
}
}
MyException類
這個類是自定義異常的聲明,由於AOP中只有RuntimeException及其子類的異常可以被全局異常處理器處理,所以其繼承了RuntimeException。拓展了code和msg屬性
package cn.rayfoo.validate.exception;
import lombok.*;
/**
* @Author: rayfoo@qq.com
* @Date: 2020/7/20 9:26 下午
* @Description: 自定義的異常...
*/
@Getter@Setter@Builder@NoArgsConstructor@AllArgsConstructor
public class MyException extends RuntimeException{
private int code;
private String msg;
}
ServerExceptionResolver類
此類用來處理手動拋出的自定義異常。
package cn.rayfoo.validate.exception;
import HttpStatus;
import Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author rayfoo@qq.com
* @version 1.0
* @date 2020/8/5 14:55
* @description 全局異常處理
*/
@ControllerAdvice@Slf4j
public class ServerExceptionResolver {
/**
* 處理自定義的異常
* @param ex
* @return
*/
@ExceptionHandler(MyException.class)@ResponseBody
public Result<String> resolveMyException(MyException ex){
//打印完整的異常信息
ex.printStackTrace();
//創建result
Result<String> result = new Result<>();
//設置result屬性
result.setData(ex.getMsg());
result.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
result.setMsg(ex.getMsg());
//保存自定義異常日志
log.error(ex.getMsg());
return result;
}
}
enums包
很明顯這個包中存放的是一個枚舉,這個枚舉是為了@Verify注解創建的,其作為@Verify的屬性提供正則校驗表。
RegexOption枚舉
如果需要新的校驗條件只需要拓展這個枚舉即可。
package cn.rayfoo.validate.enums;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>參數校驗枚舉</p>
* @date 2020/8/7 15:51
*/
public enum RegexOption {
/**
* 缺省,表示不進行正則校驗
*/
DEFAULT(""),
/**
* 郵箱正則
*/
EMAIL_REGEX("^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$"),
/**
* 手機號正則
*/
PHONE_NUMBER_REGEX("^((13[0-9])|(14[0|5|6|7|9])|(15[0-3])|(15[5-9])|(16[6|7])|(17[2|3|5|6|7|8])|(18[0-9])|(19[1|8|9]))\\d{8}$"),
/**
* 身份證正則
*/
IDENTITY_CARD_REGEX("(^\\d{18}$)|(^\\d{15}$)"),
/**
* URL正則
*/
URL_REGEX("http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?"),
/**
* IP地址正則
*/
IP_ADDR_REGEX("(25[0-5]|2[0-4]\\d|[0-1]\\d{2}|[1-9]?\\d)"),
/**
* 用戶名正則
*/
USERNAME_REGEX("^[a-zA-Z]\\w{5,20}$"),
/**
* 密碼正則
*/
PASSWORD_REGEX("^[a-zA-Z0-9]{6,20}$");
/**
* 正則
*/
private String regex;
/**
* 構造方法
*
* @param regex
*/
private RegexOption(String regex) {
this.regex = regex;
}
public String getRegex() {
return regex;
}
public void setRegex(String regex) {
this.regex = regex;
}
}
annotation包
顯而易見,這個包是用來存放注解的。這四個注解可以完成大部分情況下的參數校驗工作。
@Verify注解
標注在參數、字段上,用於標注需要校驗的數據以及其校驗的規則。
- name:描述,當校驗結果為false時,返回用戶提示信息時使用
- maxLength:最大長度,用於判斷字符串類型,如果時默認值代表不進行判斷
- minLength:最小長度,用於判斷字符串類型,如果時默認值代表不進行判斷
- required:是否為必填屬性
- notNull:是否允許為空
- regular:是否需要進行正則校驗,如果需要正則內容是什么,默認為不進行正則校驗
- isEntity:是否是一個entity,如果是,遞歸調用判斷其內的屬性
package com.github.validate.annotation;
import com.github.validate.enums.RegexOption;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/7 15:33
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
public @interface Verify {
/** 參數名稱 */
String name();
/** 參數最大長度 */
int maxLength() default Integer.MAX_VALUE;
/*** 是否是實體,如果是,繼續判斷其內部的值 */
boolean isEntity() default false;
/** 是否必填 這里只是判斷是否為null */
boolean required() default true;
/** 是否為非空 是否為null和空串都判斷 */
boolean notNull() default true;
/** 最小長度 */
int minLength() default Integer.MIN_VALUE;
/** 正則匹配 */
RegexOption regular() default RegexOption.DEFAULT;
}
@RequestMap注解
用於標注Controller方法的參數,用於指定Map類型參數。
- baseEntityList制定Entity的全類名,如果Entity的屬性名和key一致,就進行參數校驗,校驗規則由Entity中加了@Verify注解的屬性來決定
package cn.rayfoo.validate.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p>對Map</p>
* @date 2020/8/8 19:50
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestMap {
/**
* 實體類全類名列表
*/
String[] baseEntityList();
}
@RequestEntity注解
用法同上,不過其標注的是Entity類型。
package cn.rayfoo.validate.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/8 22:43
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestEntity {
String value() default "";
}
@EnableVerify注解
用於標注在SpringBoot啟動類
- execution:指定execution表達式,只有execution標識的方法才會進行全局參數校驗
package cn.rayfoo.validate.annotation;
import EnableVerifyConfig;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/9 13:36
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({EnableVerifyConfig.class})
public @interface EnableVerify {
String execution();
}
Config包
EnableVerifyConfig類
@EnableVerify引用了EnableVerifyConfig類,下面我們介紹一下這個類是如何 編寫的:
這個類被@EnableVerify所引用,當讀取到此注解時,就會自動注冊此類registerBeanDefinitions方法內的代碼主要是進行Bean的注冊(由於默認時沒有掃描cn,rayfoo包的,所以需要注冊到Spring中的Bean要手動的放入Bean容器中),所以我們需要手動的注冊好Aspect類,這里借鑒了通用Mapper的源碼。
package cn.rayfoo.validate.config;
import EnableVerify;
import ValidateAdvice;
import org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/9 13:37
*/
public class EnableVerifyConfig implements ImportBeanDefinitionRegistrar{
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
/*
* 注冊Aspect
*/
//獲取啟動器上的注解
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(EnableVerify.class.getName()));
//獲取自定義注解中的屬性
String execution = annoAttrs.getString("execution");
//創建Bean
BeanDefinition validateAdvice = new GenericBeanDefinition();
//指定Bean的字節碼
validateAdvice.setBeanClassName(AspectJExpressionPointcutAdvisor.class.getName());
MutablePropertyValues validateAdviceProp = validateAdvice.getPropertyValues();
//設置Bean的屬性
validateAdviceProp.addPropertyValue("advice", new ValidateAdvice());
validateAdviceProp.addPropertyValue("expression", execution);
//注冊bean
registry.registerBeanDefinition("validateAdvice",validateAdvice);
/*
* 注冊全局異常處理 如果你的包恰好是cn.rayfoo就需要將其注釋
*/
//創建Bean
// BeanDefinition serverExceptionResolver = new GenericBeanDefinition();
// //指定Bean的字節碼
// serverExceptionResolver.setBeanClassName(ServerExceptionResolver.class.getName());
// //注冊bean
// registry.registerBeanDefinition("serverExceptionResolver",serverExceptionResolver);
}
}
aspect包
ValidateAdvice類
最后就是此篇文章的重中之重了,所有的注解解析和參數校驗都是在這個類中完成的,其主要使用了前置通知,在通知中獲取目標方法,通過反射拿到目標方法的參數,根據方法參數的類型再選擇進行何種判斷。
package com.github.validate.aspect;
import com.github.validate.annotation.RequestEntity;
import com.github.validate.annotation.RequestMap;
import com.github.validate.annotation.Verify;
import com.github.validate.exception.MyAssert;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/9 12:42
*/
public class ValidateAdvice implements MethodInterceptor {
/**
* 校驗的類型
*/
private static final String LINK_HASH_MAP_TYPE = "java.util.LinkedHashMap";
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
//獲取方法
Method method = invocation.getMethod();
//獲取參數上的所有注解
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
//獲取參數列表
Object[] args = invocation.getArguments();
//判斷是否加了RequestMap注解
for (Annotation[] parameterAnnotation : parameterAnnotations) {
//獲取當前參數的位置
int index = indexOf(parameterAnnotations, parameterAnnotation);
for (Annotation annotation : parameterAnnotation) {
//獲取此注解修飾的具體的參數
Object param = args[index];
//如果有@RequestEntity注解
hasRequestEntity(annotation, param);
//如果有Verify注解 由於是參數上的注解 注意:此處傳遞的是具體的param 而非args
hasVerify(annotation, param);
//如果有RequestMap注解 由於是參數上的注解 注意:此處傳遞的是具體的param 而非args
hasRequestMap(annotation, param);
}
}
//執行方法
return invocation.proceed();
}
/**
* 如果參數存在RequestEntity注解
*
* @param annotation 參數上的注解
* @param param 具體的參數
*/
private void hasRequestEntity(Annotation annotation, Object param) throws Exception {
//獲取注解的全類名
String requestEntityName = RequestEntity.class.getName();
//獲取當前注解的全類名
String name = annotation.annotationType().getName();
//匹配是否相同
if (requestEntityName.equals(name)) {
//是否是實體類
isEntity(param);
}
}
/**
* 遞歸判斷是否存在實體屬性
* @param entity 判斷的內容
*/
public void isEntity(Object entity) throws Exception {
//獲取其內部的所有屬性
Field[] fields = entity.getClass().getDeclaredFields();
//遍歷屬性
for (Field field : fields) {
//獲取私有屬性值
field.setAccessible(true);
//需要做校驗的參數
if (field.isAnnotationPresent(Verify.class)) {
//獲取注解對象
Verify verify = field.getAnnotation(Verify.class);
//校驗的對象
Object fieldObj = field.get(entity);
//如果這個屬性是一個實體
if (verify.isEntity()) {
//遞歸調用
isEntity(fieldObj);
}
//校驗
validate(verify, fieldObj);
}
}
}
/**
* 如果參數上加的是Verify注解
*
* @param annotation 參數上的注解
* @param param 參數
*/
private void hasVerify(Annotation annotation, Object param) throws Exception {
//獲取注解的全類名
String verifyName = Verify.class.getName();
//獲取當前注解的全類名
String name = annotation.annotationType().getName();
//匹配是否相同
if (verifyName.equals(name)) {
//獲取此注解修飾的具體的參數
//獲取當前注解的具值
Verify verify = (Verify) annotation;
//進行校驗
validate(verify, param);
}
}
/**
* 判斷是否加了@RequestMap注解 加了再進行下一步的操作
*
* @param annotation 所有參數前的注解
* @param param 當前參數
*/
private void hasRequestMap(Annotation annotation, Object param) throws Exception {
//獲取注解的全類名
String RequestMapName = RequestMap.class.getName();
//獲取當前注解的全類名
String name = annotation.annotationType().getName();
//匹配是否相同
if (RequestMapName.equals(name)) {
//如果存在此注解,執行方法
isLinkedHashMap(annotation, param);
}
}
/**
* 判斷是否為LinkedHashMap,如果是,進行進一步的操作
*
* @param annotation 參數上的注解
* @param param 注解所修飾的參數
*/
private void isLinkedHashMap(Annotation annotation, Object param) throws Exception {
//獲取注解
RequestMap RequestMap = (RequestMap) annotation;
//獲取要校驗的所有entity
String[] entitys = RequestMap.baseEntityList();
//如果是map接收參數
if (LINK_HASH_MAP_TYPE.equals(param.getClass().getName())) {
//如果存在Verify注解
hasVerify(entitys, param);
}
}
/**
* 如果EntityList中的實體存在Verify注解
*
* @param entityList 實體列表
* @param param 加入@RequestMap的注解 的參數
*/
private void hasVerify(String[] entityList, Object param) throws Exception {
//迭代entityList
for (String entity : entityList) {
//獲取所有字段
Field[] fields = Class.forName(entity).getDeclaredFields();
//迭代字段
for (Field field : fields) {
field.setAccessible(true);
//判斷是否加入了Verify注解
if (field.isAnnotationPresent(Verify.class)) {
//如果有 獲取注解的實例
Verify verify = field.getAnnotation(Verify.class);
//校驗
fieldIsNeedValidate(param, verify, field.getName());
}
}
}
}
/**
* 字段是否需要校驗
*
* @param param 增加@RequestMap注解的參數
* @param verify Verify注解的實例
* @param fieldName 加了Verify的屬性name值
*/
private void fieldIsNeedValidate(Object param, Verify verify, String fieldName) throws Exception {
//獲取集合
LinkedHashMap map = (LinkedHashMap) param;
//獲取key列表
Set set = map.keySet();
//迭代key
for (Object key : set) {
//如果key和注解的fieldName一致
if (fieldName.equals(key)) {
//當前值
Object fieldObj = map.get(key);
//真正的進行校驗
validate(verify, fieldObj);
}
}
}
/**
* 正則的校驗方法
*
* @param verify 校驗規則
* @param fieldObj 校驗者
*/
private void validate(Verify verify, Object fieldObj) throws Exception {
//獲取verify的name
String name = verify.name();
//是否時必傳 斷言判斷
if (verify.required()) {
MyAssert.assertMethod(fieldObj != null, String.format("【%s】為必傳參數", name));
}
//字符串的 非空校驗
if (verify.notNull()) {
MyAssert.assertMethod(!(fieldObj == null || "".equals(fieldObj)), String.format("【%s】不能為空", name));
}
//是否有最大長度限制 斷言判斷
int maxLength = verify.maxLength();
if (Integer.MAX_VALUE != maxLength) {
MyAssert.assertMethod(maxLength > String.valueOf(fieldObj).length(), String.format("【%s】長度不合理,最大長度為【%s】", name, maxLength));
}
//是否有最小長度限制 斷言判斷
int minLength = verify.minLength();
if (Integer.MIN_VALUE != minLength) {
MyAssert.assertMethod(minLength < String.valueOf(fieldObj).length(), String.format("【%s】長度不合理,最小長度為【%s】", name, minLength));
}
//是否有正則校驗
if (!"".equals(verify.regular().getRegex())) {
//初始化Pattern
Pattern pattern = Pattern.compile(verify.regular().getRegex());
//斷言判斷正則
MyAssert.assertMethod(pattern.matcher(String.valueOf(fieldObj)).matches(), String.format("參數【%s】的請求數據不符合規則", name));
}
}
/**
* hutool中的方法
*/
private <T> int indexOf(T[] array, Object value) {
if (null != array) {
for (int i = 0; i < array.length; ++i) {
if (equal(value, array[i])) {
return i;
}
}
}
return -1;
}
/**
* hutool中的方法
*/
private boolean equal(Object obj1, Object obj2) {
return Objects.equals(obj1, obj2);
}
}
使用Param-Validate
導入依賴
<dependency>
<groupId>com.github.18338862369</groupId>
<artifactId>param-validate</artifactId>
<version>1.0.0</version>
</dependency>
此依賴中包含了下述依賴,如果是其他SpringBoot版本,建議對依賴進行排除,並重新引入依賴。
本地引入依賴
創建一個文件夾將param-validate-1.0.0.jar放入,創建一個名為install.bat的bat文件,執行文件即可將jar安裝到本地maven倉庫。
mvn install:install-file -Dfile=%~dp0param-validate-1.0.0.jar -DgroupId=com.github.18338862369 -DartifactId=param-validate -Dversion=1.0.0 -Dpackaging=jar
使用
在啟動類上加入@EnableVerify(execution = "execution(* cn.rayfoo.web..*(..))")
注解
package cn.rayfoo;
import EnableVerify;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/9 16:05
*/
@SpringBootApplication
@EnableVerify(execution = "execution(* cn.rayfoo.web..*(..))")
public class Runner {
public static void main(String[] args) {
SpringApplication.run(Runner.class, args);
}
}
在Controller測試
package cn.rayfoo.web;
import Verify;
import RegexOption;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author rayfoo@qq.com
* @version 1.0
* <p></p>
* @date 2020/8/9 16:12
*/
@RestController
public class TestController {
@GetMapping("/test")
public void test(@Verify(name = "用戶名",regular = RegexOption.USERNAME_REGEX)String username){
System.out.println("validate");
}
}
使用postMan測試
如果成功進行了參數校驗,表示jar包執行正常
下載地址:https://github.com/18338862369/param-validate/releases/tag/1.0.0.RELEASE
如果對你有用,歡迎star支持一下~