Java 小記 — Spring Boot 的實踐與思考

前言

本篇隨筆用於記錄我在學習 Java 和構建 Spring Boot 項目過程當中的一些思考,包含架構、組件和部署方式等。下文僅爲概要,待閒時逐一整理爲詳細文檔。
html

1. 組件

開源社區如火如荼,若在當下咱們還要去重複 「造輪子」 那真是罪過罪過(固然也並不意味着全部的一切均可拿來即用,瞭解他,使用他,如有能力,去完善它)。所以,當咱們拿到需求的時候首先應當進行拆解,哪些模塊在社區中已有比較成熟的解決方案,而後大體羅列一個粗略的所需組件列表(後續根據架構的設計和兼容狀況再進行調整)。java

1.1 ORM

用於解耦實體對象的裝載過程,他讓咱們的編程過程更關注業務邏輯自己,其重要性毋庸多言。在 Spring Boot 中比較主流的 ORM 框架有 Spring-Data-JPA 和 MyBatis。mysql

JPA 規範的好處是咱們幾乎徹底專一於業務邏輯自己,JpaRepository 中的接口可以知足大部分簡單的操做邏輯了,若要擴展,咱們也能夠再抽離出一個父類,添加自定義的通用 CRUD 操做,如此一來倉儲層的代碼變得異常優雅和簡潔。如下是一個簡單的實現案例:
application.yml:程序員

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://mysql:3306/youclk?characterEncoding=utf8&useSSL=false
    username: root
    password: youclk

  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true

BookRespository.java:redis

public interface BookRespository extends JpaRepository<Book,Integer> {
}

Book.java:spring

@Data
public class Book  {
    private Long id;

    private String name;

    private Date createTime;

}

BookRespositoryTest.java:sql

@RunWith(SpringRunner.class)
@SpringBootTest
public class BookRespositoryTest {

    @Autowired
    private BookRespository repository;

    @Test
    public void saveTest() {
        Assert.assertEquals(repository.findAll().size(),0);
    }

}

映射獲得的 sql 以下:數據庫

Hibernate: select book0_.id as id1_0_, book0_.name as name2_0_, book0_.number as number3_0_ from book book0_

若是是幾年前的話我確定會首選 JPA, 可是期間近兩年的 EFCore 開發經歷讓個人選擇變得謹慎。C# 是 Lambda 和 Linq 的先驅者,所以 .NET + EF 實踐 Code First 着實優雅。然而在遷移 EFCore 的過程當中遇到的問題真是很多,好比說 EFCore 1.x 的時候處理 GroupBy 是全表掃描而後拿到內存中過濾。對於舊項目的遷移咱們通常沒有精力去驗證 ORM 映射生成的每條 SQL 語句,並且本地環境因數據基數少,測試階段很難直觀地體現出來,但部署後就悲劇了,服務和數據庫一塊兒都要死要死的。編程

由此引起的思考是當進行里程碑版本的升級和遷移的時候,新版本 ORM 框架所生成的 SQL 還可否徹底正確體現以前代碼中的邏輯。api

在 .NET Core 中除了 EFCore 還有一個很是優秀的 ORM 框架是 Dapper,這個和 MyBatis 很是像,至關於半自動檔吧,開發者能更好地掌控 SQL,但犧牲了必定的簡潔。介於曾經的入坑經歷,至少在查詢方面我選擇 MyBatis,實例以下:

BookRespository.java:

public interface BookRepository {
    List<Book> findAll();

    @Select("SELECT name FROM book WHERE id = #{id}")
    @Results({
            @Result(property = "name", column = "name")
    })
    String findBookName(Long id);
}

BookRepository.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="com.youclk.demo.repositories.BookRepository" >
    <resultMap id="BaseResultMap" type="com.youclk.demo.domain.Book" >
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="name" property="name" jdbcType="VARCHAR" />
        <result column="createTime" property="createTime" jdbcType="DATE" />
    </resultMap>

    <sql id="Base_Column_List" >
        id, name, createTime
    </sql>

    <select id="findAll" resultMap="BaseResultMap"  >
        SELECT
        <include refid="Base_Column_List" />
        FROM book
    </select>

</mapper>

Application.java 中加一行 MapperScan 註解:

@SpringBootApplication()
@MapperScan("com.youclk.demo.repositories")
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

以上 BookRepository.java 中分別使用了註解和 xml,雖然在 Spring 中註解是王道,但我仍是喜歡 xml 這種方式。一方面接口上寫這麼一堆 SQL 和返回類型看起來很難受,頭重腳輕,並且沒有突出重點,另外一方面在於 IDEA 中對 xml 的智能提示至關友好,效率上也不見得是寫註解快多少。不過採用註解方式少了些配置文件,項目結構更優。

1.2 日誌

