天天看點

整合 Spring 開發架構及服務端技術

1、關于項目

1.1 項目的内容介紹

Spring-reference 項目為 Spring 整合 MyBatis + Spring MVC,以及 Java Web 方向上多種常用的技術的演練,作為參考和交流學習的資料。另外,也是為了更好地掌握 Spring Boot 打下基礎。該項目注釋完備,代碼規範,隻做技術演練,沒有過多複雜的業務邏輯,适合作為新手學習的參考。項目位址:Shouheng88/Spring=references。

另外,筆者整理了 Spring IOC, AOP, MVC, 事務管理以及 Servlet 和 JSP 相關的内容。主要是整理了一些比較重點的内容作為開發的參考,以下是文章的連結位址。在這篇文章中,我們不會讨論如何這些架構的基本的使用,而是對整合它們到架構當中提出解決方案:

  1. 《對 Spring IOC 機制及其配置方式的的總結》
  2. 《對 Spring AOP 機制及其配置方式的的總結》
  3. 《Spring MVC 機制及其配置方式的總結》
  4. 《對 Spring 事務管理機制及其配置方式的總結》
  5. 《了解 Servlet 和 JSP》

1.2 項目的開發環境

  • JDK 1.8及以上
  • Maven 管理jar包
  • Mysql 資料庫存儲
  • H2 嵌入式資料庫
  • Tomcat 運作用伺服器
  • Rabbit 非必須, 隊列用, 可在配置中調整
  • Lombok 需要開發環境 (IDEA) 支援

1.3 項目中整合的技術清單

  • 通用的 spring 架構搭建,AOP 全局異常處理
  • 前後端通用資料互動格式封裝
  • 提供傳回 json 類型的接口以及傳回普通 Web 頁面的接口執行個體
  • 檔案上傳接口
  • Jasypt 資料加密
  • Quartz 任務排程
  • 提供 RSS 訂閱執行個體
  • 兩種方式整合 Spring 和 MyBatis:隻使用 MyBatis 的在 master 或者 milestone1 分支,基于 Spring 的 Mybatis 在 mybatis 分支
  • 系統全局配置維護, 能實時重新整理記憶體中最新配置
  • 驗證碼生成、校驗
  • Log4j, Email 通知異常
  • Druid 資料庫連接配接池,對資料進行監控
  • Json (fastxml) 序列化與反序列化
  • 通用郵件配置及發送
  • Excel 檔案讀寫
  • CSV 檔案讀寫
  • Junit 測試,以及與 Spring 進行內建
  • RabbitMQ 隊列, 生産-消費, 控制台管理
  • 支援多個資料源,可以通過 Spring 激活配置進行更改
  • 支援請求使用代理, 及動态選擇代理
  • 子產品化開發,基礎通用功能分成獨立的子產品進行開發
  • 內建 Swagger 生成接口文檔
  • Ehcache 緩存,需要使用基于 Spring 配置的 Mybatis,需要切換分支到 mybatis

2、SSM 整合記錄

2.1 maven 項目的結構問題

在開發的時候,我們通常有一些基礎的類放在一個單獨的子產品中。在 Gradle 中,我們可以将其放置在各個子產品中。然後,我們将其通過在 setting.gradle 中配置子產品的檔案路徑。在 maven 當中,我們可以使用類似的方式來解決同樣的問題。在示例項目當中,我們将代碼放進兩個子產品當中:

  1. Common 子產品:放置一些基礎的類,比如 dao 的一些輔組類、驗證碼生成、常用的工具類等。
  2. Service 子產品:與業務相關的一些類,比如 po, bo 等、Service 等。

在 maven 當中,我們可以按照下面這樣來配置子產品的目錄:

首先,項目當中的 pom.xml 共有三個,兩個子子產品各占一個,然後一個公共的父 pom.xml。是以,項目當中的 pom.xml 檔案結構如下:

|-----
    |----common
            |----pom.xml
    |----service
            |----pom.xml
    |----pom.xml
           

在三個 pom.xml 檔案中,我麼需要作如下的配置:

  1. 首先在頂層的父 pom.xml 中,我們作如下的配置。這裡主要是對公共的部分進行管理,比如依賴以及依賴的版本。另外,屬于每個子產品的 groupId, artifactId 在每個子產品當中都是必不可少的。另外,在父 pom 當中還要指定所引用的子子產品的名稱,不然這些子產品就無法被加載到項目當中。
<?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>spring-references</groupId>
    <artifactId>spring-references</artifactId>
    <version>1.0</version>
    <packaging>pom</packaging>

    <name>Spring-references</name>
    <url>https://github.com/Shouheng88/Spring-references</url>

    <!-- 目前的子產品引用的子產品 -->
    <modules>
        <module>service</module>
        <module>common</module>
    </modules>

    <!-- 用來對所依賴的庫的版本進行管理 -->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <!-- 其他版本…… -->
    </properties>

    <!-- 用來進行依賴管理,我們在父 pom 中進行配置,然後子子產品中引用即可 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>${junit.version}</version>
                <scope>test</scope>
            </dependency>
            <!-- 依賴管理…… -->
        </dependencies>
    </dependencyManagement>

    <build>
        <finalName>spring-references</finalName>
        <pluginManagement>
            <plugins>
                <!-- 插件管理…… -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <source>8</source>
                        <target>8</target>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

</project>
           
  1. 然後對于 service 子產品的 pom 做如下處理。這裡我們需要先指定本子產品的身份資訊,即 groupId, artifactId 等。注意,這裡的 artifactId 也就是父子產品 pom 當中引用的子產品的 id. 另外,我們需要通過 parent 标簽指定父子產品的資訊。這樣才能正确地從父子產品當中繼承依賴等資訊。
<?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>

    <!-- 目前子產品的資訊 -->
    <artifactId>service</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>Service</name>
    <url>https://github.com/Shouheng88/Spring-references</url>

    <!-- 父子產品的資訊 -->
    <parent>
        <groupId>spring-references</groupId>
        <artifactId>spring-references</artifactId>
        <version>1.0</version>
    </parent>

    <!-- 該子產品當中引用的依賴 -->
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>
           

對于 common 子產品的 pom 可以采用類似的處理方式,這裡不再進行說明。

2.2 整合 Spring

在目前的示例項目當中,我們通過 Spring 提供的一些架構來實作一些常用的功能:

  1. 基于 Json 的 restful 風格的接口
  2. 基于 Spring MVC 來展示 jsp 頁面
  3. 檔案的上傳接口
  4. 程式中的異常處理
  5. 資料源如何配置

為了實作 json 格式的接口,那麼要考慮的問題又包括:

  1. 互動的資料如何進行封裝;

下面我們來整理下這些東西是如何進行處理的。

2.2.1 前後端互動的資料結構

對于後端自身而言,通常我們會根據業務将資料分成幾種類型,包括

  1. PO:對應于資料庫的資料結構,字段的資訊與資料庫列對應;
  2. SO:前端傳入到後端的資料結構,通常用來傳遞一些查詢資訊;
  3. VO:後端傳回給前端的資料結構,可以根據需要下發的字段進行自定義。
1. 使用 Lombok 簡化資料結構

為了簡化我們的代碼,我們還可以使用一個叫做 lombok 的插件來簡化我們的資料結構。當然,lombok 的作用無非就是簡化 setter/getter 和一些構造方法。這能讓我們的資料實體看上去更加清爽。

為了使用 lombok 我們隻需要做兩處配置就可以了。

  1. 在 maven 當中添加 lombok 的依賴:
<dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${lombok}</version>
    </dependency>
           
  1. 在 IDEA 的插件當中添加 lombok:在 IDEA 當中搜尋 “Lombok Plugin” 并進行安裝即可。
2. PO 的設計

首先呢,一個基礎的 PO 是必不可少的。我們通常使用它們來定義所有的 PO 都應該均有的基礎字段,比如 id, 建立時間, 最後的更新時間, 備注和樂觀鎖版本資訊等。是以,一個基礎的 PO 類應該是下面這樣:

@Data
public abstract class AbstractPO implements Serializable {

    private static final long serialVersionUID = 6982434571375510313L;

    @Id
    @Column(name = "id")
    private Long id;

    @Column(name = "created_time")
    private Date createdTime;

    @Column(name = "updated_time")
    private Date updatedTime;

    @Column(name = "remark")
    private String remark;

    @Version
    @Column(name = "lock_version")
    private Integer lockVersion;
}
           
3.VO 的設計

VO 的設計包含兩部分内容。首先,我們需要定義對應于 PO 的 VO 類。這種類用來定義傳回給前端的資料結構。它并不一定與 PO 類的結構完全相同,而是根據接口的需要選擇性地下發部分字段或者新增一些字段。

對 VO,我們可以定義一個基礎的抽象類如下。也就是定義了一些對應于 PO 的字段資訊:

@Data
public abstract class AbstractVO implements Serializable {

    private static final long serialVersionUID = 1;

    private Long id;
    private Date createdTime;
    private Date updatedTime;
    private String remark;
    private Integer lockVersion;
}
           

除了 VO 我們還要定義 PackVo. 它用來包裝 VO 的資訊,另外提供一些用來傳回給前端的備援字段資訊,以及服務端傳回的錯誤資訊封裝等。對于 PackVo,我們也可以定義一個基礎的抽象類 AbstractPackVo:

@Data
public abstract class AbstractPackVo implements Serializable {

    private static final long serialVersionUID = -2119661016457733317L;

    private Boolean success = true;
    private List<ClientMessage> messages;
    private Long udf1;
    private String udf2;
    private String udf3;
    private String udf4;
    private String udf5;
    private String udf6;
}
           

這裡的 AbstractPackVo 是一個基礎的抽象類。針對具體要傳回的業務資料結構,我們需要定義對應的具體的類,并繼承 AbstractPackVo:

@Data
public class PackTaskVo extends AbstractPackVo {

    private static final long serialVersionUID = 1L;

    private TaskVo vo;
    private List<TaskVo> voList;
}
           
4.ClientMessage 封裝

ClientMessage 就是用來傳回給前端的錯誤資訊的包裝類。我們可以按照下面這樣的方式來進行定義:

@Data
public class ClientMessage implements Serializable {

    private static final long serialVersionUID = -1L;

    private Long id;
    private String code;
    private String message;
    private String messageCN;
}
           

這裡的 code 用來表示錯誤資訊的代碼,我們可以統一将錯誤資訊定義在 properties 檔案中,并将其置于項目的 resources 目錄下面。

#測試消息
E000000000000000=測試消息{0} {1}
#樂觀鎖異常
E000000000000001=該記錄已經被其他使用者修改
#空指針異常
E000000000000002=NullPointerException
#系統錯誤
E000000000000003=SystemErrorException
#DAO錯誤
E000000000000004=DAOException
           

然後,在我們的項目中,我們可以通過單例的工具類來從 properties 檔案中讀取錯誤資訊:

public class ErrorDispUtils {

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

    private static final String CONFIG_FILE = "error-disp.properties";

    private static ErrorDispUtils instance = new ErrorDispUtils();

    private static Configuration config;

    public static ErrorDispUtils getInstance() {
        return instance;
    }

    private ErrorDispUtils() {
        try {
            config = new PropertiesConfiguration(CONFIG_FILE);
        } catch (Exception e) {
            logger.error("ErrorDispUtils initialize error" ,e);
        }
    }

    // 用來讀取指定 code 的字元串
    public String getValue(String key) {
        return config.getString(key);
    }
}
           

對于 ClientMessage,我們可以在項目中通過 AOP 來進行攔截,然後做一個統一的處理。

5.SO 的設計

SO 是前端送出給用戶端的 json 的資料結構,對于它我們也需要做一個簡單的封裝。這裡我們也提供一個基礎的類 SearchObject:

public class SearchObject implements Pageable, Sortable, Serializable {

    private static final long serialVersionUID = 4009650343975989289L;

    private int currentPage;
    private int pageSize;
    private List<Sort> sorts = new LinkedList<>();

    // ... setters and getters
}
           

這裡的 SearchObject 實作了 Pageable 和 Sortable 兩個自定義接口。它們分别用來進行分頁和指定用來排序的字段:

public interface Pageable {

    int getCurrentPage();

    void setCurrentPage(int currentPage);

    int getPageSize();

    void setPageSize(int pageSize);
}

public interface Sortable {

    List<Sort> getSorts();

    void addSort(Sort sort);
}

public class Sort implements Serializable {

    private static final long serialVersionUID = 7739709965769082011L;

    private String sortKey;
    private String sortDir;
}
           

這裡的 Sort 定義了兩個字元串類型的字段,分别表示排序的字段以及排序的方向。

6. 前後端互動

上面我們定義的資料結構是用在服務端自身的,比如 Service 傳回資料。但是,它還無法直接用于前後端的互動。因為前端可能需要一些額外的參數代表裝置資訊,傳回給後端的資料也可能包含一些錯誤資訊等,也就是 ClientMessage 中的資訊。是以,我們需要設計新的資料結構。下面 BusinessRequest 和 BusinessResponse 就分别用作将前端資料傳遞給後端以及後端傳回資料給前端:

@Data
public class BusinessRequest<T> {
    private Integer clientVersion;
    private Date clientTime;
    private String phoneNumber;
    private String iMEI;
    private String iMSI;
    private String deviceID;
    private Long userID;
    private String token;
    private String requestType;
    private T requestData;
    private List<T> requestDataList;
}


@Data
public class BusinessResponse<T> {
    private Boolean isSuccess;
    private Long serverFlag;
    private String serverMessage;
    private T responseData;
    private List<T> responseDataList;
    private Long udf1;
}
           
7.總結

以上隻是我們定義前後端互動的資料結構的一種方式,實際上我們可以有很多不同的方式來定義互動的格式。隻是,在項目開發之前,這種基礎的互動格式需要前後端進行溝通之後來确定。上面的開發的方式也需要按照我們指定的格式來定義才能正确地把我們傳回的資料從 Json 映射成基礎的 java 類。

除了前後端的問題,定義資料結構還将影響我們的開發。比如我們可以在 MyBatis 的檔案中定義一個 sql 代碼片段來在我們的項目中使用 Sort 字段:

<sql id="Order_By_Clause">
  <if test="sorts != null and sorts.size > 0">
    ORDER BY
      <foreach item="item" collection="sorts" separator = ",">
        <if test="item.sortKey != null and item.sortKey != ''">t.${item.sortKey}</if>
        <if test="item.sortDir != null and item.sortDir != ''">${item.sortDir}</if>
	</foreach>
  </if>
</sql>
           

很顯然,這得益于我們以上定義的那一套資料結構規範。而這套規範一旦最終确定下來,我們可以做更多的操作來簡化我們的開發。(顯然,如果這樣做了,一旦資料結構改動,調整的成本也是很高的!)

不過,這種資料互動的格式,我們還是盡量采用一種可靠的、比較成熟都是解決方案,免得在開發的過程中因為資料結構的問題延緩開發進度。而上面的這種格式本身就是一種比較成熟的解決方案了。

2.2.2 Spring 整合

在使用 Spring 開發的過程中幾乎必選的三個核心的子產品:IoC, AOP 和 MVC. 這裡我們主要說明的是 Spring 的這三個子產品的整合。

1. 引入依賴

首先我們需要在項目當中引用 Spring 所需的各種依賴:

<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
        </dependency>
           

這裡我們引用的比較多,主要包括:Spring 核心庫、Spring 容器、Spring 事務管理架構以及 MVC 架構。

然後,我們需要在項目當中整合 Spring 的各個庫。

首先是 Spring MVC. 我們需要在項目的 web.xml 中添加如下的配置:

<!--Spring MVC 相關的 Servlet 的配置-->
    <servlet>
        <servlet-name>spring-mvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/spring-*.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>spring-mvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
           

這裡我們指定 Servlet 的資訊,這裡前面使用了 Spring MVC 的 Servlet 來對請求做分發。然後使用 init-param 标簽指定了 Spring 上下文的配置檔案的位置。本質上這裡作用的原理是通過解析 xml 檔案來讀取配置資訊并生成項目中的類。比如,這裡會建立一個 DispatcherServlet 類,然後這裡的 init-param 中的鍵值對會被通過 setter 方法注入到生成的 DispatcherServlet 執行個體中。本質上這些邏輯都是在 Servlet 即 Tomcat 或者 Netty 等當中完成的。

這裡的 servlet 和 servlet-mapping 标簽是一一對應的,它們通過 servlet-name 來實作比對關系。這裡的 servlet-mapping 标簽用來指定名為 servlet-mvc 的 servlet 能夠處理的 url.

上面,我們使用了通配符來指定多個配置檔案,它們的規則是名稱以

spring-

開頭的 xml 檔案,并且都處于 resources 的 spring 目錄下面。這樣做是因為我們希望把項目當中的 Spring 的配置檔案按照它的功能配置設定到不同的配置檔案當中去。比如,在示例項目中,我們的 Spring 的配置檔案如下:

  1. spring-dao.xml

    : 用來配置 DAO 相關的各種 Bean;
  2. spring-service.xml

    :用來配置 Service 相關的各種 Bean;
  3. spring-shiro.xml

    :用來配置 shiro 相關的 Bean 的資訊;
  4. spring-web.xml

    :用來配置 web 相關的 Bean 的資訊。

預設情況下,DispatcherServlet 會加載

WEB-INF/[DispatcherServlet的Servlet名字]-servlet.xml

下面的配置檔案。根據上面的配置我們需要在目前項目的 WEB-INF 目錄下面加入 spring-mvc-servlet.xml 檔案。

另外需要注意的地方是,這裡我們直接使用 Spring MVC 來分發所有類型的請求。具體是使用 Json 進行互動還是傳回 Jsp 頁面,我們在代碼中使用 Spring 的 RequestMapping 注解來完成。

下面我們先看一下使用 Spring MVC 的時候需要做哪些配置。

2. 整合 MVC

首先讓我們看下 Spring 的 web 相關的配置檔案:

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

    <description>Spring Web 層的配置</description>

    <!--啟用注解驅動-->
    <mvc:annotation-driven />

    <!--指定 Controller 的掃描位置-->
    <context:component-scan base-package="me.shouheng.service.controller">
    </context:component-scan>

    <!--請求映射器-->
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>

    <!--請求擴充卡-->
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>

    <!--配置 Spring MVC 的視圖解析器,需要傳回簡單頁面的時候會用到-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <!--用來處理檔案上傳的請求-->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize" value="4194304" />
        <property name="maxInMemorySize" value="4194304" />
    </bean>

</beans>
           

上面的注解已經很完備了,這裡我們再簡單說明下各個配置項的作用:

  1. 啟用注解驅動:因為我們使用注解來對請求的路由進行映射,是以我們需要啟用注解驅動;
  2. 然後,我們需要指定我們的 Controller 的位置,這裡我們隻需要使用 component-scan 标簽來指定掃描的包的路徑即可。(另外,說明下在項目開發過程中,我們的包結構應該結構清晰和責任獨立。)
  3. 然後,我們需要定義請求映射器和請求适配。請求映射器用來指定請求被映射到哪個 Handler. 不同的 Handler 又需要通過擴充卡以得到情網的結果。這裡我們使用的是 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter. 它們可以幫助我們将請求按照注解的方式進行映射。
  4. 至于視圖解析器。它主要用在傳回 jsp 頁面的請求,指定了 jsp 頁面的字首和字尾。
  5. 最後的一項配置用來處理檔案上傳的請求。

本質上按照上面的配置,我們已經可以把前端傳入的請求處理之後傳回給後端了。我們可以使用下面的例子來進行簡單的測試:

@Controller
@RequestMapping(path = {PATH_PREFIX})
public class TaskController {

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

    static final String PATH_PREFIX = "/task";

    private static final String LIST = "/all";

    private static final String PAGE = "/page";

    /**
     * 測試用來發送 restful 類型的請求的接口
     *
     * @param taskSo 請求對象,Json 類型,放置在 body 中
     * @return 傳回對象
     */
    @ResponseBody
    @RequestMapping(value = LIST, method = RequestMethod.POST)
    public PackTaskVo listAll(@RequestBody TaskSo taskSo) {
        logger.info("----------- received : " + taskSo);
        return new PackTaskVo();
    }

    /**
     * 測試用來請求 jsp 頁面的接口
     *
     * @return jsp 頁面(名稱),映射到 view/task.jsp
     */
    @RequestMapping(value = PAGE, method = RequestMethod.GET)
    public String testPage() {
        logger.info("----------- requesting test page.");
        return "task";
    }
}
           

在上面的這個例子中,我們使用到了之前定義的資料結構。這裡的 @Controller 注解表明這個類是一個控制器。這裡的 @RequestMapping 注解用來指定路由的映射關系。

