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。


个人公众号《有刻》,咱们共同成长!

相关文章
相关标签/搜索