Java 中主流的日誌框架有 JUL(java.util.logging)、Log4j、Log4j2 和 Logback 這四款,JUL 因過於簡陋優先淘汰,剩下的三款都是同一個做者開發,Log4j 太舊速度慢,Log4j2 太新問題多,所以 Logback 就是最優解,對應的接口門面我選擇 SLF4j。須要導入的包有: slf4j-api、ogback-classic 和 logback-core,如下是個人案例:

logback-spring.xml:

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{35} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="FILE_WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>WARN</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>log/com.youclk.demo/warm/%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>90</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{35} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>log/com.youclk.demo/error/%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>90</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{35} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="warn">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE_WARN"/>
        <appender-ref ref="FILE_ERROR"/>
    </root>

</configuration>

LoggerTest.java:

@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class LoggerTest {

    @Test
    public void testOutput() {
        log.error("error");
        log.warn("warn");
        log.info("info");
    }

}

以上將日誌的輸出整理歸類,若要分佈式部署,則只要每一個 Container 都掛載 log 目錄即可作日誌的集中管理。Logback 更詳細信息可查閱 「官方文檔」

1.3 緩存

Memcached 和 Redis 都老生常談了,Redis 支持更多的數據結構和操做,而且兩者性能差距不大,所以選他無疑,實現上也極其簡單,以下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTest {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testSetCache() {
        ValueOperations<String, Book> operations = redisTemplate.opsForValue();

        operations.set("my-book",new Book(),5, TimeUnit.SECONDS);

        System.out.println(operations.get("my-book"));
    }
}

另外還有一種註解的用法功能上要強大很多:

@Service
@CacheConfig(cacheNames="book")
public class BookService {

    @Autowired
    private BookRepository bookRepository;

    @Cacheable()
    public List<Book> findAll() {
        return bookRepository.findAll();
    }
}

2. 架構

程序員界一直存在着一條所謂的 「語言鄙視鏈」,曾經爲了 「打嘴炮」 而粗略地對比過 Java 和 C#,因爲未深刻探究,所以我一直以來的觀念都是 C# 的語法糖比 Java 優雅太多。直到我切身感覺了使用 Java 構建項目,或許就原生的兩者來講確實是 C# 更優雅,但加上社區的力量可就很差說了。好比習慣了 C# 自動屬性的我最不喜歡的就是 Java 那麼一堆冗長的 get 和 set, 直到我認識了 lombok,簡直汗顏啊,源碼註解原來還能這麼靈活地使用,由此展開只要你足夠有耐心,想要什麼語法糖自定義註解去實現就好。還有其餘的零零總總, Java 中註解和 AOP 範式的成熟應用拓展了更多的 「編程姿式」 呵(C# 中經過反射也能作到,奈何封閉的生態致使爲其造輪子的很少,現今微軟已擁抱開源,給個祝福吧,互相競爭才更有意思嘛)。

迴歸本節主題,在 .NET 中 DDD 架構的應用可謂是至關普及,你要是不曉得領域、充血模型、事件驅動等概念都很差意思說熟悉 .NET。

這裏簡要歸納一下,順便談談個人想法,傳統 DDD 架構主要分四層,分別爲:User Interface(用戶界面層)、Application(應用層)、Domain(領域層) 和 Infrastructure(基礎設施層)。界面層就不說了,應用層主要起協調做用,好比一個請求從用戶界面層過來,應用層應當分析其須要哪幾個領域模塊參與,並協調他們工做,但其自己不該包含任何的業務規則,基礎設施層在實際應用中最重要的功能就是提供數據持久化機制。領域層則爲整個項目的核心,其應囊括幾乎所有的業務規則,咱們應當在該層根據項目需求設計領域模型,抽離出領域服務,每一個領域模塊應當專一於處理其自身的核心業務邏輯,非核心的業務可封裝爲領域事件交由異步隊列處理。其次,領域層做爲核心,他不該該對其餘層有所依賴,所以通常他會包含基礎設施層的實現接口。

以前對於領域模塊中的通用邏輯或非核心業務,我一般的處理方案是封裝爲領域事件分發,如今想一想如此作法不合理之處,領域事件有些被濫用了。介於 AOP 在 Spring Boot 的普遍應用,領域模型中除了領域實體、值對象、領域服務、領域事件和工做單元以外再加一個領域切面也是極好的。另外,對於領域實體最後的持久化操做若是使用 MyBatis 此類的 ORM 框架那整個編程過程就變得至關繁瑣,在領域中比較容易作到的是對實體狀態的跟蹤,所以持久化選擇 JPA 規範的 ORM 框架才更爲合理,但在查詢上我更喜歡 MyBatis,所以若要作讀寫分離的話, JPA 和 MyBatis 分別對應主備數據庫操做正好。

3. 部署

自從習慣了 Docker 以後,我已經不適應服務的單獨部署了,具體操做詳見個人這篇博文:「Compose & Swarm」

結語

近期正在尋覓 「全棧」 或 「Java 開發」 的工做崗位,如有意向,歡迎留言,微信: youclk。


個人公衆號《有刻》,咱們共同成長!

相關文章
相關標籤/搜索