SpringBoot應用開發框架搭建

Spring的簡史

第一階段:XML配置,在Spring1.x時代,使用Spring開發滿眼都是xml配置的Bean,隨着項目的擴大,咱們須要把xml配置文件分放到不一樣的配置文件裏,那時候須要頻繁的在開發的類和配置文件之間切換。html

第二階段:註解配置,在Spring2.x時代,Spring提供聲明Bean的註解,大大減小了配置量。應用的基本配置用xml,業務配置用註解。前端

第三階段:Java配置,從Spring3.x到如今,Spring提供了Java配置,使用Java配置可讓你更理解你所配置的Bean。java

Spring Boot:使用「習慣優於配置」的理念讓你的項目快速運行起來。使用Spring Boot很容易建立一個獨立運行、準生產級別的基於Spring框架的項目,使用Spring Boot你能夠不用或者只須要不多的Spring配置。mysql

下面就來使用Spring Boot一步步搭建一個先後端分離的應用開發框架,而且之後不斷的去完善這個框架,往裏面添加功能。後面以實戰爲主,不會介紹太多概念,取而代之的是詳細的操做。git

零、開發技術簡介

開發平臺:windowsgithub

開發工具:Intellij IDEA 2017.1web

JDK:Java 8redis

Maven:maven-3.3.9算法

服務器:tomcat 8.0spring

數據庫:MySQL 5.7

數據源:Druid1.1.6

緩存:Redis 3.2

日誌框架:SLF4J+Logback

Spring Boot:1.5.9.RELEASE

ORM框架:MyBatis+通用Mapper

Spring Boot官方文檔:Spring Boot Reference Guide

1、建立項目

這一節建立項目的基礎結構,按照spring boot的思想,將各個不一樣的功能按照starter的形式拆分開來,作到靈活組合,並簡單介紹下Spring Boot相關的東西。

一、建立工程

① 經過File > New > Project,新建工程,選擇Spring Initializr,而後Next。

② 儘可能爲本身的框架想個好點的名字,能夠去申請個本身的域名。我這裏項目名稱爲Sunny,項目路徑爲com.lyyzoo.sunny。

③ 這裏先什麼都不選,後面再去集成。注意個人Spring Boot版本爲1.5.9。Next

④ 定義好工程的目錄,用一個專用目錄吧,不要在一個目錄下和其它東西雜在一塊兒。以後點擊Finish。

上面說的這麼詳細,只有一個目的,從一個開始就作好規範。

⑤ 生成的項目結構以下,能夠本身去看下pom.xml裏的內容。

二、建立Starter

先建立一個core核心、cache緩存、security受權認證,其它的後面再集成進去。

跟上面同樣的方式,在Sunny下建立sunny-starter-core、sunny-starter-cache、sunny-starter-security子模塊。

這樣分模塊後,咱們之後須要哪一個模塊就引入哪一個模塊便可,若是哪一個模塊不知足需求,還能夠重寫該模塊。

最終的項目結構以下:

三、啓動項目

首先在core模塊下來啓動並瞭解SpringBoot項目。

① 在com.lyyzoo.core根目錄下,有一個SunnyStarterCoreApplication,這是SpringBoot的入口類,一般是*Application的命名。

入口類裏有一個main方法,其實就是一個標準的Java應用的入口方法。在main方法中使用SpringApplication.run啓動Spring Boot項目。

而後看看@SpringBootApplication註解,@SpringBootApplication是Spring Boot的核心註解,是一個組合註解。

@EnableAutoConfiguration讓Spring Boot根據類路徑中的jar包依賴爲當前項目進行自動配置。

Spring Boot會自動掃描@SpringBootApplication所在類的同級包以及下級包裏的Bean。

② 先啓動項目,這裏能夠看到有一個Spring Boot的啓動程序,點擊右邊的按鈕啓動項目。看到控制檯Spring的標誌,就算是啓動成功了。

③ 替換默認的banner

能夠到http://patorjk.com/software/taag/這個網站生成一個本身項目的banner。建立banner.txt並放到resources根目錄下。

四、Spring Boot 配置

① 配置文件

Spring Boot使用一個全局的配置文件application.properties或application.yaml,放置在src/main/resources目錄下。咱們能夠在這個全局配置文件中對一些默認的配置值進行修改。

具體有哪些配置可到官網查找,有很是多的配置,不過大部分使用默認便可。Common application properties

而後,須要爲不一樣的環境配置不一樣的配置文件,全局使用application-{profile}.properties指定不一樣環境配置文件。

我這裏增長了開發環境(dev)和生產環境(prod)的配置文件,並經過在application.properties中設置spring.profiles.active=dev來指定當前環境。

② starter pom

Spring Boot爲咱們提供了簡化開發絕大多數場景的starter pom,只要使用了應用場景所需的starter pom,無需繁雜的配置,就能夠獲得Spring Boot爲咱們提供的自動配置的Bean。

後面咱們將會經過加入這些starter來一步步集成咱們想要的功能。具體有哪些starter,能夠到官網查看:Starters

③ 自動配置

Spring Boot關於自動配置的源碼在spring-boot-autoconfigure中以下:

咱們能夠在application.properties中加入debug=true,查看當前項目中已啓用和未啓用的自動配置。

咱們在application.properties中的配置其實就是覆蓋spring-boot-autoconfigure裏的默認配置,好比web相關配置在web包下。

常見的如HttpEncodingProperties配置http編碼,裏面自動配置的編碼爲UTF-8。

MultipartProperties,上傳文件的屬性,設置了上傳最大文件1M。

ServerProperties,配置內嵌Servlet容器,配置端口、contextPath等等。

以前說@SpringBootApplication是Spring Boot的核心註解,但他的核心功能是由@EnableAutoConfiguration註解提供的。

@EnableAutoConfiguration註解經過@Import導入配置功能,在AutoConfigurationImportSelector中,經過SpringFactoriesLoader.loadFactoryNames掃描META-INF/spring.factories文件。

在spring.factories中,配置了須要自動配置的類,咱們也能夠經過這種方式添加本身的自動配置。

在spring-boot-autoconfigure下就有一個spring.factories,以下:

說了這麼多,只爲說明一點,Spring Boot爲咱們作了不少自動化的配置,搭建快速方便。

可是,正由於它爲咱們作了不少事情,就有不少坑,有時候,出了問題,咱們可能很難找出問題所在,這時候,咱們可能就要考慮下是不是自動配置致使的,有可能配置衝突了,或者沒有使用上自定義的配置等等。

五、項目結構劃分

core是項目的核心模塊,結構初步規劃以下:

 base是項目的基礎核心,定義一些基礎類,如BaseController、BaseService等;

    cache是緩存相關;

    config是配置中心,模塊全部的配置放到config裏統一管理;

    constants裏定義系統的常量。

    exception裏封裝一些基礎的異常類;

 system是系統模塊;

    util裏則是一些通用工具類;

2、基礎結構功能

一、web支持

只需在pom.xml中加入spring-boot-starter-web的依賴便可。

以後,查看POM的依賴樹(插件:Maven Helper),能夠看到引入了starter、tomcat、web支持等。能夠看出,Sping Boot內嵌了servlet容器,默認tomcat。

自動配置在WebMvcAutoConfiguration和WebMvcProperties裏,可自行查看源碼,通常咱們不需添加其餘配置就能夠啓動這個web項目了。

二、基礎功能

在core中添加一些基礎的功能支持。

① 首先引入一些經常使用的依賴庫,主要是一些經常使用工具類,方便之後的開發。

<!-- ******************************* 經常使用依賴庫 ********************************** -->
<!-- 針對開發IO流功能的工具類庫 -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>${commons.io.version}</version>
</dependency>
<!-- 文件上傳 -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>${commons.fileupload.version}</version>
    <exclusions>
        <exclusion>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- 經常使用的集合操做,豐富的工具類 -->
<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>${commons.collections.version}</version>
</dependency>
<!-- 操做javabean的工具包 -->
<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>${commons.beanutils.version}</version>
    <exclusions>
        <exclusion>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- 包含一些通用的編碼解碼算法. 如:MD五、SHA一、Base64等 -->
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>${commons.codec.version}</version>
</dependency>
<!-- 包含豐富的工具類如 StringUtils -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>${commons.lang3.version}</version>
</dependency>
<!--
    Guava工程包含了若干被Google的Java項目普遍依賴的核心庫. 集合[collections] 、緩存[caching] 、原生類型支持[primitives support] 、
    併發庫[concurrency libraries] 、通用註解[common annotations] 、字符串處理[string processing] 、I/O 等等。
-->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>${guava.version}</version>
</dependency>
View Code

版本號以下:

② 在base添加一個Result類,做爲前端的返回對象,Controller的直接返回對象都是Result。

package com.lyyzoo.core.base;

import com.fasterxml.jackson.annotation.JsonInclude;

import java.io.Serializable;

/**
 * 前端返回對象
 *
 * @version 1.0
 * @author bojiangzhou 2017-12-28
 */
public class Result implements Serializable {
    private static final long serialVersionUID = 1430633339880116031L;

    /**
     * 成功與否標誌
     */
    private boolean success = true;
    /**
     * 返回狀態碼,爲空則默認200.前端須要攔截一些常見的狀態碼如40三、40四、500等
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private Integer status;
    /**
     * 編碼,可用於前端處理多語言,不須要則不用返回編碼
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String code;
    /**
     * 相關消息
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String msg;
    /**
     * 相關數據
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private Object data;


    public Result() {}

    public Result(boolean success) {
        this.success = success;
    }

    public Result(boolean success, Integer status) {
        this.success = success;
        this.status = status;
    }

    public Result(boolean success, String code, String msg){
        this(success);
        this.code = code;
        this.msg = msg;
    }

    public Result(boolean success, Integer status, String code, String msg) {
        this.success = success;
        this.status = status;
        this.code = code;
        this.msg = msg;
    }

    public Result(boolean success, String code, String msg, Object data){
        this(success);
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}
View Code

以後在util添加生成Result的工具類Results,用於快速方便的建立Result對象。

package com.lyyzoo.core.util;

import com.lyyzoo.core.base.Result;

/**
 * Result生成工具類
 *
 * @version 1.0
 * @author bojiangzhou 2017-12-28
 */
public class Results {

    protected Results() {}

    public static Result newResult() {
        return new Result();

    }

    public static Result newResult(boolean success) {
        return new Result(success);
    }

    //
    // 業務調用成功
    // ----------------------------------------------------------------------------------------------------
    public static Result success() {
        return new Result();
    }

    public static Result success(String msg) {
        return new Result(true, null, msg);
    }

    public static Result success(String code, String msg) {
        return new Result(true, code, msg);
    }

    public static Result successWithStatus(Integer status) {
        return new Result(true, status);
    }

    public static Result successWithStatus(Integer status, String msg) {
        return new Result(true, status, null, msg);
    }

    public static Result successWithData(Object data) {
        return new Result(true, null, null, data);
    }

    public static Result successWithData(Object data, String msg) {
        return new Result(true, null, msg, data);
    }

    public static Result successWithData(Object data, String code, String msg) {
        return new Result(true, code, msg, data);
    }

    //
    // 業務調用失敗
    // ----------------------------------------------------------------------------------------------------
    public static Result failure() {
        return new Result(false);
    }

    public static Result failure(String msg) {
        return new Result(false, null, msg);
    }

    public static Result failure(String code, String msg) {
        return new Result(false, code, msg);
    }

    public static Result failureWithStatus(Integer status) {
        return new Result(false, status);
    }

    public static Result failureWithStatus(Integer status, String msg) {
        return new Result(false, status, null, msg);
    }

    public static Result failureWithData(Object data) {
        return new Result(false, null, null, data);
    }

    public static Result failureWithData(Object data, String msg) {
        return new Result(false, null, msg, data);
    }

    public static Result failureWithData(Object data, String code, String msg) {
        return new Result(false, code, msg, data);
    }

}
View Code

③ 在base添加BaseEnum<K, V>枚舉接口,定義了獲取值和描述的接口。

package com.lyyzoo.core.base;

/**
 * 基礎枚舉接口
 *
 * @version 1.0
 * @author bojiangzhou 2017-12-31
 */
public interface BaseEnum<K, V> {

    /**
     * 獲取編碼
     *
     * @return 編碼
     */
    K code();

    /**
     * 獲取描述
     * 
     * @return 描述
     */
    V desc();

}
View Code

而後在constants下定義一個基礎枚舉常量類,咱們把一些描述信息維護到枚舉裏面,儘可能不要在代碼中直接出現魔法值(如一些編碼、中文等),之後的枚舉常量類也能夠按照這種模式來寫。

package com.lyyzoo.core.constants;

import com.lyyzoo.core.base.BaseEnum;

import java.util.HashMap;
import java.util.Map;

/**
 * 基礎枚舉值
 *
 * @version 1.0
 * @author bojiangzhou 2018-01-01
 */
public enum BaseEnums implements BaseEnum<String, String> {

    SUCCESS("request.success", "請求成功"),

    FAILURE("request.failure", "請求失敗"),

    OPERATION_SUCCESS("operation.success", "操做成功"),

    OPERATION_FAILURE("operation.failure", "操做失敗"),

    ERROR("system.error", "系統異常"),

    NOT_FOUND("not_found", "請求資源不存在"),

    FORBIDDEN("forbidden", "無權限訪問"),

    VERSION_NOT_MATCH("record_not_exists_or_version_not_match", "記錄版本不存在或不匹配"),

    PARAMETER_NOT_NULL("parameter_not_be_null", "參數不能爲空");

    private String code;

    private String desc;

    private static Map<String, String> allMap = new HashMap<>();

    BaseEnums(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    static {
        for(BaseEnums enums : BaseEnums.values()){
            allMap.put(enums.code, enums.desc);
        }
    }

    @Override
    public String code() {
        return code;
    }

    @Override
    public String desc() {
        return desc;
    }

    public String desc(String code) {
        return allMap.get(code);
    }

}
View Code

④ 再添加一個經常使用的日期工具類對象,主要包含一些經常使用的日期時間格式化,後續可再繼續往裏面添加一些公共方法。

package com.lyyzoo.core.util;


import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 日期時間工具類
 *
 * @version 1.0
 * @author bojiangzhou 2017-12-28
 */
public class Dates {

    /**
     * 日期時間匹配格式
     */
    public interface Pattern {
        //
        // 常規模式
        // ----------------------------------------------------------------------------------------------------
        /**
         * yyyy-MM-dd
         */
        String DATE = "yyyy-MM-dd";
        /**
         * yyyy-MM-dd HH:mm:ss
         */
        String DATETIME = "yyyy-MM-dd HH:mm:ss";
        /**
         * yyyy-MM-dd HH:mm
         */
        String DATETIME_MM = "yyyy-MM-dd HH:mm";
        /**
         * yyyy-MM-dd HH:mm:ss.SSS
         */
        String DATETIME_SSS = "yyyy-MM-dd HH:mm:ss.SSS";
        /**
         * HH:mm
         */
        String TIME = "HH:mm";
        /**
         * HH:mm:ss
         */
        String TIME_SS = "HH:mm:ss";

        //
        // 系統時間格式
        // ----------------------------------------------------------------------------------------------------
        /**
         * yyyy/MM/dd
         */
        String SYS_DATE = "yyyy/MM/dd";
        /**
         * yyyy/MM/dd HH:mm:ss
         */
        String SYS_DATETIME = "yyyy/MM/dd HH:mm:ss";
        /**
         * yyyy/MM/dd HH:mm
         */
        String SYS_DATETIME_MM = "yyyy/MM/dd HH:mm";
        /**
         * yyyy/MM/dd HH:mm:ss.SSS
         */
        String SYS_DATETIME_SSS = "yyyy/MM/dd HH:mm:ss.SSS";

        //
        // 無鏈接符模式
        // ----------------------------------------------------------------------------------------------------
        /**
         * yyyyMMdd
         */
        String NONE_DATE = "yyyyMMdd";
        /**
         * yyyyMMddHHmmss
         */
        String NONE_DATETIME = "yyyyMMddHHmmss";
        /**
         * yyyyMMddHHmm
         */
        String NONE_DATETIME_MM = "yyyyMMddHHmm";
        /**
         * yyyyMMddHHmmssSSS
         */
        String NONE_DATETIME_SSS = "yyyyMMddHHmmssSSS";
    }

    public static final String DEFAULT_PATTERN = Pattern.DATETIME;

    public static final String[] PARSE_PATTERNS = new String[]{
            Pattern.DATE,
            Pattern.DATETIME,
            Pattern.DATETIME_MM,
            Pattern.DATETIME_SSS,
            Pattern.SYS_DATE,
            Pattern.SYS_DATETIME,
            Pattern.SYS_DATETIME_MM,
            Pattern.SYS_DATETIME_SSS
    };

    /**
     * 格式化日期時間
     * 
     * @param date 日期時間
     *
     * @return yyyy-MM-dd HH:mm:ss
     */
    public static String format(Date date) {
        return format(date, DEFAULT_PATTERN);
    }

    /**
     * 格式化日期
     * 
     * @param date 日期(時間)
     *
     * @param pattern 匹配模式 參考:{@link Dates.Pattern}
     *
     * @return 格式化後的字符串
     */
    public static String format(Date date, String pattern) {
        if (date == null) {
            return null;
        }
        pattern = StringUtils.isNotBlank(pattern) ? pattern : DEFAULT_PATTERN;
        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
        return sdf.format(date);
    }

    /**
     * 解析日期
     *
     * @param date 日期字符串
     *
     * @return 解析後的日期 默認格式:yyyy-MM-dd HH:mm:ss
     */
    public static Date parseDate(String date) {
        if (StringUtils.isBlank(date)) {
            return null;
        }
        try {
            return DateUtils.parseDate(date, PARSE_PATTERNS);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 解析日期
     *
     * @param date 日期
     *
     * @param pattern 格式 參考:{@link Dates.Pattern}
     *
     * @return 解析後的日期,默認格式:yyyy-MM-dd HH:mm:ss
     */
    public static Date parseDate(String date, String pattern) {
        if (StringUtils.isBlank(date)) {
            return null;
        }
        String[] parsePatterns;
        parsePatterns = StringUtils.isNotBlank(pattern) ? new String[]{pattern} : PARSE_PATTERNS;
        try {
            return DateUtils.parseDate(date, parsePatterns);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }



}
View Code

⑤ Constants定義系統級的通用常量。

package com.lyyzoo.core.constants;

import com.google.common.base.Charsets;

import java.nio.charset.Charset;

/**
 * 系統級常量類
 *
 * @version 1.0
 * @author bojiangzhou 2017-12-28
 */
public class Constants {

    public static final String APP_NAME = "sunny";

    /**
     * 系統編碼
     */
    public static final Charset CHARSET = Charsets.UTF_8;

    /**
     * 標識:是/否、啓用/禁用等
     */
    public interface Flag {

        Integer YES = 1;

        Integer NO = 0;
    }

    /**
     * 操做類型
     */
    public interface Operation {
        /**
         * 添加
         */
        String ADD = "add";
        /**
         * 更新
         */
        String UPDATE = "update";
        /**
         * 刪除
         */
        String DELETE = "delete";
    }

    /**
     * 性別
     */
    public interface Sex {
        /**
         * 男
         */
        Integer MALE = 1;
        /**
         * 女
         */
        Integer FEMALE = 0;
    }

}
View Code

⑥ 在base添加空的BaseController、BaseDTO、Service、Mapper,先定義好基礎結構,後面再添加功能。

BaseDTO:標準的who字段、版本號、及10個擴展字段。

由於這裏用到了@Transient註解,先引入java持久化包:

package com.lyyzoo.core.base;

import com.fasterxml.jackson.annotation.*;
import com.lyyzoo.core.Constants;
import com.lyyzoo.core.util.Dates;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

import javax.persistence.Transient;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 基礎實體類
 *
 * @version 1.0
 * @author bojiangzhou 2017-12-29
 */
public class BaseDTO implements Serializable {
    private static final long serialVersionUID = -4287607489867805101L;

    public static final String FIELD_OPERATE = "operate";
    public static final String FIELD_OBJECT_VERSION_NUMBER = "versionNumber";
    public static final String FIELD_CREATE_BY = "createBy";
    public static final String FIELD_CREATOR = "creator";
    public static final String FIELD_CREATE_DATE = "createDate";
    public static final String FIELD_UPDATE_BY = "updateBy";
    public static final String FIELD_UPDATER = "updater";
    public static final String FIELD_UPDATE_DATE = "updateDate";


    /**
     * 操做類型,add/update/delete 參考:{@link Constants.Operation}
     */
    @Transient
    private String _operate;

    /**
     * 數據版本號,每發生update則自增,用於實現樂觀鎖.
     */
    private Long versionNumber;

    //
    // 下面是標準 WHO 字段
    // ----------------------------------------------------------------------------------------------------
    /**
     * 建立人用戶名
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private Long createBy;
    /**
     * 建立人名稱
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    @Transient
    private String creator;
    /**
     * 建立時間
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    @JsonFormat(pattern = Dates.DEFAULT_PATTERN)
    private Date createDate;

    /**
     * 更新人用戶名
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private Long updateBy;
    /**
     * 更新人名稱
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    @Transient
    private String updater;
    /**
     * 更新時間
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    @JsonFormat(pattern = Dates.DEFAULT_PATTERN)
    private Date updateDate;

    /**
     * 其它屬性
     */
    @JsonIgnore
    @Transient
    protected Map<String, Object> innerMap = new HashMap<>();

    //
    // 下面是擴展屬性字段
    // ----------------------------------------------------------------------------------------------------

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String attribute1;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String attribute2;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String attribute3;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String attribute4;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String attribute5;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String attribute6;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String attribute7;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String attribute8;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String attribute9;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String attribute10;

    public String get_operate() {
        return _operate;
    }

    public void set_operate(String _operate) {
        this._operate = _operate;
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
    }

    public String toJSONString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
    }

    public Long getVersionNumber() {
        return versionNumber;
    }

    public void setVersionNumber(Long versionNumber) {
        this.versionNumber = versionNumber;
    }

    public Long getCreateBy() {
        return createBy;
    }

    public void setCreateBy(Long createBy) {
        this.createBy = createBy;
    }

    public String getCreator() {
        return creator;
    }

    public void setCreator(String creator) {
        this.creator = creator;
    }

    public Date getCreateDate() {
        return createDate;
    }

    public void setCreateDate(Date createDate) {
        this.createDate = createDate;
    }

    public Long getUpdateBy() {
        return updateBy;
    }

    public void setUpdateBy(Long updateBy) {
        this.updateBy = updateBy;
    }

    public String getUpdater() {
        return updater;
    }

    public void setUpdater(String updater) {
        this.updater = updater;
    }

    public Date getUpdateDate() {
        return updateDate;
    }

    public void setUpdateDate(Date updateDate) {
        this.updateDate = updateDate;
    }

    @JsonAnyGetter
    public Object getAttribute(String key) {
        return innerMap.get(key);
    }

    @JsonAnySetter
    public void setAttribute(String key, Object obj) {
        innerMap.put(key, obj);
    }

    public String getAttribute1() {
        return attribute1;
    }

    public void setAttribute1(String attribute1) {
        this.attribute1 = attribute1;
    }

    public String getAttribute2() {
        return attribute2;
    }

    public void setAttribute2(String attribute2) {
        this.attribute2 = attribute2;
    }

    public String getAttribute3() {
        return attribute3;
    }

    public void setAttribute3(String attribute3) {
        this.attribute3 = attribute3;
    }

    public String getAttribute4() {
        return attribute4;
    }

    public void setAttribute4(String attribute4) {
        this.attribute4 = attribute4;
    }

    public String getAttribute5() {
        return attribute5;
    }

    public void setAttribute5(String attribute5) {
        this.attribute5 = attribute5;
    }

    public String getAttribute6() {
        return attribute6;
    }

    public void setAttribute6(String attribute6) {
        this.attribute6 = attribute6;
    }

    public String getAttribute7() {
        return attribute7;
    }

    public void setAttribute7(String attribute7) {
        this.attribute7 = attribute7;
    }

    public String getAttribute8() {
        return attribute8;
    }

    public void setAttribute8(String attribute8) {
        this.attribute8 = attribute8;
    }

    public String getAttribute9() {
        return attribute9;
    }

    public void setAttribute9(String attribute9) {
        this.attribute9 = attribute9;
    }

    public String getAttribute10() {
        return attribute10;
    }

    public void setAttribute10(String attribute10) {
        this.attribute10 = attribute10;
    }

}
View Code

同時,重寫了toString方法,增長了toJsonString方法,使得能夠格式化輸出DTO的數據:

直接打印DTO,輸出的格式大概就是這個樣子:

⑦ 在exception添加BaseException,定義一些基礎異常類

基礎異常類都繼承自運行時異常類(RunntimeException),儘量把受檢異常轉化爲非受檢異常,更好的面向接口編程,提升代碼的擴展性、穩定性。

BaseException:添加了一個錯誤編碼,其它自定義的異常應當繼承該類。

package com.lyyzoo.core.exception;

/**
 * 基礎異常類
 *
 * @version 1.0
 * @author bojiangzhou 2017-12-31
 */
public class BaseException extends RuntimeException {
    private static final long serialVersionUID = -997101946070796354L;

    /**
     * 錯誤編碼
     */
    protected String code;

    public BaseException() {}

    public BaseException(String message) {
        super(message);
    }

    public BaseException(String code, String message) {
        super(message);
        this.code = code;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }
}
View Code

ServiceException:繼承BaseException,Service層往Controller拋出的異常。

package com.lyyzoo.core.exception;

/**
 * Service層異常
 *
 * @version 1.0
 * @author bojiangzhou 2017-12-31
 */
public class ServiceException extends BaseException {
    private static final long serialVersionUID = 6058294324031642376L;

    public ServiceException() {}

    public ServiceException(String message) {
        super(message);
    }

    public ServiceException(String code, String message) {
        super(code, message);
    }

}
View Code

三、添加系統用戶功能,使用Postman測試接口

① 在system模塊下,再分紅dto、controller、service、mapper、constants子包,之後一個模塊功能開發就是這樣一個基礎結構。

User:系統用戶

package com.lyyzoo.core.system.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.lyyzoo.core.base.BaseDTO;
import com.lyyzoo.core.util.Dates;

import java.util.Date;

/**
 * 系統用戶
 *
 * @version 1.0
 * @author bojiangzhou 2017-12-31
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User extends BaseDTO {
    private static final long serialVersionUID = -7395431342743009038L;

    /**
     * 用戶ID
     */
    private Long userId;
    /**
     * 用戶名
     */
    private String username;
    /**
     * 密碼
     */
    private String password;
    /**
     * 暱稱
     */
    private String nickname;
    /**
     * 生日
     */
    @JsonFormat(pattern = Dates.Pattern.DATE)
    private Date birthday;
    /**
     * 性別:1-男/0-女
     */
    private Integer sex;
    /**
     * 是否啓用:1/0
     */
    private Integer enabled;

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public Integer getSex() {
        return sex;
    }

    public void setSex(Integer sex) {
        this.sex = sex;
    }

    public Integer getEnabled() {
        return enabled;
    }

    public void setEnabled(Integer enabled) {
        this.enabled = enabled;
    }

}
View Code

UserController:用戶控制層;用@RestController註解,先後端分離,由於無需返回視圖,採用Restful風格,直接返回數據。

package com.lyyzoo.core.system.controller;

import com.lyyzoo.core.Constants;
import com.lyyzoo.core.base.BaseController;
import com.lyyzoo.core.base.BaseEnums;
import com.lyyzoo.core.base.Result;
import com.lyyzoo.core.system.dto.User;
import com.lyyzoo.core.util.Dates;
import com.lyyzoo.core.util.Results;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

/**
 * 用戶Controller
 *
 * @version 1.0
 * @author bojiangzhou 2017-12-31
 */
@RequestMapping("/sys/user")
@RestController
public class UserController extends BaseController {

    private static List<User> userList = new ArrayList<>();

    // 先靜態模擬數據
    static {
        User user1 = new User();
        user1.setUserId(1L);
        user1.setUsername("lufei");
        user1.setNickname("蒙奇D路飛");
        user1.setBirthday(Dates.parseDate("2000-05-05"));
        user1.setSex(Constants.Sex.MALE);
        user1.setEnabled(Constants.Flag.YES);
        userList.add(user1);

        User user2 = new User();
        user2.setUserId(2L);
        user2.setUsername("nami");
        user2.setNickname("娜美");
        user2.setBirthday(Dates.parseDate("2000/7/3"));
        user2.setSex(Constants.Sex.FEMALE);
        user2.setEnabled(Constants.Flag.YES);
        userList.add(user2);
    }

    @RequestMapping("/queryAll")
    public Result queryAll(){
        return Results.successWithData(userList, BaseEnums.SUCCESS.code(), BaseEnums.SUCCESS.description());
    }

    @RequestMapping("/queryOne/{userId}")
    public Result queryOne(@PathVariable Long userId){
        User user = null;
        for(User u : userList){
            if(u.getUserId().longValue() == userId){
                user = u;
            }
        }
        return Results.successWithData(user);
    }
}
View Code

② Postman請求:請求成功,基礎的HTTP服務已經實現了。

3、集成MyBatis,實現基礎Mapper和Service

一、添加JDBC、配置數據源

添加spring-boot-starter-jdbc以支持JDBC訪問數據庫,而後添加MySql的JDBC驅動mysql-connector-java;

在application.properties裏配置mysql的數據庫驅動

以後在application-dev.properties裏配置開發環境數據庫的鏈接信息,添加以後,Springboot就會自動配置數據源了。

二、集成MyBatis

MyBatis官方爲了方便Springboot集成MyBatis,專門提供了一個符合Springboot規範的starter項目,即mybatis-spring-boot-starter。

在application.properties裏添加mybatis映射配置:

三、添加MyBatis通用Mapper

通用Mapper能夠極大的簡化開發,極其方便的進行單表的增刪改查。

關於通用Mapper,參考網站地址:

  MyBatis通用Mapper

  MyBatis 相關工具

以後,在core.base下建立自定義的Mapper,按需選擇接口。

具體可參考:根據須要自定義接口

package com.lyyzoo.core.base;

import tk.mybatis.mapper.common.BaseMapper;
import tk.mybatis.mapper.common.ConditionMapper;
import tk.mybatis.mapper.common.IdsMapper;
import tk.mybatis.mapper.common.special.InsertListMapper;

/**
 *
 * BaseMapper
 *
 * @name BaseMapper
 * @version 1.0
 * @author bojiangzhou 2017-12-31
 */
public interface Mapper<T> extends BaseMapper<T>, ConditionMapper<T>, IdsMapper<T>, InsertListMapper<T> {

}
View Code

定義好基礎Mapper後,就具備下圖中的基本通用方法了。每一個實體類對應的*Mapper繼承Mapper<T>來得到基本的增刪改查的通用方法。

在application.properties裏配置自定義的基礎Mapper

四、添加分頁插件PageHelper

參考地址:

  MyBatis 分頁插件 - PageHelper

  分頁插件使用方法

分頁插件配置,通常狀況下,不須要作任何配置。

以後,咱們就能夠在代碼中使用 PageHelper.startPage(1, 10) 對緊隨其後的一個查詢進行分頁查詢,很是方便。

五、配置自動掃描Mapper

在config下建立MyBatisConfig配置文件,經過mapperScannerConfigurer方法配置自動掃描Mapper文件。

package com.lyyzoo.core.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import tk.mybatis.spring.mapper.MapperScannerConfigurer;

/**
 * MyBatis相關配置.
 *
 * @version 1.0
 * @author bojiangzhou 2018-01-07
 */
@Configuration
public class MyBatisConfig {

    /**
     * Mapper掃描配置. 自動掃描將Mapper接口生成代理注入到Spring.
     */
    @Bean
    public static MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        // 注意這裏的掃描路徑: 1.不要掃描到自定義的Mapper; 2.定義的路徑不要掃描到tk.mybatis.mapper(如定義**.mapper).
        // 兩個作法都會致使掃描到tk.mybatis的Mapper,就會產生重複定義的報錯.
        mapperScannerConfigurer.setBasePackage("**.lyyzoo.**.mapper");
        return mapperScannerConfigurer;
    }

}
View Code

注意這裏的 MapperScannerConfigurer 是tk.mybatis.spring.mapper.MapperScannerConfigurer,而不是org.mybatis,不然使用通用Mapper的方法時會報相似下面的這種錯誤

六、定義基礎Service

通常來講,咱們不能在Controller中直接訪問Mapper,所以咱們須要加上Service,經過Service訪問Mapper。

首先定義基礎Service<T>接口,根據Mapper定義基本的增刪改查接口方法。

package com.lyyzoo.core.base;

import java.util.List;

/**
 * Service 基礎通用接口
 *
 * @name BaseService
 * @version 1.0
 * @author bojiangzhou 2017-12-31
 */
public interface Service<T> {

    //
    // insert
    // ----------------------------------------------------------------------------------------------------
    /**
     * 保存一個實體,null的屬性也會保存,不會使用數據庫默認值
     *
     * @param record
     * @return
     */
    T insert(T record);

    /**
     * 批量插入,null的屬性也會保存,不會使用數據庫默認值
     *
     * @param recordList
     * @return
     */
    List<T> insert(List<T> recordList);

    /**
     * 保存一個實體,null的屬性不會保存,會使用數據庫默認值
     *
     * @param record
     * @return
     */
    T insertSelective(T record);

    /**
     * 批量插入,null的屬性不會保存,會使用數據庫默認值
     *
     * @param recordList
     * @return
     */
    List<T> insertSelective(List<T> recordList);

    //
    // update
    // ----------------------------------------------------------------------------------------------------
    /**
     * 根據主鍵更新實體所有字段,null值會被更新
     *
     * @param record
     * @return
     */
    T update(T record);

    /**
     * 批量更新,根據主鍵更新實體所有字段,null值會被更新
     *
     * @param recordList
     * @return
     */
    List<T> update(List<T> recordList);

    /**
     * 根據主鍵更新屬性不爲null的值
     *
     * @param record
     * @return
     */
    T updateSelective(T record);

    /**
     * 批量更新,根據主鍵更新屬性不爲null的值
     *
     * @param recordList
     * @return
     */
    List<T> updateSelective(List<T> recordList);

    //
    // delete
    // ----------------------------------------------------------------------------------------------------
    /**
     * 根據主鍵刪除
     *
     * @param id id不能爲空
     * @return
     */
    int delete(Long id);

    /**
     * 根據主鍵字符串進行刪除,類中只有存在一個帶有@Id註解的字段
     *
     * @param ids 相似1,2,3
     */
    int delete(String ids);

    /**
     * 根據主鍵刪除多個實體,ID數組
     *
     * @param ids 相似[1,2,3],不能爲空
     */
    int delete(Long[] ids);

    /**
     * 根據實體屬性做爲條件進行刪除
     *
     * @param record
     * @return
     */
    int delete(T record);

    /**
     * 根據主鍵刪除多個實體
     *
     * @param recordList
     * @return
     */
    int delete(List<T> recordList);

    //
    // insert or update or delete
    // ----------------------------------------------------------------------------------------------------
    /**
     * 根據實體的operate決定哪一種操做. null的屬性也會保存,不會使用數據庫默認值
     *
     * @param record
     * @return
     */
    T persist(T record);

    /**
     * 批量操做.根據實體的operate決定哪一種操做. null的屬性也會保存,不會使用數據庫默認值
     *
     * @param recordList
     * @return
     */
    List<T> persist(List<T> recordList);

    /**
     * 根據實體的operate決定哪一種操做. 根據主鍵更新屬性不爲null的值
     *
     * @param record
     * @return
     */
    T persistSelective(T record);

    /**
     * 批量操做.根據實體的operate決定哪一種操做. 根據主鍵更新屬性不爲null的值
     *
     * @param recordList
     * @return
     */
    List<T> persistSelective(List<T> recordList);


    //
    // select
    // ----------------------------------------------------------------------------------------------------
    /**
     * 根據主鍵查詢
     *
     * @param id 不能爲空
     * @return
     */
    T get(Long id);

    /**
     * 根據實體中的屬性進行查詢,只能有一個返回值,有多個結果是拋出異常
     *
     * @param record
     * @return
     */
    T get(T record);

    /**
     * 根據字段和值查詢 返回一個
     * @param key 不能爲空
     * @param value 不能爲空
     * @return
     */
    T get(String key, Object value);


    /**
     * 根據主鍵字符串進行查詢
     *
     * @param ids 如 "1,2,3,4"
     * @return
     */
    List<T> select(String ids);

    /**
     * 根據實體中的屬性值進行查詢
     *
     * @param record
     * @return
     */
    List<T> select(T record);

    /**
     * 根據屬性和值查詢
     *
     * @param key
     * @param value
     * @return
     */
    List<T> select(String key, Object value);

    /**
     * 根據實體中的屬性值進行分頁查詢
     *
     * @param record
     * @param pageNum
     * @param pageSize
     * @return
     */
    List<T> select(T record, int pageNum, int pageSize);

    /**
     * 查詢所有結果
     *
     * @return
     */
    List<T> selectAll();

    /**
     * 根據實體中的屬性查詢總數
     *
     * @param record
     * @return
     */
    int count(T record);

}
View Code

而後是實現類BaseService,之後的開發中,Service接口實現Service<T>,Service實現類繼承BaseService<T>。

package com.lyyzoo.core.base;

import com.github.pagehelper.PageHelper;
import com.lyyzoo.core.constants.Constants;
import com.lyyzoo.core.exception.UpdateFailedException;
import com.lyyzoo.core.util.Reflections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import javax.annotation.PostConstruct;
import javax.persistence.Id;
import java.lang.reflect.Field;
import java.util.List;

/**
 * 基礎Service實現類
 *
 * @version 1.0
 * @author bojiangzhou 2018-01-04
 */
public abstract class BaseService<T> implements Service<T> {

    @Autowired
    private Mapper<T> mapper;

    private Class<T> entityClass;

    @SuppressWarnings("unchecked")
    @PostConstruct
    public void init() {
        this.entityClass = Reflections.getClassGenericType(getClass());
    }

    //
    // insert
    // ----------------------------------------------------------------------------------------------------
    @Transactional(rollbackFor = Exception.class)
    public T insert(T record) {
        mapper.insert(record);
        return record;
    }

    @Transactional(rollbackFor = Exception.class)
    public List<T> insert(List<T> recordList) {
        mapper.insertList(recordList);
        return recordList;
    }

    @Transactional(rollbackFor = Exception.class)
    public T insertSelective(T record) {
        mapper.insertSelective(record);
        return record;
    }

    @Transactional(rollbackFor = Exception.class)
    public List<T> insertSelective(List<T> recordList) {
        // 因爲Mapper暫未提供Selective的批量插入,此處循環查詢. 固然也可參考InsertListMapper本身實現.
        for(T record : recordList){
            mapper.insertSelective(record);
        }
        return recordList;
    }

    //
    // update
    // ----------------------------------------------------------------------------------------------------
    @Transactional(rollbackFor = Exception.class)
    public T update(T record) {
        int count = mapper.updateByPrimaryKey(record);
        checkUpdate(count, record);
        return record;
    }

    @Transactional(rollbackFor = Exception.class)
    public List<T> update(List<T> recordList) {
        // Mapper暫未提供批量更新,此處循實現
        for(T record : recordList){
            int count = mapper.updateByPrimaryKey(record);
            checkUpdate(count, record);
        }
        return recordList;
    }

    @Transactional(rollbackFor = Exception.class)
    public T updateSelective(T record) {
        int count = mapper.updateByPrimaryKeySelective(record);
        checkUpdate(count, record);
        return record;
    }

    @Transactional(rollbackFor = Exception.class)
    public List<T> updateSelective(List<T> recordList) {
        // Mapper暫未提供批量更新,此處循實現
        for(T record : recordList){
            int count = mapper.updateByPrimaryKeySelective(record);
            checkUpdate(count, record);
        }
        return recordList;
    }

    //
    // delete
    // ----------------------------------------------------------------------------------------------------
    @Transactional(rollbackFor = Exception.class)
    public int delete(Long id) {
        return mapper.deleteByPrimaryKey(id);
    }

    @Transactional(rollbackFor = Exception.class)
    public int delete(Long[] ids) {
        int count = 0;
        for(Long id : ids){
            mapper.deleteByPrimaryKey(id);
            count++;
        }
        return count;
    }

    @Transactional(rollbackFor = Exception.class)
    public int delete(T record) {
        return mapper.delete(record);
    }

    @Transactional(rollbackFor = Exception.class)
    public int delete(List<T> recordList) {
        int count = 0;
        for(T record : recordList){
            mapper.delete(record);
            count++;
        }
        return count;
    }

    //
    // all operate. insert or update or delete
    // ----------------------------------------------------------------------------------------------------
    @Transactional(rollbackFor = Exception.class)
    public T persist(T record) {
        BaseDTO dto = (BaseDTO) record;
        Assert.notNull(dto.get_operate(), "_operate not be null.");
        switch (dto.get_operate()) {
            case Constants.Operation.ADD:
                insert(record);
                break;
            case Constants.Operation.UPDATE:
                update(record);
                break;
            case Constants.Operation.DELETE:
                delete(record);
                break;
            default:
                break;
        }
        dto.set_operate(null);
        return record;
    }

    @Transactional(rollbackFor = Exception.class)
    public List<T> persist(List<T> recordList) {
        for(T record : recordList){
            BaseDTO dto = (BaseDTO) record;
            Assert.notNull(dto.get_operate(), "_operate not be null.");
            switch (dto.get_operate()) {
                case Constants.Operation.ADD:
                    insert(record);
                    break;
                case Constants.Operation.UPDATE:
                    update(record);
                    break;
                case Constants.Operation.DELETE:
                    delete(record);
                    break;
                default:
                    break;
            }
            dto.set_operate(null);
        }
        return recordList;
    }

    @Transactional(rollbackFor = Exception.class)
    public T persistSelective(T record) {
        BaseDTO dto = (BaseDTO) record;
        Assert.notNull(dto.get_operate(), "_operate not be null.");
        switch (dto.get_operate()) {
            case Constants.Operation.ADD:
                insertSelective(record);
                break;
            case Constants.Operation.UPDATE:
                updateSelective(record);
                break;
            case Constants.Operation.DELETE:
                delete(record);
                break;
            default:
                break;
        }
        return record;
    }

    @Transactional(rollbackFor = Exception.class)
    public List<T> persistSelective(List<T> recordList) {
        for(T record : recordList){
            BaseDTO dto = (BaseDTO) record;
            Assert.notNull(dto.get_operate(), "_operate not be null.");
            switch (dto.get_operate()) {
                case Constants.Operation.ADD:
                    insertSelective(record);
                    break;
                case Constants.Operation.UPDATE:
                    updateSelective(record);
                    break;
                case Constants.Operation.DELETE:
                    delete(record);
                    break;
                default:
                    break;
            }
        }
        return recordList;
    }

