SpringBoot統一參數校驗開源項目:param-validate


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支持一下~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM