前言
本篇随笔用于记录我在学习 Java 和构建 Spring Boot 项目过程中的一些思考, 包含架构, 组件和部署方式等. 下文仅为概要, 待闲时逐一整理为详细文档.
1. 组件
开源社区如火如荼, 若在当下我们还要去重复 "造轮子" 那真是罪过罪过 (当然也并不意味着所有的一切都可拿来即用, 了解他, 使用他, 若有能力, 去完善它). 因此, 当我们拿到需求的时候首先应当进行拆解, 哪些模块在社区中已有比较成熟的解决方案, 然后大致罗列一个粗略的所需组件列表 (后续根据架构的设计和兼容情况再进行调整).
1.1 ORM
用于解耦实体对象的装载过程, 他让我们的编程过程更关注业务逻辑本身, 其重要性毋庸多言. 在 Spring Boot 中比较主流的 ORM 框架有 Spring-Data-JPA 和 MyBatis.
JPA 规范的好处是我们几乎完全专注于业务逻辑本身, JpaRepository 中的接口能够满足大部分简单的操作逻辑了, 若要扩展, 我们也可以再抽离出一个父类, 添加自定义的通用 CRUD 操作, 如此一来仓储层的代码变得异常优雅和简洁. 以下是一个简单的实现案例:
- application.YAML:
- 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:
- public interface BookRespository extends JpaRepository<Book,Integer> {
- }
- Book.java:
- @Data
- public class Book {
- private Long id;
- private String name;
- private Date createTime;
- }
- BookRespositoryTest.java:
- @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 还能否完全正确体现之前代码中的逻辑.
在 .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 更详细信息可查阅 "官方文档" https://logback.qos.ch/
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" http://www.cnblogs.com/youclk/p/8453526.html .
结语
来源: http://www.jianshu.com/p/01bb979b61f8