zoukankan      html  css  js  c++  java
  • Java 小记 — Spring Boot 的实践与思考

    前言

    本篇随笔用于记录我在学习 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。


    我的公众号《有刻》,我们共同成长!

    作者:捷义
    出处:http://www.cnblogs.com/youclk/
    说明:转载请标明来源和作者
  • 相关阅读:
    性能调优利器之strace
    如何写出优雅的Python(二)
    c# 模拟 网页实现12306登陆、自动刷票、自动抢票完全篇
    使用Javascript无限添加QQ好友原理解析
    微信公众账号开发之微信登陆Oauth授权-第一篇
    WPF下的仿QQ图片查看器
    不用写软件,纯JS 实现QQ空间自动点赞
    软件分层架构下的另类设计框架-工厂模式的使用
    Javascript实现Linq查询方式
    c# 使用正则表达式 提取章节小说正文全本篇
  • 原文地址:https://www.cnblogs.com/youclk/p/8566805.html
Copyright © 2011-2022 走看看