另外,上面我們使用到了 Looger,這是一個日志架構,我們稍後會說明如何內建日志架構。

3. 配置資料源

關于資料源的問題我們幾個問題需要關注:

  1. 使用哪種資料源,關系型還是非關系型,如果是關系型資料庫那麼是 MySQL 還是其他資料庫;
  2. 使用哪種資料庫通路架構,MyBatis 還是 Herbinate;
  3. 如何根據開發開發環境選擇不同的資料源。

對于資料庫類型而言,業務開發的時候通常使用關系型資料庫,進行緩存的時候會使用非關系型資料庫比如 Redis 或者 MemCached. 對于關系型資料庫,我們可以使用 MySQL 或者其他資料庫。這裡我們為了進行示範,使用兩種關系型資料庫。分别是 MySQL 對應于生成環境,H2 資料庫對應于測試開發環境。是以這就需要我們引入 MySQL 和 H2 的資料庫連接配接驅動的依賴:

<dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql-connecotr}</version>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>${h2.version}</version>
    </dependency>
           

那麼根據開發環境選擇資料庫的時候,我們有很多種配置方式。比如,我們可以在 MyBatis 中建構資料庫連接配接的時候進行配置或者在 Spring 加載不同的配置檔案的時候指定環境。但是,我們應該盡量使用一種配置方式,避免同時使用多種配置方式。這裡我們使用 Spring 上下文來進行配置:

  1. 我們可以在 web.xml 中指定目前要執行的環境,或者在虛拟機啟動參數中指定。如果在 web.xml 中指定上下文的話,那麼我們需要做在該檔案當中增加下面幾行代碼:
<!--選擇要激活的 Spring 環境配置-->
    <context-param>
        <param-name>spring.profiles.active</param-name>
        <param-value>dev</param-value>
    </context-param>
           
  1. 然後在 spring-dao.xml 配置檔案中,我們可以按照下面的方式來指定使用哪個資料源配置檔案:
<beans profile="dev">
        <context:property-placeholder location="classpath*:configs/jdbc-dev.properties" />
    </beans>

    <beans profile="test">
        <context:property-placeholder location="classpath*:configs/jdbc-test.properties" />
    </beans>

    <beans profile="prod">
        <context:property-placeholder location="classpath*:configs/jdbc-prod.properties" />
    </beans>
           

具體資料庫連接配接等資訊被放置在各個配置環境檔案當中。

對于資料庫通路架構的問題,我們這裡使用 MyBatis. 畢竟它目前屬于主流的資料庫通路架構,相對于 Herbinate 拓展性比較好。對于 MyBatis 的內建我們會在後面進行說明。

4. 使用 Spring AOP 進行異常處理

當程式在運作的過程中出現錯誤的時候,我們希望能夠對錯誤進行統一的處理,然後将錯誤資訊包裝成 ClientMenssage 之後傳回給用戶端。在 Spring 當中,我們可以使用 AOP,通過切面來實作異常的統一處理。

這裡我們使用如下的配置來實作對異常的處理:

<!--啟用自動掃描-->
    <context:component-scan base-package="me.shouheng.service.*"/>

    <!--Service 方法切入進行事務管理,比較粗粒度的事務管理,是以需要配置事務的傳播行為-->
    <bean id="serviceHandler" class="me.shouheng.service.common.aop.ServiceMethodInterceptor"/>
    <aop:config>
        <!--對Service的方法的攔截-->
        <aop:pointcut id="servicePointcut" expression="within(me.shouheng.service.service.impl.*)"/>
        <aop:advisor advice-ref="serviceHandler" id="serviceAdvisor" pointcut-ref="servicePointcut"/>
    </aop:config>
           

這裡的第一點是啟用注解自動掃描以發現項目中的 Service,然後定義一個攔截器 ServiceMethodInterceptor。這裡使用的是 JDK 動态代理來實作對異常的控制。

在下面的 aop:config 标簽當中我們定義了要攔截的 Service 的規則。也就是 impl 包下面的所有的類的所有方法。

那麼下面我們來看下這個攔截器是如何定義的:

public class ServiceMethodInterceptor implements MethodInterceptor {

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

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        Method targetMethod = methodInvocation.getMethod();
        Object ret;
        try {
            // 初始化資料庫連接配接
            SqlSessionHolder.initReuseSqlSession();
            // 進行安全校驗并觸發方法
            ret = checkSecurityAndInvokeBizMethod(methodInvocation);
            // 送出事務
            SqlSessionHolder.commitSession();
        } catch (Exception e) {
            logger.error("Error calling " + targetMethod.getName() + " : " + e);
            // 事務復原
            SqlSessionHolder.rollbackSession();
            // 包裝異常資訊之後将其傳回給用戶端
            return this.createExceptionResult(methodInvocation, e);
        } finally {
            // 清空會話資訊
            SqlSessionHolder.clearSession();
        }
        return ret;
    }
}
           

上面是我們的自定義攔截器。這裡在執行攔截的方法之前會調用

SqlSessionHolder.initReuseSqlSession()

啟動資料庫連接配接會話。然後在

checkSecurityAndInvokeBizMethod()

方法中執行指定的方法。這裡如果方法執行的過程中沒有出現任何錯誤,那麼我們就可以使用

SqlSessionHolder.commitSession()

