前言
本篇随笔用于记录我在学习 Java 和构建 Spring Boot 项目过程中的一些思考,包含架构、组件和部署方式等。下文仅为概要,待闲时逐一整理为详细文档。
1. 组件
开源社区如火如荼,若在当下我们还要去重复 “造轮子” 那真是罪过罪过(当然也并不意味着所有的一切都可拿来即用,了解他,使用他,若有能力,去完善它)。因此,当我们拿到需求的时候首先应当进行拆解,哪些模块在社区中已有比较成熟的解决方案,然后大致罗列一个粗略的所需组件列表(后续根据架构的设计和兼容情况再进行调整)。
1.1 ORM
用于解耦实体对象的装载过程,他让我们的编程过程更关注业务逻辑本身,其重要性毋庸多言。在 Spring Boot 中比较主流的 ORM 框架有 Spring-Data-JPA 和 MyBatis。
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:
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 更详细信息可查阅 “官方文档”
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。
我的公众号《有刻》,我们共同成长!
网友评论