    //
    // select
    // ----------------------------------------------------------------------------------------------------
    public T get(Long id) {
        T entity = null;
        try {
            entity = entityClass.newInstance();
            Field idField = Reflections.getFieldByAnnotation(entityClass, Id.class);
            idField.set(entity, id);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return mapper.selectByPrimaryKey(entity);
    }

    public T get(T record) {
        return mapper.selectOne(record);
    }

    public T get(String key, Object value) {
        T entity = null;
        try {
            entity = entityClass.newInstance();
            Field field = Reflections.getField(entityClass, key);
            field.set(entity, value);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return mapper.selectOne(entity);
    }

    public List<T> select(String ids) {
        return mapper.selectByIds(ids);
    }

    public List<T> select(T record) {

        return mapper.select(record);
    }

    public List<T> select(String key, Object value) {
        T entity = null;
        try {
            entity = entityClass.newInstance();
            Field field = Reflections.getField(entityClass, key);
            field.set(entity, value);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return mapper.select(entity);
    }

    public List<T> select(T record, int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        return mapper.select(record);
    }

    public List<T> selectAll() {
        return mapper.selectAll();
    }

    public int count(T record) {
        return mapper.selectCount(record);
    }

    /**
     * 檢查樂觀鎖<br>
     * 更新失敗時,拋出 UpdateFailedException 異常
     *
     * @param updateCount update,delete 操做返回的值
     * @param record 操做參數
     */
    protected void checkUpdate(int updateCount, Object record) {
        if (updateCount == 0 && record instanceof BaseDTO) {
            BaseDTO baseDTO = (BaseDTO) record;
            if (baseDTO.getVersion() != null) {
                throw new UpdateFailedException();
            }
        }
    }

}
View Code

BaseService的實現用到了反射工具類Reflections:

package com.lyyzoo.core.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

/**
 * 反射工具類.
 *
 * @version 1.0
 * @author bojiangzhou 2018-01-06
 */

public abstract class Reflections {

    private static Logger logger = LoggerFactory.getLogger(Reflections.class);

    /**
     * 經過反射, 得到Class定義中聲明的泛型參數的類型, 注意泛型必須定義在父類處. 如沒法找到, 返回Object.class.
     *
     * @param clazz class類
     *
     * @return the 返回第一個聲明的泛型類型. 若是沒有,則返回Object.class
     */
    @SuppressWarnings("unchecked")
    public static Class getClassGenericType(final Class clazz) {
        return getClassGenericType(clazz, 0);
    }

    /**
     * 經過反射, 得到Class定義中聲明的父類的泛型參數的類型. 如沒法找到, 返回Object.class.
     *
     * @param clazz class類
     *
     * @param index 獲取第幾個泛型參數的類型,默認從0開始,即第一個
     *
     * @return 返回第index個泛型參數類型.
     */
    public static Class getClassGenericType(final Class clazz, final int index) {
        Type genType = clazz.getGenericSuperclass();

        if (!(genType instanceof ParameterizedType)) {
            return Object.class;
        }

        Type[] params = ((ParameterizedType) genType).getActualTypeArguments();

        if (index >= params.length || index < 0) {
            logger.warn("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + params.length);
            return Object.class;
        }
        if (!(params[index] instanceof Class)) {
            logger.warn(clazz.getSimpleName() + " not set the actual class on superclass generic parameter");
            return Object.class;
        }

        return (Class) params[index];
    }

    /**
     * 根據註解類型獲取實體的Field
     *
     * @param entityClass 實體類型
     * 
     * @param annotationClass 註解類型
     *
     * @return 返回第一個有該註解類型的Field,若是沒有則返回null.
     */
    @SuppressWarnings("unchecked")
    public static Field getFieldByAnnotation(Class entityClass, Class annotationClass) {
        Field[] fields = entityClass.getDeclaredFields();
        for (Field field : fields) {
            if (field.getAnnotation(annotationClass) != null) {
                makeAccessible(field);
                return field;
            }
        }
        return null;
    }

    /**
     * 獲取實體的字段
     *
     * @param entityClass 實體類型
     *
     * @param fieldName 字段名稱
     *
     * @return 該字段名稱對應的字段,若是沒有則返回null.
     */
    public static Field getField(Class entityClass, String fieldName){
        try {
            Field field = entityClass.getDeclaredField(fieldName);
            makeAccessible(field);
            return field;
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 改變private/protected的成員變量爲public.
     */
    public static void makeAccessible(Field field) {
        if (!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())) {
            field.setAccessible(true);
        }
    }

}
View Code

七、獲取AOP代理

Spring 只要引入aop則是默認開啓事務的,通常咱們只要在須要事務管理的地方加上@Transactional註解便可支持事務,通常咱們會加在Service的類或者具體的增長、刪除、更改的方法上。

我這裏要說的是獲取代理的問題。Service的事務管理是AOP實現的,AOP的實現用的是JDK動態代理或CGLIB動態代理。因此,若是你想在你的代理方法中以 this 調用當前接口的另外一個方法,另外一個方法的事務是不會起做用的。由於事務的方法是代理對象的,而 this 是當前類對象,不是一個代理對象,天然事務就不會起做用了。這是我在不久前的開發中遇到的實際問題,我自定義了一個註解,加在方法上,使用AspectJ來攔截該註解,卻沒攔截到,緣由就是這個方法是被另外一個方法以 this 的方式調用的,因此AOP不能起做用。

更詳細的可參考:Spring AOP沒法攔截內部方法調用

因此添加一個獲取自身代理對象的接口,以方便獲取代理對象來操做當前類方法。Service接口只須要繼承該接口,T爲接口自己便可,就能夠經過self()獲取自身的代理對象了。

package com.lyyzoo.core.base;

import org.springframework.aop.framework.AopContext;

/**
 * 獲取代理對象自己.
 */
public interface ProxySelf<T> {
    /**
     * 取得當前對象的代理.
     * 
     * @return 代理對象,若是未被代理,則拋出 IllegalStateException
     */
    @SuppressWarnings("unchecked")
    default T self() {
        return (T) AopContext.currentProxy();
    }
}
View Code

還須要開啓開啓 exposeProxy = true,暴露代理對象,不然 AopContext.currentProxy() 會拋出異常。

八、數據持久化測試

① 實體映射

實體類按照以下規則和數據庫表進行轉換,註解所有是JPA中的註解:

  • 表名默認使用類名,駝峯轉下劃線(只對大寫字母進行處理),如UserInfo默認對應的表名爲user_info

  • 表名可使@Table(name = "tableName")進行指定,對不符合第一條默認規則的能夠經過這種方式指定表名。

  • 字段默認和@Column同樣,都會做爲表字段,表字段默認爲Java對象的Field名字駝峯轉下劃線形式。

  • 可使用@Column(name = "fieldName")指定不符合第3條規則的字段名。

  • 使用@Transient註解能夠忽略字段,添加該註解的字段不會做爲表字段使用,注意,若是沒有與表關聯,必定要用@Transient標註。

  • 建議必定是有一個@Id註解做爲主鍵的字段,能夠有多個@Id註解的字段做爲聯合主鍵。

  • 默認狀況下,實體類中若是不存在包含@Id註解的字段,全部的字段都會做爲主鍵字段進行使用(這種效率極低)。

  • 因爲基本類型,如int做爲實體類字段時會有默認值0,並且沒法消除,因此實體類中建議不要使用基本類型。

User實體主要加了@Table註解,映射表名;而後在userId上標註主鍵註解;其它字段若是沒加@Transient註解的默認都會做爲表字段。

package com.lyyzoo.core.system.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.lyyzoo.core.base.BaseDTO;
import com.lyyzoo.core.util.Dates;

import javax.persistence.*;
import java.util.Date;
import java.util.List;

/**
 * 系統用戶
 *
 * @name User
 * @version 1.0
 * @author bojiangzhou 2017-12-31
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@Table(name = "SYS_USER")
public class User extends BaseDTO {
    private static final long serialVersionUID = -7395431342743009038L;

    /**
     * 用戶ID
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @OrderBy("DESC")
    private Long userId;
    /**
     * 用戶名
     */
    private String username;
    /**
     * 密碼
     */
    private String password;
    /**
     * 暱稱
     */
    private String nickname;
    /**
     * 生日
     */
    @JsonFormat(pattern = Dates.Pattern.DATE)
    private Date birthday;
    /**
     * 性別:1-男/0-女
     */
    private Integer sex;
    /**
     * 是否啓用:1/0
     */
    private Integer enabled;


    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public Integer getSex() {
        return sex;
    }

    public void setSex(Integer sex) {
        this.sex = sex;
    }

    public Integer getEnabled() {
        return enabled;
    }

    public void setEnabled(Integer enabled) {
        this.enabled = enabled;
    }

}
View Code

② 建立表結構

CREATE TABLE `sys_user` (
  `USER_ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '表ID,主鍵,供其餘表作外鍵',
  `USERNAME` varchar(30) NOT NULL COMMENT '用戶名',
  `PASSWORD` varchar(100) NOT NULL COMMENT '密碼',
  `NICKNAME` varchar(30) NOT NULL COMMENT '用戶名稱',
  `BIRTHDAY` date DEFAULT NULL COMMENT '生日',
  `SEX` int(1) DEFAULT NULL COMMENT '性別:1-男;0-女',
  `ENABLED` int(1) NOT NULL DEFAULT '1' COMMENT '啓用標識:1/0',
  `VERSION_NUMBER` int(11) NOT NULL DEFAULT '1' COMMENT '行版本號,用來處理鎖',
  `CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  `CREATE_BY` bigint(11) NOT NULL DEFAULT '-1' COMMENT '建立人',
  `UPDATE_BY` bigint(11) NOT NULL DEFAULT '-1' COMMENT '更新人',
  `UPDATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新時間',
  `ATTRIBUTE1` varchar(150) DEFAULT NULL,
  `ATTRIBUTE2` varchar(150) DEFAULT NULL,
  `ATTRIBUTE3` varchar(150) DEFAULT NULL,
  `ATTRIBUTE4` varchar(150) DEFAULT NULL,
  `ATTRIBUTE5` varchar(150) DEFAULT NULL,
  `ATTRIBUTE6` varchar(150) DEFAULT NULL,
  `ATTRIBUTE7` varchar(150) DEFAULT NULL,
  `ATTRIBUTE8` varchar(150) DEFAULT NULL,
  `ATTRIBUTE9` varchar(150) DEFAULT NULL,
  `ATTRIBUTE10` varchar(150) DEFAULT NULL,
  PRIMARY KEY (`USER_ID`),
  UNIQUE KEY `USERNAME` (`USERNAME`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='系統用戶';
View Code

③ 建立UserMapper

在system.mapper下建立UserMapper接口,繼承Mapper<User>:

package com.lyyzoo.core.system.mapper;

import com.lyyzoo.core.base.Mapper;
import com.lyyzoo.core.system.dto.User;

/**
 *
 * @name UserMapper
 * @version 1.0
 * @author bojiangzhou 2018-01-06
 */
public interface UserMapper extends Mapper<User> {

}
View Code

④ 建立UserService

在system.service下建立UserService接口,只需繼承Service<User>接口便可。

package com.lyyzoo.core.system.service;

import com.lyyzoo.core.base.Service;
import com.lyyzoo.core.system.dto.User;

/**
 * 用戶Service接口
 *
 * @version 1.0
 * @author bojiangzhou 2018-01-06
 */
public interface UserService extends Service<User> {

}
View Code

在system.service.impl下建立UserServiceImpl實現類,繼承BaseService<User>類,實現UserService接口。同時加上@Service註解。

package com.lyyzoo.core.system.service.impl;

import org.springframework.stereotype.Service;

import com.lyyzoo.core.base.BaseService;
import com.lyyzoo.core.system.dto.User;
import com.lyyzoo.core.system.service.UserService;

/**
 * 用戶Service實現類
 *
 * @version 1.0
 * @author bojiangzhou 2018-01-06
 */
@Service
public class UserServiceImpl extends BaseService<User> implements UserService {

}
View Code

⑤ 修改UserController,注入UserService,增長一些測試API

package com.lyyzoo.core.system.controller;

import com.lyyzoo.core.base.BaseController;
import com.lyyzoo.core.base.BaseEnums;
import com.lyyzoo.core.base.Result;
import com.lyyzoo.core.system.dto.User;
import com.lyyzoo.core.system.service.UserService;
import com.lyyzoo.core.util.Results;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;

/**
 * 用戶Controller
 *
 * @version 1.0
 * @author bojiangzhou 2017-12-31
 */
@RequestMapping
@RestController
public class UserController extends BaseController {

    @Autowired
    private UserService userService;


    @PostMapping("/sys/user/queryAll")
    public Result queryAll(){
        List<User> list = userService.selectAll();
        return Results.successWithData(list, BaseEnums.SUCCESS.code(), BaseEnums.SUCCESS.description());
    }

    @RequestMapping("/sys/user/queryOne/{userId}")
    public Result queryOne(@PathVariable Long userId){
        User user = userService.get(userId);
        return Results.successWithData(user);
    }

    @PostMapping("/sys/user/save")
    public Result save(@Valid @RequestBody User user){
        user = userService.insertSelective(user);
        return Results.successWithData(user);
    }

    @PostMapping("/sys/user/update")
    public Result update(@Valid @RequestBody List<User> user){
        user = userService.persistSelective(user);
        return Results.successWithData(user);
    }

    @RequestMapping("/sys/user/delete")
    public Result delete(User user){
        userService.delete(user);
        return Results.success();
    }

    @RequestMapping("/sys/user/delete/{userId}")
    public Result delete(@PathVariable Long userId){
        userService.delete(userId);
        return Results.success();
    }

}
View Code

⑥ 測試結果

查詢全部:

批量保存/修改:

九、代碼生成器

使用代碼生成器來生成基礎的代碼結構,生成DTO、XML等等。

MyBatis官方提供了代碼生成器MyBatis Generator,但通常須要定製化。MyBatis Generator

我這裏從網上找了一個使用起來比較方便的界面工具,可生成DTO、Mapper、Mapper.xml,生成以後還需作一些小調整。另須要本身建立對應的Service、Controller。以後有時間再從新定製化一個符合本項目的代碼生成器。

 

4、日誌及全局異常處理

在前面的測試中,會發現控制檯輸出的日誌不怎麼友好,有不少日誌也沒有輸出,不便於查找排查問題。對於一個應用程序來講日誌記錄是必不可少的一部分。線上問題追蹤,基於日誌的業務邏輯統計分析等都離不日誌。

先貼出一些參考資料:

  logback 配置詳解

  日誌組件slf4j介紹及配置詳解

  Java經常使用日誌框架介紹

一、日誌框架簡介

Java有不少經常使用的日誌框架,如Log4j、Log4j 二、Commons Logging、Slf4j、Logback等。有時候你可能會感受有點混亂,下面簡單介紹下。

  • Log4j:Apache Log4j是一個基於Java的日誌記錄工具,是Apache軟件基金會的一個項目。

  • Log4j 2:Apache Log4j 2是apache開發的一款Log4j的升級產品。

  • Commons Logging:Apache基金會所屬的項目,是一套Java日誌接口。

  • Slf4j:相似於Commons Logging,是一套簡易Java日誌門面,自己並沒有日誌的實現。(Simple Logging Facade for Java,縮寫Slf4j)。

  • Logback:一套日誌組件的實現(slf4j陣營)。

Commons Logging和Slf4j是日誌門面,提供一個統一的高層接口,爲各類loging API提供一個簡單統一的接口。log4j和Logback則是具體的日誌實現方案。能夠簡單的理解爲接口與接口的實現,調用者只須要關注接口而無需關注具體的實現,作到解耦。

比較經常使用的組合使用方式是Slf4j與Logback組合使用,Commons Logging與Log4j組合使用。

基於下面的一些優勢,選用Slf4j+Logback的日誌框架:

  • 更快的執行速度,Logback重寫了內部的實現,在一些關鍵執行路徑上性能提高10倍以上。並且logback不只性能提高了,初始化內存加載也更小了

  • 自動清除舊的日誌歸檔文件,經過設置TimeBasedRollingPolicy 或者 SizeAndTimeBasedFNATP的 maxHistory 屬性,你就能夠控制日誌歸檔文件的最大數量

  • Logback擁有遠比log4j更豐富的過濾能力,能夠不用下降日誌級別而記錄低級別中的日誌。

  • Logback必須配合Slf4j使用。因爲Logback和Slf4j是同一個做者,其兼容性不言而喻。

  • 默認狀況下,Spring Boot會用Logback來記錄日誌,並用INFO級別輸出到控制檯。

二、配置日誌

能夠看到,只要集成了spring-boot-starter-web,就引入了spring-boot-starter-logging,即slf4j和logback。

其它的幾個包:jcl-over-slf4j,代碼直接調用common-logging會被橋接到slf4j;jul-to-slf4j,代碼直接調用java.util.logging會被橋接到slf4j;log4j-over-slf4j,代碼直接調用log4j會被橋接到slf4j。

還需引入janino,若是不加入這個包會報錯。

在resources下添加logback.xml配置文件,Logback默認會查找classpath下的logback.xml文件。

具體配置以下,有較詳細的註釋,很容易看懂。能夠經過application.properties配置日誌記錄級別、日誌輸出文件目錄等。

<?xml version="1.0" encoding="UTF-8"?>

<!-- 級別從高到低 OFF 、 FATAL 、 ERROR 、 WARN 、 INFO 、 DEBUG 、 TRACE 、 ALL -->
<!-- 日誌輸出規則 根據當前ROOT 級別,日誌輸出時,級別高於root默認的級別時 會輸出 -->
<!-- 如下 每一個配置的 filter 是過濾掉輸出文件裏面,會出現高級別文件,依然出現低級別的日誌信息,經過filter 過濾只記錄本級別的日誌 -->
<!-- scan 當此屬性設置爲true時,配置文件若是發生改變,將會被從新加載,默認值爲true。 -->
<!-- scanPeriod 設置監測配置文件是否有修改的時間間隔,若是沒有給出時間單位,默認單位是毫秒。當scan爲true時,此屬性生效。默認的時間間隔爲1分鐘。 -->
<!-- debug 當此屬性設置爲true時,將打印出logback內部日誌信息,實時查看logback運行狀態。默認值爲false。 -->
<configuration debug="false" scan="false" scanPeriod="5 minutes">

    <!-- 引入配置文件 -->
    <property resource="application.properties"/>
    <property resource="application-${app.env:-dev}.properties"/>

    <property name="app.name" value="${app.name:-sunny}"/>
    <property name="app.env" value="${app.env:-dev}"/>

    <!-- 日誌記錄級別 -->
    <property name="logback_level" value="${logback.level:-DEBUG}"/>
    <!-- 是否輸出日誌到文件 -->
    <property name="logback_rolling" value="${logback.rolling:-false}"/>
    <!-- 設置日誌輸出目錄 -->
    <property name="logback_rolling_path" value="${logback.rolling.path:-/data/logs}"/>
    <!-- 日誌文件最大大小 -->
    <property name="logback_max_file_size" value="${logback.max_file_size:-10MB}"/>
    <!-- 格式化輸出:%d:表示日期,%thread:表示線程名,%-5level:級別從左顯示5個字符寬度,%logger:日誌輸出者的名字(一般是所在類的全名),%L:輸出代碼中的行號,%msg:日誌消息,%n:換行符 -->
    <property name="logback_pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger %L  -| %msg%n"/>


    <if condition='p("logback_rolling").equals("true")'>
        <then>
            <!-- 滾動記錄文件 -->
            <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
                <file>${logback_rolling_path}/${app.name}.log</file>
                <!-- rollingPolicy:當發生滾動時,決定RollingFileAppender的行爲,涉及文件移動和重命名 -->
                <!-- TimeBasedRollingPolicy:最經常使用的滾動策略,它根據時間來制定滾動策略 -->
                <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                    <!-- 活動文件的名字會根據fileNamePattern的值,每隔一段時間改變一次 -->
                    <fileNamePattern>${logback_rolling_path}/${app.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>

                    <!-- 日誌文件的保存期限爲30天 -->
                    <maxHistory>30</maxHistory>

                    <timeBasedFileNamingAndTriggeringPolicy  class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                        <!-- maxFileSize:這是活動文件的大小,默認值是10MB -->
                        <maxFileSize>${logback_max_file_size}</maxFileSize>
                    </timeBasedFileNamingAndTriggeringPolicy>
                </rollingPolicy>
                <encoder>
                    <pattern>${logback_pattern}</pattern>
                    <charset>UTF-8</charset>
                </encoder>
            </appender>

            <root>
                <appender-ref ref="FILE"/>
            </root>
        </then>
    </if>


    <!-- 將日誌打印到控制檯 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${logback_pattern}</pattern>
        </encoder>
    </appender>

    <root level="${logback_level}">
        <appender-ref ref="CONSOLE"/>
    </root>

    <contextName>${app.name}</contextName>

</configuration>
View Code

加入配置文件後,就能夠看到控制檯格式化後的日誌輸出,還能夠看到具體代碼行數等,比以前的友好多了。

同時,將日誌滾動輸出到日誌文件,保留歷史記錄。可經過logback.rolling=false控制是否須要輸出日誌到文件。

三、使用Logger

配置好以後,就可使用Logger來輸出日誌了,使用起來也是很是方便。

* 能夠看到引入的包是slf4j.Logger,代碼裏並無引用任何一個跟 Logback 相關的類,這即是使用 Slf4j的好處,在須要將日誌框架切換爲其它日誌框架時,無需改動已有的代碼。

* LoggerFactory 的 getLogger() 方法接收一個參數,以這個參數決定 logger 的名字,好比第二圖中的日誌輸出。在爲 logger 命名時,用類的全限定類名做爲 logger name 是最好的策略,這樣可以追蹤到每一條日誌消息的來源

* 能夠看到,能夠經過提供佔位符,以參數化的方式打印日誌,避免字符串拼接的沒必要要損耗,也無需經過logger.isDebugEnabled()這種方式判斷是否須要打印。

四、全局異常處理

如今有一個問題,當日志級別設置到INFO級別後,只會輸出INFO以上的日誌,如INFO、WARN、ERROR,這沒毛病,問題是,程序中拋出的異常堆棧(運行時異常)都沒有打印了,不利於排查問題。

並且,在某些狀況下,咱們在Service中想直接把異常往Controller拋出不作處理,但咱們不能直接把異常信息輸出到客戶端,這是很是不友好的。

因此,在config下建一個GlobalExceptionConfig做爲全局統一異常處理。主要處理了自定義的ServiceException、AuthorityException、BaseException,以及系統的NoHandlerFoundException和Exception異常。

package com.lyyzoo.core.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import com.lyyzoo.core.base.Result;
import com.lyyzoo.core.constants.BaseEnums;
import com.lyyzoo.core.exception.AuthorityException;
import com.lyyzoo.core.exception.BaseException;
import com.lyyzoo.core.exception.ServiceException;
import com.lyyzoo.core.util.Results;

/**
 * 全局異常處理
 *
 * @author bojiangzhou 2018-02-06
 * @version 1.0
 */
@RestControllerAdvice
public class GlobalExceptionConfig {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionConfig.class);

    /**
     * 處理 ServiceException 異常
     */
    @ExceptionHandler(ServiceException.class)
    public Result handleServiceException(ServiceException e){
        Result result = Results.failure(e.getCode(), e.getMessage());
        result.setStatus(HttpStatus.BAD_REQUEST.value());
        logger.info("ServiceException[code: {}, message: {}]", e.getCode(), e.getMessage());
        return result;
    }

    /**
     * 處理 AuthorityException 異常
     */
    @ExceptionHandler(AuthorityException.class)
    public Result handleAuthorityException(AuthorityException e){
        Result result = Results.failure(BaseEnums.FORBIDDEN.code(), BaseEnums.FORBIDDEN.desc());
        result.setStatus(HttpStatus.FORBIDDEN.value());
        logger.info("AuthorityException[code: {}, message: {}]", e.getCode(), e.getMessage());
        return result;
    }

    /**
     * 處理 NoHandlerFoundException 異常. <br/>
     * 需配置 [spring.mvc.throw-exception-if-no-handler-found=true]
     * 需配置 [spring.resources.add-mappings=false]
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public Result handleNotFoundException(NoHandlerFoundException e){
        Result result = Results.failure(BaseEnums.NOT_FOUND.code(), BaseEnums.NOT_FOUND.desc());
        result.setStatus(HttpStatus.NOT_FOUND.value());
        logger.info(e.getMessage());
        return result;
    }

    /**
     * 處理 BaseException 異常
     */
    @ExceptionHandler(BaseException.class)
    public Result handleBaseException(BaseException e){
        Result result = Results.failure(e.getCode(), e.getMessage());
        result.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        logger.error("BaseException[code: {}, message: {}]", e.getCode(), e.getMessage(), e);
        return result;
    }

    /**
     * 處理 Exception 異常
     */
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e){
        Result result = Results.failure(BaseEnums.ERROR.code(), BaseEnums.ERROR.desc());
        result.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        logger.error(e.getMessage(), e);
        return result;
    }

}
View Code

看上面的代碼,@ControllAdvice(@RestControllerAdvice能夠返回ResponseBody),可看作Controller加強器,能夠在@ControllerAdvice做用類下添加@ExceptionHandler,@InitBinder,@ModelAttribute註解的方法來加強Controller,都會做用在被 @RequestMapping 註解的方法上。

使用@ExceptionHandler 攔截異常,咱們能夠經過該註解實現自定義異常處理。在每一個處理方法中,封裝Result,返回對應的消息及狀態碼等。

經過Logger打印對應級別的日誌,也能夠看到控制檯及日誌文件中有異常堆棧的輸出了。注意除了BaseException、Exception,其它的都只是打印了簡單信息,且爲INFO級別。Exception是ERROR級別,且打印了堆棧信息。

NoHandlerFoundException 是404異常,這裏注意要先關閉DispatcherServlet的NotFound默認異常處理。

測試以下:這種返回結果就比較友好了。

    

5、數據庫樂觀鎖

一、樂觀鎖

在併發修改同一條記錄時,爲避免更新丟失,須要加鎖。要麼在應用層加鎖,要麼在緩存層加鎖,要麼在數據庫層使用樂觀鎖,使用version做爲更新依據【強制】。 —— 《阿里巴巴Java開發手冊》

樂觀鎖,基於數據版本(version)記錄機制實現,爲數據庫表增長一個"version"字段。讀取出數據時,將此版本號一同讀出,以後更新時,對此版本號加一。提交數據時,提交的版本數據與數據庫表對應記錄的當前版本信息進行比對,若是提交的數據版本號大於數據庫表當前版本號,則予以更新,不然認爲是過時數據。

所以,這節就來處理BaseDTO中的"version"字段,經過增長一個mybatis插件來實現更新時版本號自動+1。

二、MyBatis插件介紹

MyBatis 容許在己映射語句執行過程當中的某一點進行攔截調用。默認狀況下, MyBatis 容許使用插件來攔截的接口和方法包括如下幾個:

  • Executor (update 、query 、flushStatements 、commit 、rollback getTransaction 、close 、isClosed)

  • ParameterHandler (getParameterObject 、setParameters)

  • ResultSetHandler (handleResul tSets 、handleCursorResultSets、handleOutputParameters)

  • StatementHandler (prepare 、parameterize 、batch update 、query) 

MyBatis 插件實現攔截器接口Interceptor,在實現類中對攔截對象和方法進行處理 。 

  • setProperties:傳遞插件的參數,能夠經過參數來改變插件的行爲。

  • plugin:參數 target 就是要攔截的對象,做用就是給被攔截對象生成一個代理對象,並返回。

  • intercept:會覆蓋所攔截對象的原方法,Invocation參數能夠反射調度原來對象的方法,能夠獲取到不少有用的東西。

除了須要實現攔截器接口外還須要給實現類配置攔截器簽名。 使用 @Intercepts 和 @Signature 這兩個註解來配置攔截器要攔截的接口的方法,接口方法對應的簽名基本都是固定的。

@Intercepts 註解的屬性是一個 @Signature  數組,能夠在同 個攔截器中同時攔截不一樣的接口和方法。

@Signature 註解包含如下三個屬性。

  • type:設置攔截接口,可選值是前面提到的4個接口 

  • method:設置攔截接口中的方法名, 可選值是前面4個接口對應的方法,須要和接口匹配 

  • args:設置攔截方法的參數類型數組,經過方法名和參數類型能夠肯定惟一一個方法 。

三、數據版本插件

要實現版本號自動更新,咱們須要在SQL被執行前修改SQL,所以咱們須要攔截的就是 StatementHandler  接口的 prepare 方法,該方法會在數據庫執行前被調用,優先於當前接口的其它方法而被執行。

在 core.plugin 包下新建一個VersionPlugin插件,實現Interceptor攔截器接口。

該接口方法簽名以下:

在 interceptor 方法中對 UPDATE 類型的操做,修改原SQL,加入version,修改後的SQL相似下圖,更新時就會自動將version+1。同時帶上version條件,若是該版本號小於數據庫記錄版本號,則不會更新。

VersionInterceptor插件:

package com.lyyzoo.core.plugins;

import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.operators.arithmetic.Addition;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.util.List;
import java.util.Properties;

/**
 * 樂觀鎖:數據版本插件
 *
 * @version 1.0
 * @author bojiangzhou 2018-02-10
 */
@Intercepts(
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
)
public class VersionInterceptor implements Interceptor {

    private static final String VERSION_COLUMN_NAME = "version";

    private static final Logger logger = LoggerFactory.getLogger(VersionInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 獲取 StatementHandler,實際是 RoutingStatementHandler
        StatementHandler handler = (StatementHandler) processTarget(invocation.getTarget());
        // 包裝原始對象,便於獲取和設置屬性
        MetaObject metaObject = SystemMetaObject.forObject(handler);
        // MappedStatement 是對SQL更高層次的一個封裝,這個對象包含了執行SQL所需的各類配置信息
        MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        // SQL類型
        SqlCommandType sqlType = ms.getSqlCommandType();
        if(sqlType != SqlCommandType.UPDATE) {
            return invocation.proceed();
        }
        // 獲取版本號
        Object originalVersion = metaObject.getValue("delegate.boundSql.parameterObject." + VERSION_COLUMN_NAME);
        if(originalVersion == null || Long.valueOf(originalVersion.toString()) <= 0){
            return invocation.proceed();
        }
        // 獲取綁定的SQL
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        // 原始SQL
        String originalSql = boundSql.getSql();
        // 加入version的SQL
        originalSql = addVersionToSql(originalSql, originalVersion);
        // 修改 BoundSql
        metaObject.setValue("delegate.boundSql.sql", originalSql);

        // proceed() 能夠執行被攔截對象真正的方法,該方法實際上執行了method.invoke(target, args)方法
        return invocation.proceed();
    }

    /**
     * Plugin.wrap 方法會自動判斷攔截器的簽名和被攔截對象的接口是否匹配,只有匹配的狀況下才會使用動態代理攔截目標對象.
     *
     * @param target 被攔截的對象
     * @return 代理對象
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 設置參數
     */
    @Override
    public void setProperties(Properties properties) {

    }

    /**
     * 獲取代理的原始對象
     *
     * @param target
     * @return
     */
    private static Object processTarget(Object target) {
        if(Proxy.isProxyClass(target.getClass())) {
            MetaObject mo = SystemMetaObject.forObject(target);
            return processTarget(mo.getValue("h.target"));
        }
        return target;
    }

    /**
     * 爲原SQL添加version
     *
     * @param originalSql 原SQL
     * @param originalVersion 原版本號
     * @return 加入version的SQL
     */
    private String addVersionToSql(String originalSql, Object originalVersion){
        try{
            Statement stmt = CCJSqlParserUtil.parse(originalSql);
            if(!(stmt instanceof Update)){
                return originalSql;
            }
            Update update = (Update)stmt;
            if(!contains(update)){
                buildVersionExpression(update);
            }
            Expression where = update.getWhere();
            if(where != null){
                AndExpression and = new AndExpression(where, buildVersionEquals(originalVersion));
                update.setWhere(and);
            }else{
                update.setWhere(buildVersionEquals(originalVersion));
            }
            return stmt.toString();
        }catch(Exception e){
            logger.error(e.getMessage(), e);
            return originalSql;
        }
    }

    private boolean contains(Update update){
        List<Column> columns = update.getColumns();
        for(Column column : columns){
            if(column.getColumnName().equalsIgnoreCase(VERSION_COLUMN_NAME)){
                return true;
            }
        }
        return false;
    }

    private void buildVersionExpression(Update update){
        // 列 version
        Column versionColumn = new Column();
        versionColumn.setColumnName(VERSION_COLUMN_NAME);
        update.getColumns().add(versionColumn);

        // 值 version+1
        Addition add = new Addition();
        add.setLeftExpression(versionColumn);
        add.setRightExpression(new LongValue(1));
        update.getExpressions().add(add);
    }

    private Expression buildVersionEquals(Object originalVersion){
        Column column = new Column();
        column.setColumnName(VERSION_COLUMN_NAME);

        // 條件 version = originalVersion
        EqualsTo equal = new EqualsTo();
        equal.setLeftExpression(column);
        equal.setRightExpression(new LongValue(originalVersion.toString()));
        return equal;
    }

}
View Code

以後還需配置該插件,只須要在MyBatisConfig中加入該配置便可。

最後,若是版本不匹配,更新失敗,須要往外拋出異常提醒,因此修改BaseService的update方法,增長檢查更新是否失敗。

最後,能不用插件儘可能不要用插件,由於它將修改MyBatis的底層設計。插件生成的是層層代理對象的責任鏈模式,經過反射方法運行,會有必定的性能消耗。

咱們也能夠修改 tk.mapper 生成SQL的方法,加入version,這裏經過插件方式實現樂觀鎖主要是不爲了去修改 mapper 的底層源碼,比較方便。

6、Druid數據庫鏈接池

建立數據庫鏈接是一個很耗時的操做,也很容易對數據庫形成安全隱患。對數據庫鏈接的管理能顯著影響到整個應用程序的伸縮性和健壯性,影響程序的性能指標。

數據庫鏈接池負責分配、管理和釋放數據庫鏈接,它容許應用程序重複使用一個現有的數據庫鏈接,而不是再從新創建一個;釋放空閒時間超過最大空閒時間的數據庫鏈接來避免由於沒有釋放數據庫鏈接而引發的數據庫鏈接遺漏。數據庫鏈接池能明顯提升對數據庫操做的性能。

參考:

  Druid常見問題集錦

  經常使用數據庫鏈接池 (DBCP、c3p0、Druid) 配置說明

 

一、Druid

Druid首先是一個數據庫鏈接池,但它不只僅是一個數據庫鏈接池,它還包含一個ProxyDriver,一系列內置的JDBC組件庫,一個SQLParser。Druid支持全部JDBC兼容的數據庫,包括Oracle、MySql、Derby、Postgresql、SQLServer、H2等等。 Druid針對Oracle和MySql作了特別優化,好比Oracle的PSCache內存佔用優化,MySql的ping檢測優化。Druid在監控、可擴展性、穩定性和性能方面都有明顯的優點。Druid提供了Filter-Chain模式的擴展API,能夠本身編寫Filter攔截JDBC中的任何方法,能夠在上面作任何事情,好比說性能監控、SQL審計、用戶名密碼加密、日誌等等。

二、配置

Druid配置到core模塊下,只需在application.properties中添加以下配置便可,大部分配置是默認配置,可更改。有詳細的註釋,比較容易理解。

####################################
# Druid
####################################
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

# 初始化鏈接大小[0]
spring.datasource.druid.initial-size=1
# 最小空閒鏈接數[0]
spring.datasource.druid.min-idle=1
# 最大鏈接數[8]
spring.datasource.druid.max-active=20

# 配置獲取鏈接等待超時的時間(毫秒)[-1]
spring.datasource.druid.max-wait=60000
# 查詢超時時間(秒)
spring.datasource.druid.query-timeout=90

# 用來檢測鏈接是否有效的sql,要求是一個查詢語句
spring.datasource.druid.validation-query=SELECT 'x'
# 申請鏈接時檢測鏈接可用性[false]
spring.datasource.druid.test-on-borrow=false
# 歸還鏈接檢測[false]
spring.datasource.druid.test-on-return=false
# 超時是否檢測鏈接可用性[true]
spring.datasource.druid.test-while-idle=true

# 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接 (毫秒)
spring.datasource.druid.time-between-eviction-runs-millis=60000
#  配置一個鏈接在池中最小生存的時間(毫秒,默認30分鐘)
spring.datasource.druid.min-evictable-idle-time-millis=300000
# 經過別名的方式配置擴展插件,經常使用的插件有:監控統計用的filter:stat;日誌用的filter:log4j;防護sql注入的filter:wall
spring.datasource.druid.filters=stat,wall,slf4j
# 合併多個DruidDataSource的監控數據
spring.datasource.druid.use-global-data-source-stat=true

# 是否緩存PreparedStatement. PSCache對支持遊標的數據庫性能提高巨大,好比說oracle.在mysql下建議關閉.
spring.datasource.druid.pool-prepared-statements=false
# 每一個鏈接上PSCache的大小
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20

# StatViewServlet [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatViewServlet%E9%85%8D%E7%BD%AE]
spring.datasource.druid.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
# 監控頁面的用戶名和密碼
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=admin
spring.datasource.druid.stat-view-servlet.reset-enable=false

# StatFilter [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatFilter]
spring.datasource.druid.filter.stat.db-type=mysql
#慢SQL記錄
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=2000
# SQL合併
spring.datasource.druid.filter.stat.merge-sql=false

# WallFilter [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE-wallfilter]
spring.datasource.druid.filter.wall.enabled=true
spring.datasource.druid.filter.wall.db-type=mysql
spring.datasource.druid.filter.wall.config.delete-allow=false
spring.datasource.druid.filter.wall.config.drop-table-allow=false
View Code

以後啓動項目在地址欄輸入/druid/index.html並登陸就能夠看到Druid監控頁面:

7、Redis緩存

對於現在的一箇中小型系統來講,至少也須要一個緩存來緩存熱點數據,加快數據的訪問數據,這裏選用Redis作緩存數據庫。在之後可使用Redis作分佈式緩存、作Session共享等。

一、SpringBoot的緩存支持

Spring定義了org.springframework.cache.CacheManager和org.springframework.cache.Cache接口來統一不一樣的緩存技術。CacheManager是Spring提供的各類緩存技術抽象接口,Cache接口包含緩存的各類操做。

針對不一樣的緩存技術,須要實現不一樣的CacheManager,Redis緩存則提供了RedisCacheManager的實現。

我將redis緩存功能放到sunny-starter-cache模塊下,cache模塊下能夠有多種緩存技術,同時,對於其它項目來講,緩存是可插拔的,想用緩存直接引入cache模塊便可。

首先引入Redis的依賴:

SpringBoot已經默認爲咱們自動配置了多個CacheManager的實現,在autoconfigure.cache包下。在Spring Boot 環境下,使用緩存技術只需在項目中導入相關的依賴包便可。

在 RedisCacheConfiguration 裏配置了默認的 CacheManager;SpringBoot提供了默認的redis配置,RedisAutoConfiguration 是Redis的自動化配置,好比建立鏈接池、初始化RedisTemplate等。

二、Redis 配置及聲明式緩存支持

Redis 默認配置了 RedisTemplate 和 StringRedisTemplate ,其使用的序列化規則是 JdkSerializationRedisSerializer,緩存到redis後,數據都變成了下面這種樣式,很是不易於閱讀。

所以,從新配置RedisTemplate,使用 Jackson2JsonRedisSerializer 來序列化 Key 和 Value。同時,增長HashOperations、ValueOperations等Redis數據結構相關的操做,這樣比較方便使用。

package com.lyyzoo.cache.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * Redis配置.
 *
 * 使用@EnableCaching開啓聲明式緩存支持. 以後就可使用 @Cacheable/@CachePut/@CacheEvict 註解緩存數據.
 *
 * @author bojiangzhou 2018-02-11
 * @version 1.0
 */
@Configuration
@EnableCaching
public class RedisConfig {
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    @Autowired
    private Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder;

    /**
     * 覆蓋默認配置 RedisTemplate,使用 String 類型做爲key,設置key/value的序列化規則
     */
    @Bean
    @SuppressWarnings("unchecked")
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 使用 Jackson2JsonRedisSerialize 替換默認序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = jackson2ObjectMapperBuilder.createXmlMapper(false).build();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 設置value的序列化規則和key的序列化規則
        redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

    @Bean
    public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForHash();
    }

    @Bean
    public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
        return redisTemplate.opsForValue();
    }

    @Bean
    public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForList();
    }

    @Bean
    public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForSet();
    }

    @Bean
    public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForZSet();
    }

    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate());
        cacheManager.setUsePrefix(true);
        return cacheManager;
    }

}
View Code

同時,使用@EnableCaching開啓聲明式緩存支持,這樣就可使用基於註解的緩存技術。註解緩存是一個對緩存使用的抽象,經過在代碼中添加下面的一些註解,達到緩存的效果。

  • @Cacheable:在方法執行前Spring先查看緩存中是否有數據,若是有數據,則直接返回緩存數據;沒有則調用方法並將方法返回值放進緩存。

  • @CachePut:將方法的返回值放到緩存中。

  • @CacheEvict:刪除緩存中的數據。

 

Redis服務器相關的一些配置可在application.properties中進行配置:

三、Redis工具類

添加一個Redis的統一操做工具,主要是對redis的經常使用數據類型操做類作了一個歸集。

ValueOperations用於操做String類型,HashOperations用於操做hash數據,ListOperations操做List集合,SetOperations操做Set集合,ZSetOperations操做有序集合。

關於redis的key命令和數據類型可參考個人學習筆記:

Redis 學習(一) —— 安裝、通用key操做命令

Redis 學習(二) —— 數據類型及操做

package com.lyyzoo.cache.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Redis 操做工具
 *
 * @version 1.0
 * @author bojiangzhou 2018-02-12
 */
@Component
public class RedisOperator {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private ValueOperations<String, String> valueOperator;
    @Autowired
    private HashOperations<String, String, Object> hashOperator;
    @Autowired
    private ListOperations<String, Object> listOperator;
    @Autowired
    private SetOperations<String, Object> setOperator;
    @Autowired
    private ZSetOperations<String, Object> zSetOperator;

    /**
     * 默認過時時長,單位:秒
     */
    public final static long DEFAULT_EXPIRE = 60 * 60 * 24;

    /** 不設置過時時長 */
    public final static long NOT_EXPIRE = -1;

    /**
     * Redis的根操做路徑
     */
    @Value("${redis.root:sunny}")
    private String category;

    public RedisOperator setCategory(String category) {
        this.category = category;
        return this;
    }

    /**
     * 獲取Key的全路徑
     * 
     * @param key key
     * @return full key
     */
    public String getFullKey(String key) {
        return this.category + ":" + key;
    }


    //
    // key
    // ------------------------------------------------------------------------------
    /**
     * 判斷key是否存在
     *
     * <p>
     * <i>exists key</i>
     *
     * @param key key
     */
    public boolean existsKey(String key) {
        return redisTemplate.hasKey(getFullKey(key));
    }

    /**
     * 判斷key存儲的值類型
     *
     * <p>
     * <i>type key</i>
     *
     * @param key key
     * @return DataType[string、list、set、zset、hash]
     */
    public DataType typeKey(String key){
        return redisTemplate.type(getFullKey(key));
    }

    /**
     * 重命名key. 若是newKey已經存在,則newKey的原值被覆蓋
     *
     * <p>
     * <i>rename oldKey newKey</i>
     *
     * @param oldKey oldKeys
     * @param newKey newKey
     */
    public void renameKey(String oldKey, String newKey){
        redisTemplate.rename(getFullKey(oldKey), getFullKey(newKey));
    }

    /**
     * newKey不存在時才重命名.
     *
     * <p>
     * <i>renamenx oldKey newKey</i>
     *
     * @param oldKey oldKey
     * @param newKey newKey
     * @return 修改爲功返回true
     */
    public boolean renameKeyNx(String oldKey, String newKey){
        return redisTemplate.renameIfAbsent(getFullKey(oldKey), getFullKey(newKey));
    }

    /**
     * 刪除key
     *
     * <p>
     * <i>del key</i>
     *
     * @param key key
     */
    public void deleteKey(String key){
        redisTemplate.delete(key);
    }

    /**
     * 刪除key
     *
     * <p>
     * <i>del key1 key2 ...</i>
     *
     * @param keys 可傳入多個key
     */
    public void deleteKey(String ... keys){
        Set<String> ks = Stream.of(keys).map(k -> getFullKey(k)).collect(Collectors.toSet());
        redisTemplate.delete(ks);
    }

    /**
     * 刪除key
     *
     * <p>
     * <i>del key1 key2 ...</i>
     *
     * @param keys key集合
     */
    public void deleteKey(Collection<String> keys){
        Set<String> ks = keys.stream().map(k -> getFullKey(k)).collect(Collectors.toSet());
        redisTemplate.delete(ks);
    }

    /**
     * 設置key的生命週期,單位秒
     *
     * <p>
     * <i>expire key seconds</i><br>
     * <i>pexpire key milliseconds</i>
     *
     * @param key key
     * @param time 時間數
     * @param timeUnit TimeUnit 時間單位
     */
    public void expireKey(String key, long time, TimeUnit timeUnit){
        redisTemplate.expire(key, time, timeUnit);
    }

    /**
     * 設置key在指定的日期過時
     *
     * <p>
     * <i>expireat key timestamp</i>
     *
     * @param key key
     * @param date 指定日期
     */
    public void expireKeyAt(String key, Date date){
        redisTemplate.expireAt(key, date);
    }

    /**
     * 查詢key的生命週期
     *
     * <p>
     * <i>ttl key</i>
     *
     * @param key key
     * @param timeUnit TimeUnit 時間單位
     * @return 指定時間單位的時間數
     */
    public long getKeyExpire(String key, TimeUnit timeUnit){
        return redisTemplate.getExpire(key, timeUnit);
    }

    /**
     * 將key設置爲永久有效
     *
     * <p>
     * <i>persist key</i>
     *
     * @param key key
     */
    public void persistKey(String key){
        redisTemplate.persist(key);
    }


    /**
     *
     * @return RedisTemplate
     */
    public RedisTemplate<String, Object> getRedisTemplate() {
        return redisTemplate;
    }

    /**
     *
     * @return ValueOperations
     */
    public ValueOperations<String, String> getValueOperator() {
        return valueOperator;
    }

    /**
     *
     * @return HashOperations
     */
    public HashOperations<String, String, Object> getHashOperator() {
        return hashOperator;
    }

    /**
     *
     * @return ListOperations
     */
    public ListOperations<String, Object> getListOperator() {
        return listOperator;
    }

    /**
     *
     * @return SetOperations
     */
    public SetOperations<String, Object> getSetOperator() {
        return setOperator;
    }

    /**
     *
     * @return ZSetOperations
     */
    public ZSetOperations<String, Object> getZSetOperator() {
        return zSetOperator;
    }

}
View Code

8、Swagger支持API文檔

一、Swagger

作先後端分離,前端和後端的惟一聯繫,變成了API接口;API文檔變成了先後端開發人員聯繫的紐帶,變得愈來愈重要,swagger就是一款讓你更好的書寫API文檔的框架。

Swagger是一個簡單又強大的能爲你的Restful風格的Api生成文檔的工具。在項目中集成這個工具,根據咱們本身的配置信息可以自動爲咱們生成一個api文檔展現頁,能夠在瀏覽器中直接訪問查看項目中的接口信息,同時也能夠測試每一個api接口。

二、配置

我這裏直接使用別人已經整合好的swagger-spring-boot-starter,快速方便。

參考:spring-boot-starter-swagger

新建一個sunny-starter-swagger模塊,作到可插拔。

根據文檔,通常只須要作些簡單的配置便可:

但若是想要顯示swagger-ui.html文檔展現頁,還必須注入swagger資源:

package com.lyyzoo.swagger.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import com.spring4all.swagger.EnableSwagger2Doc;

/**
 * @version 1.0
 * @author bojiangzhou 2018-02-19
 */
@Configuration
@EnableSwagger2Doc
@PropertySource(value = "classpath:application-swagger.properties")
public class SunnySwaggerConfig extends WebMvcConfigurerAdapter {
    /**
     * 注入swagger資源文件
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

}
View Code

三、使用

通常只須要在Controller加上swagger的註解便可顯示對應的文檔信息,如@Api、@ApiOperation、@ApiParam等。

經常使用註解參考:swagger-api-annotations

package com.lyyzoo.admin.system.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import com.lyyzoo.admin.system.dto.Menu;
import com.lyyzoo.admin.system.service.MenuService;
import com.lyyzoo.core.base.BaseController;
import com.lyyzoo.core.base.Result;
import com.lyyzoo.core.util.Results;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;

@Api(tags = "菜單管理")
@RequestMapping
@RestController
public class MenuController extends BaseController {

    @Autowired
    private MenuService service;

    /**
     * 查找單個用戶
     *
     * @param menuId 菜單ID
     * @return Result
     */
    @ApiOperation("查找單個用戶")
    @ApiImplicitParam(name = "menuId", value = "菜單ID", paramType = "path")
    @GetMapping("/sys/menu/get/{menuId}")
    public Result get(@PathVariable Long menuId){
        Menu menu = service.selectById(menuId);
        return Results.successWithData(menu);
    }

    /**
     * 保存菜單
     *
     * @param menu 菜單
     * @return Result
     */
    @ApiOperation("保存菜單")
    @PostMapping("/sys/menu/save")
    public Result save(@ApiParam(name = "menu", value = "菜單")@RequestBody Menu menu){
        menu = service.save(menu);
        return Results.successWithData(menu);
    }

    /**
     * 刪除菜單
     *
     * @param menuId 菜單ID
     * @return Result
     */
    @ApiOperation("刪除菜單")
    @ApiImplicitParam(name = "menuId", value = "菜單ID", paramType = "path")
    @PostMapping("/sys/menu/delete/{menuId}")
    public Result delete(@PathVariable Long menuId){
        service.deleteById(menuId);
        return Results.success();
    }

}
View Code

 

以後訪問swagger-ui.html頁面就能夠看到API文檔信息了。

若是不須要swagger,在配置文件中配置swagger.enabled=false,或移除sunny-starter-swagger的依賴便可。

9、項目優化調整

到這裏,項目最基礎的一些功能就算完成了,但因爲前期的一些設計不合理及未考慮周全等因素,對項目作一些調整。並參考《阿里巴巴Java開發手冊》對代碼作了一些優化。

一、項目結構

目前項目分爲5個模塊:

最外層的Sunny做爲聚合模塊負責管理全部子模塊,方便統一構建。而且繼承 spring-boot-starter-parent ,其它子模塊則繼承該模塊,方便統一管理 Spring Boot 及本項目的版本。這裏已經把Spring Boot的版本升到 1.5.10.RELEASE。

<?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.lyyzoo</groupId>
    <artifactId>sunny</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>

    <name>Sunny</name>
    <description>Lyyzoo Base Application development platform</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.10.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>

        <sunny.version>0.0.1-SNAPSHOT</sunny.version>
        <springboot.version>1.5.10.RELEASE</springboot.version>
    </properties>

    <modules>
        <module>sunny-starter</module>
        <module>sunny-starter-core</module>
        <module>sunny-starter-cache</module>
        <module>sunny-starter-security</module>
        <module>sunny-starter-admin</module>
        <module>sunny-starter-swagger</module>
    </modules>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
View Code

sunny-starter 則引入了其他幾個模塊,在開發項目時,只須要繼承或引入sunny-starter便可,而無需一個個引入各個模塊。

<?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>

    <parent>
        <groupId>com.lyyzoo</groupId>
        <artifactId>sunny</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <groupId>com.lyyzoo.parent</groupId>
    <artifactId>sunny-starter</artifactId>
    <packaging>jar</packaging>

    <name>sunny-starter</name>
    <description>Sunny Parent</description>

    <dependencies>
        <!-- core -->
        <dependency>
            <groupId>com.lyyzoo.core</groupId>
            <artifactId>sunny-starter-core</artifactId>
            <version>${sunny.version}</version>
        </dependency>
        <!-- cache -->
        <dependency>
            <groupId>com.lyyzoo.cache</groupId>
            <artifactId>sunny-starter-cache</artifactId>
            <version>${sunny.version}</version>
        </dependency>
        <!-- security -->
        <dependency>
            <groupId>com.lyyzoo.security</groupId>
            <artifactId>sunny-starter-security</artifactId>
            <version>${sunny.version}</version>
        </dependency>
        <!-- admin -->
        <dependency>
            <groupId>com.lyyzoo.admin</groupId>
            <artifactId>sunny-starter-admin</artifactId>
            <version>${sunny.version}</version>
        </dependency>
        <!-- swagger -->
        <dependency>
            <groupId>com.lyyzoo.swagger</groupId>
            <artifactId>sunny-starter-swagger</artifactId>
            <version>${sunny.version}</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>
View Code

對於一個Spring Boot項目,應該只有一個入口,即 @SpringBootApplication 註解的類。經測試,其它的模塊的配置文件application.properties的配置不會生效,應該是引用了入口模塊的配置文件。

因此爲了讓各個模塊的配置文件都能生效,只需使用 @PropertySource 引入該配置文件便可,每一個模塊都如此。在主模塊定義的配置會覆蓋其它模塊的配置。

二、開發規範

10、結語

到此,基礎架構篇結束!學習了不少新東西,如Spring Boot、Mapper、Druid;有些知識也深刻地學習了,如MyBatis、Redis、日誌框架、Maven等等。

在這期間,看完兩本書,可參考:《MyBatis從入門到精通》、《JavaEE開發的顛覆者 Spring Boot實戰》,另外,開發規範聽從《阿里巴巴Java開發手冊》,其它的參考資料都在文中有體現。

 

緊接着,後面會完成 sunny-starter-security 模塊的開發,主要使用spring-security技術,開發用戶登陸及權限控制等。

 

轉自:http://www.cnblogs.com/chiangchou/

相關文章
相關標籤/搜索