送出事務。如果在執行方法的過程中出現了錯誤,那麼我們就在上面的攔截器當中 catch,根據 Service 的傳回格式,建立包含異常資訊的傳回結果。并進行事務復原。

不過這裡有一個問題就是,如果一個 Controller 調用了多個 Service. 當其中的一個出現問題的時候隻能保證這個 Service 的方法傳回了錯誤的資訊。但這個 Controller 可能會繼續調用其他的 Service 的方法。是以,這種攔截的邏輯最好以 Controller 的方法作為次元進行控制。

2.2.3 整合 MyBatis

就像前面提到的,我們提供了兩種整合 MyBatis 的方式。一種是對 MyBatis 的各個方法做了封裝的方式。這種方式有固定的格式,對 Mapper 的命名以及其内部的方法的命名有嚴格的要求。這種整合方式配合我們的 Generator 可以降低我們開發的複雜度。另一種整合方式即使用 MyBatis 檔案内部的規則來實作映射關系。

1. 定義資料源

上面我們已經提到過 MyBatis 的資料源如何根據環境來進行配置。本質上上面提到的隻是根據當期的開發環境在 xml 配置檔案中引用不同的 properties 檔案。下面我們來看下如何進行配置:

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <!-- ... -->
    </bean>
           

這裡其實我們使用的是阿裡巴巴開源的 Druid 資料庫連接配接池的資料源來實作的。其中的主要的内容是上面的幾個占位符,這些也就是我們需要從 properties 檔案中讀取到的屬性的值。比如,開發環境中我們可能指定這些值:

jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8
jdbc.username=root
jdbc.password=qweasdzxc
           
2. 事務管理

Spring 本身已經為我們提供了一套事務管理機制。這裡我們使用一套自定義的事務管理機制。上面說明 AOP 的時候我們也提到過事務復原的和送出的内容,那就是我們用來實作事務的邏輯。這裡我們主要說明下這裡是如何基于 MyBatis 的事務進行管理的。

因為本質上我們之心 SQL 的時候都是使用 MyBatis 的 SqlSession 來完成的,而 SqlSession 又都是從 SqlSessionFactory 中擷取到的,SqlSessionFactory 又是根據各個配置檔案來建立的。是以,對于每個線程的會話,我們可以将其放置到線程的局部變量中緩存起來。然後需要執行 SQL 的時候從緩存中提取并使用即可。

是以,在進行資料庫通路之前我們需要初始化 SqlSessionFactory 以擷取 SqlSession;在執行完 SQL 之後再根據需要使用 SqlSession 的方法送出或者復原。

這裡我們使用類 SqlMapClientHolder 來初始化各個環境對應的 SqlSessionFactory. 然後在 SqlSessionHolder 中使用 SqlMapClientHolder 擷取 SqlSessionFactory 并存取各個線程的單例的 SqlSession.

以上就是我們基于 MyBatis 的 SqlSession 進行事務管理的實作邏輯。

3. 第 1 種整合方式:直接借助 MyBatis

這種配置方式比較簡單,我們可以先來說明下這種配置是如何實作的。首先,我們需要在 spring-dao.xml 配置檔案中對 MyBatis 進行配置:

<!-- 配置SqlSessionFactory對象 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- 注入資料庫連接配接池 -->
        <property name="dataSource" ref="dataSource" />
        <!-- 配置MyBaties全局配置檔案:ibatis-config.xml -->
        <property name="configLocation" value="classpath:ibatis-config.xml" />
        <!-- 掃描entity包 使用别名 -->
        <property name="typeAliasesPackage" value="me.shouheng.service.model" />
        <!-- 掃描sql配置檔案:mapper需要的xml檔案 -->
        <property name="mapperLocations" value="classpath:mybatis/*.xml" />
        <!-- 類型處理器:用來将擷取的值以合适的方式轉換成 Java 類型 -->
        <property name="typeHandlers">
            <array>
                <bean class="me.shouheng.common.dao.handler.DateTypeHandler"/>
            </array>
        </property>
    </bean>

    <!-- 4.配置掃描Dao接口包,動态實作Dao接口,注入到spring容器中 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- 注入sqlSessionFactory -->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
        <!-- 給出需要掃描Dao接口包 -->
        <property name="basePackage" value="me.shouheng.service.dao" />
    </bean>
           

在上面的這段代碼中,我們指定了資料源、MyBatis 配置檔案的位置、entity 包的位置、Mapper 檔案的位置以及一些自定義的類型處理器。這樣我們就可以實作 DAO 到 Mapper 的映射。

然後,我們隻需要定義各個 DAO 的接口即可。這裡我們定義了一個頂層的接口,然後具體的 DAO 可以繼承這個接口以添加自己的 DAO 方法:

public interface DAO<T extends AbstractPO> {

    void insert(T entity) throws DAOException;

    int update(T entity) throws DAOException;

    int updatePOSelective(T entity) throws DAOException;

    List<T> searchBySo(SearchObject so) throws DAOException;

    <E> List<E> searchVosBySo(SearchObject so) throws DAOException;

    long searchCountBySo(SearchObject so) throws DAOException;

    void deleteByPrimaryKey(Long id) throws DAOException;

    T selectByPrimaryKey(Long id) throws DAOException;

    T selectVoByPrimaryKey(Long id) throws DAOException;
}

