整合 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/.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章