public interface TaskDAO extends DAO<Task> {

    // 這個接口中需要增加的方法
}
           

然後,當然就是 DAO 的方法到 Mapper 檔案中的 xml 元素的映射關系:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="me.shouheng.service.dao.TaskDAO">

    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

    <insert id="insert" parameterType="Task">
        insert into gt_task(
        <!-- 0-->id,
        <!-- 1-->created_time,
        <!-- .... -->
    </insert>

    <update id="update" parameterType="Task">
        update gt_task set
            created_time=#{createdTime:BIGINT},
        <!-- .... -->
    </update>

    <!-- ... -->
</mapper>
           

上面一個一個方法地去寫未免過于繁瑣,這種工作我們完全可以交給一些腳本程式來完成。後面我們會介紹我們的項目當中的腳本的實作和用途。

4. 第 2 種配置方式

這種配置方式比前面的配置方式略微複雜一些。我們需要使用前面的 SqlSessionHolder 來擷取目前線程對應的 SqlSession,然後使用它的方法進行資料庫操作。這裡我們定義了 BaseDAO,這是一個抽象類:

public abstract class BaseDAO<T extends AbstractPO> implements DAO<T> {

    private static final String POSTFIX_SPLIT = ".";

    private static final String POSTFIX_INSERT = "insert";

    public BaseDAO() {
        entityClass = (Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }

    protected SqlSession getSqlSession() {
        return SqlSessionHolder.getSession();
    }

    protected String getStatementPrefix() {
        return entityClass.getSimpleName() + POSTFIX_SPLIT;
    }

    @Override
    public Long createPO(T entity) throws DAOException {
        try {
            Long start = System.currentTimeMillis();
            getSqlSession().insert(getStatementPrefix() + POSTFIX_INSERT, entity);
            logger.debug("insert cost is :" + (System.currentTimeMillis() - start));
            return entity.getId();
        } catch (Exception e) {
            logger.error(getStatementPrefix() + " createPO", e);
            throw new DAOException(e);
        }
    }

    // ...
}
           

如上所示,這裡我們需要先擷取到目前 DAO 對應的 Entity 的類名稱,然後使用它拼接成映射到 Mapper 的字元串。是以,這裡對類名、Mapper 名有一些要求。當然,在這種開發方式中,我們也可以使用 Generator 來生成 Mapper 等各種檔案來簡化我們開發的複雜度。這種方式的好處就是我們可以在代碼中對資料庫操作進行包裝。

3、常用三方庫的整合記錄

3.1 消息隊列 RabbitMQ

RabbitMQ 可以用來進行伺服器之間的解耦,本質上作用原理是生産者-消費者模式。比如伺服器 A 和 B 以及 MQ 伺服器,此時 A 發送一個消息到 MQ 伺服器,然後 B 監聽并擷取到了消息之後進行處理。這樣伺服器 A 和 B 之間沒有進行代碼上面的耦合而隻是通過 MQ 中維護的消息隊列進行互動。這同時也意味着 A 和 B 伺服器甚至不需要使用同一種語言進行開發。

使用 RabbitMQ 之前需要先安裝 ErLang 環境,配置環境變量,然後就可以使用了。參考下面的連結來完成環境配置:

  1. ErLang 下載下傳位址:http://www.erlang.org/downloads
  2. RabbitMQ 下載下傳位址:https://www.rabbitmq.com/install-windows.html
  3. RabbitMQ 參考資料:https://www.cnblogs.com/LipeiNet/p/5973061.html

在使用 RabbitMQ 之前需要先添加 RabbitMQ 的依賴:

<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit</artifactId>
</dependency>
           

然後,我們可以使用下面的代碼來進行基本的測試:

private static final String QUEUE_NAME = "rabbitMQ.test";

    @Test
    public void testProducer() throws IOException, TimeoutException {
        // 建立連接配接工廠
        ConnectionFactory factory = new ConnectionFactory();
        // 設定 RabbitMQ 相關資訊
        factory.setHost("localhost");
        // factory.setUsername("lp");
        // factory.setPassword("");
        // factory.setPort(2088);
        // 建立一個新的連接配接
        Connection connection = factory.newConnection();
        // 建立一個通道
        Channel channel = connection.createChannel();
        // 聲明一個隊列
        // channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        String message = "Hello Rabbit MQ";
        // 發送消息到隊列中
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
        log.debug("Producer Send +'{}'", message);
        // 關閉通道和連接配接
        channel.close();
        connection.close();
    }

    @Test
    public void testConsumer() throws IOException, TimeoutException, InterruptedException {
        // 建立連接配接工廠
        ConnectionFactory factory = new ConnectionFactory();
        // 設定RabbitMQ位址
        factory.setHost("localhost");
        // 建立一個新的連接配接
        Connection connection = factory.newConnection();
        // 建立一個通道
        Channel channel = connection.createChannel();
        // 聲明要關注的隊列
        channel.queueDeclare(QUEUE_NAME, false, false, true, null);
        log.debug("Customer Waiting Received messages");
        // DefaultConsumer類實作了Consumer接口,通過傳入一個頻道,
        // 告訴伺服器我們需要那個頻道的消息,如果頻道中有消息,就會執行回調函數handleDelivery
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                log.debug("Customer Received '{}'", message);
            }
        };
        // 自動回複隊列應答 -- RabbitMQ中的消息确認機制
        channel.basicConsume(QUEUE_NAME, true, consumer);
        Thread.sleep(10000);
    }
           

以上我們定義了一個生産者和消費者測試方法。當我們在程式中內建 RabbitMQ 的時候實作的方式與之類似。

3.2 緩存相關

3.2.1 緩存相關架構對比總結

常用的服務端緩存主要有 Redis、Ehcache 和 Memcached。

Ehcache 與其他兩個有明顯的不同。Ehcache 與 java 程式是綁在一起的,直接在虛拟機中緩存,速度快,效率高,但是緩存共享麻煩,叢集分布式應用不友善。

Redis 是一個獨立的程式,我們需要使用 Jedis 用戶端,通過 Socket 通路緩存服務,效率比 Ehcache 低,比資料庫要快很多,處理叢集和分布式緩存友善,有成熟的方案。

Memcached 與 Redis 類似,都是基于鍵值對的,但是 Redis 支援更多的資料結構。在 Memecached 與 Redis 之間進行選擇的時候要基于具體的業務場景:

  1. Redis 具有持久化的功能,而 Memcached 不具備持久化功能,重新開機後資料全部丢失(盡管如此,也不應該讓 Redis 完全取代傳統資料庫,如 MySQL);
  2. 如果鍵值對需要複雜的資料結構,如哈希、清單、集合、有序集合等的時候,應該使用 Redis。最典型的場景有使用者訂單清單、使用者消息、文章評論清單等。
  3. 存儲的内容比較大時,考慮使用 Redis。Memcache 的值要求最大為 1M,如果存儲的值很大,隻能使用 Redis。
  4. 純鍵值對存儲,資料量非常大,并發量非常大的業務,使用 Memcache 或許更适合。因為Memcache 使用預配置設定記憶體池的方式管理記憶體,能夠省去記憶體配置設定時間。Redis 則是臨時申請空間,可能導緻碎片。

是以,如果是單個應用或者對緩存通路要求很高的應用,用 Ehcache。如果是大型系統,存在緩存共享、分布式部署、緩存内容很大的,可以用 Redis 或者 Memecahce。

3.2.2 內建 Redis

我們可以先在 Windows 上面安裝 Redis 來進行學習。你可以參考下面這篇來了解如何在 Windows 上面安裝 Redis:https://www.cnblogs.com/jaign/articles/7920588.html.

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
           

然後,為了使用 Redis 進行資料存儲,我們需要初始化一個 RedisPool 對象,以用來擷取 Jedis 用戶端。是以,我們需要做如下的配置:

<bean class="redis.clients.jedis.JedisPool">
        <constructor-arg name="poolConfig" ref="jedisPoolConfig"/>
        <constructor-arg name="host" value="${redis.host}"/>
        <constructor-arg name="port" value="${redis.port}"/>
        <constructor-arg name="timeout" value="${redis.timeout}"/>
        <constructor-arg name="password" value="${redis.pass}"/>
    </bean>

    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxIdle" value="${redis.maxIdle}"/>
        <property name="maxWaitMillis" value="${redis.maxWait}"/>
        <property name="testOnBorrow" value="${redis.testOnBorrow}"/>
    </bean>

    <context:property-placeholder location="classpath*:configs/redis.properties" ignore-unresolvable="true"/>
           

這裡的配置也比較簡單,就是初始化一個單例的 JedisPool。這個 JedisPool 對象的屬性從 JedisPoolConfig 和 properties 檔案兩個部分得到。

然後,我們可以使用 JedisPool 來測試我們的 Redis 環境是否搭建成功:

@Autowired
    private JedisPool jedisPool;

    @Test
    public void testRedisConnection() {
        Jedis jedis = jedisPool.getResource();
        jedis.set("the-key", "the-value");
        String value = jedis.get("the-key");
        Assert.assertEquals(value, "the-value");
    }
           

上面的内容主要是用來內建 Redis 開發環境。經過上面的配置我們已經可以在程式中使用 Redis 來做緩存了。如果要對 SQL 進行緩存中需要通過自定義注解 + AOP 進行攔截即可。

3.2.3 一個與 properties 檔案相關的問題:Could not resolve placeholder

這個原因是我們在項目當中引用了多個 properties 檔案的原因。我們可以通過在

<context:property-placeholder>

标簽後面追加一個

ignore-unresolvable="true"

屬性來避免這個問題。

這個标簽的作用是:是否忽略解析不到的屬性,如果不忽略,找不到将抛出異常。當

ignore-unresolvable

為 true 時,配置檔案

${}

找不到對應占位符的值不會報錯,會直接指派

${}

;如果設為 false,會直接報錯。

這裡需要設定它為 true 的主要原因:同個子產品中如果引用多個 properties,運作時出現

Could not resolve placeholder 'key'

的情況。原因是在加載第一個

context:property-placeholder

時會掃描所有的 bean,而有的 bean 裡面出現第二個

context:property-placeholder

引入的 properties 的占位符

${key2}

,但此時還沒有加載第二個

property-placeholder

,是以解析不了

${key2}

除了追加上面的屬性,也可以将多個 properties 檔案合并來解決這個問題。

3.2.4 Ehcache 內建

Ehcache 的快速內建可以參考官方的相關介紹:https://www.ehcache.org/.