Dew 当前基于 JDK21 + Spring boot 3.x 构建, Spring Cloud 为可选项,但更推荐使用 K8S 作为服务调度容器。

如需基于 Spring boot 2.x 的容器版本请切换到 3.0.0-RC5 tag. 如需基于 Spring boot 1.x 的非容器版本请切换到 1.5.1-RC tag.

基于Rust的微服务框架见: https://github.com/ideal-world/tardis

1. Dew微服务体系 Dew Microservice System

dew
aacfdad1579043f0a2c1928b53096b7b
Apache License 2
Maven Central

微服务一站式解决方案( http://doc.dew.idealworld.group ),提供:架构指南、容器优先/兼容Spring与Service Mesh的框架、最佳实践。

Dew [du:] 意为 露水 ,希望此体系可以像晨间的露水一样透明、静谧、丰盈。让使用者尽量不要感知Dew的存在,专注业务实现。

1.1. 设计理念

1.1.1. 微服务架构的尴尬

几乎人人都在谈微服务,每个IT企业都在做微服务架构,但大部分项目都会存在这样的尴尬:

  • 什么是微服务?怎么做微服务架构?为什么这么乱?

缺乏微服务架构设计思想 导致成功的微服务项目屈指可数,只听说微服务的好,却不知微服务的坑

  • 架构好了,框架怎么选择? dubbo、Spring Boot/Cloud、Istio、Vert.x、还是自研?大一点的企业都会选择自研,但自研又会遇到如下问题:

    • 无法传承,框架的研发人员离职后没有可以接手

    • 上手难度大,很多框架喜欢重复造轮子,做出来的与业界主流思想/标准格格不入,导致学习培训成本很高

    • 功能片面,不通用,服务框架讲求通用性,尽量让整个公司使用同一套规范以方便维护,但很多框架只实现了某些特定场景的功能,无法通用化

    • 维护成本高,尤其是对于完全自研的框架,往往需要专职人员维护

    • 与主流脱节,无法分享微服务化、容器化、服务网格化的红利

没有合适的微服务框架 导致人员技能要求高、项目研发成本高

  • 框架选型也有了,但怎么测试、发布与运维?都在说容器化,要怎么做?

缺少一体化的研发流程支撑 导致各项目规范不统一、发布效率低、容器化问题频出

1.1.2. Dew设计理念

上述问题是Dew必须面对的,应对的设计核心理念是:

提供微服务架构指南 + 扩展主流微服务框架
提供微服务架构指南

项目要上微服务,其架构思想是前提,《微服务架构设计》(https://gudaoxuri.gitbook.io/microservices-architecture) 做为入门书籍非常合适。

扩展主流微服务框架
  1. 简单,用最通用的、标准的、开发人员都熟悉的开发模型

  2. 全面,尽量重用市场已有能力实现,减少框架自身的维护成本

  3. 轻量,原则上不引入高侵入性的三方框架/类库

  4. 可替换,只做扩展,尽量不修改基础框架代码,开发人员完全可以直接基于基础框架开发

  5. 主流,整合流行的微服务框架

实现上我们选择 Spring Boot 这一业界主流框架,对上兼容 Spring BootService Mesh

1.2. 项目结构

|- framework
|-  |- modules
|-  |-  |- parent-starter                  // 父Pom模块
|-  |-  |- boot-starter                    // 核心模块,包含Spring Boot Web相关依赖
|-  |-  |- cluster-common                  // 集群能力接口
|-  |-  |- cluster-common-test             // 集群测试模块
|-  |-  |- cluster-hazelcast               // Hazelcast集群能力实现
|-  |-  |- cluster-rabbit                  // RabbitMQ集群能力实现
|-  |-  |- cluster-redis                   // Redis集群能力实现
|-  |-  |- cluster-mqtt                    // MQTT集群能力实现
|-  |-  |- cluster-rocket                  // Rocket MQ集群能力实现
|-  |-  |- cluster-skywalking              // Skywalking集群能力实现
|-  |-  |- idempotent-starter              // 幂等处理模块
|-  |-  |- dbutils-starter                 // 动态数据库处理模块
|-  |-  |- ossutils-starter                // OSS处理模块
|-  |-  |- hbase-starter                   // Spring Boot HBase Starter 模块
|-  |-  |- test-starter                    // 单元测试模块
|-  |- assists                             // 框架辅助工具
|-  |-  |- sdkgen-maven-plugin             // SDK自动生成、上传插件
|-  |- checkstyle                          // 项目CheckStyle
|- devops                                  // DevOps部分 【!新版本暂不可用!】
|-  |- maven                               // DevOps使用到的Maven插件
|-  |-  |- dew-maven-plugin                // DevOps核心插件
|-  |-  |- dew-maven-agent                 // DevOps部署优化插件
|-  |- sh                                  // DevOps执行脚本
|-  |- cicd                                // 各CI服务的 CI/CD 配置
|-  |-  |- gitlabci                        // Gitlab CI CI/CD配置
|-  |-  |- jenkins                         // Jenkins CI/CD配置
|-  |- docker                              // DevOps使用到的镜像
|-  |-  |- dew-devops                      // 集成 Java Maven Node Git 的镜像
|-  |- it                                  // 集成测试
|- docs                                    // 文档

2. 架构设计 Architecture Chapter

微服务架构设计请参见本书:

3. 编码开发 Framework Chapter

3.1. 框架快速入门

本文以 To-Do 项目为示例讲解 Dew 框架部分的入门操作,项目地址: https://github.com/dew-ms/devops-example-todo

3.1.1. 需求分析

To-Do项目实现一个简单的任务记录功能,要求支持:

  1. 对任务的添加、删除,任务列表的查看等基础功能

  2. 加入类似Excel的公式计算能力,所有以 = 号开头的任务均被视为计算公式,输入后返回计算后的值, 例如输入 =1024*1024 返回1048576

  3. 终端支持H5、微信小程序等平台

3.1.2. 功能体验

体验环境要求安装 Java(>=8)、Maven、NodeJS(>=8)
git clone https://github.com/dew-ms/devops-example-todo.git
cd devops-example-todo
# 先执行安装
mvn install -Dmaven.test.skip=true
# 打开两个命令窗口分别启动两个组件(各组件的作用后文会说明)
mvn spring-boot:run -pl backend/services/kernel
mvn spring-boot:run -pl backend/services/compute
# 打开新的命令窗口,启动前端
cd frontend && npm install && npm run dev:h5
# 自动打开浏览器,切换到移动模式体验
todo demo

3.1.3. 模块设计

此程序比较简单,核心能力由 kernel 组件提供, 考虑到公式计算对CPU的要求较高,所以独立成 compute 组件,由 kernel 发起调用,同时这也演示了服务间Rest调用。 另外程序添加 notifier 组件,所有操作都可发起通知,用于演示MQ调用。 三个组件共用的代码放在 common 模块中。

为兼容不同终端,前端使用taro 框架。

目录结构如下:

|- backend                  // 后端服务
|-  |- libraries            // 类库
|-  |-  |- common           // 公共模块,三个服务组件都依赖于此
|-  |- services             // 服务组件
|-  |-  |- kernel           // 核心服务,与前端交互的唯一入口
|-  |-  |- compute          // 公式计算服务,由kernel通过Rest调用
|-  |-  |- notifier         // 通知服务,由kernel通过MQ调用
|- frontend                 // 前端
|- pom.xml                  // 父Pom

3.1.4. 核心代码说明

这里只关注后端的实现,前端代码不展开说明。
pom.xml
<!-- 引用 Dew的 parent-starter -->
<parent>
    <groupId>group.idealworld.dew</groupId>
    <artifactId>parent-starter</artifactId>
    <version>...</version>
</parent>

<!-- ... -->
backend/libraries/common/pom.xml
<!-- ... -->

<dependencies>
    <dependency>
        <groupId>group.idealworld.dew</groupId>
        <artifactId>boot-starter</artifactId>
    </dependency>
    <!-- 添加JPA支持  -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
</dependencies>

<!-- ... -->
详见 引入方式
backend/services/kernel/pom.xml
<!-- ... -->

    <dependencies>
        <dependency>
            <groupId>group.idealworld.dew.devops.it</groupId>
            <artifactId>todo-common</artifactId>
        </dependency>
        <!-- 引用 cluster-spi-redis 实现Dew集群能力的Redis实现 -->
        <dependency>
            <groupId>group.idealworld.dew</groupId>
            <artifactId>cluster-spi-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>group.idealworld.dew</groupId>
            <artifactId>test-starter</artifactId>
            <!--仅用于演示, 启用内嵌的Redis及 H2-->
            <scope>compile</scope>
        </dependency>
    </dependencies>

<!-- ... -->
backend/services/kernel/resources/bootstrap.yml
dew:
  cluster:
    mq: redis # 使用Redis做为Dew集群的MQ实现

# ...
详见 集群功能
backend/services/kernel/resources/bootstrap-default.yml
spring:
  redis: # Redis配置
    host: localhost
    port: 6379
    database: 0
  datasource: # DB配置
    url: jdbc:sqlite:sample.db

todo-compute:
  ribbon: # 使用自定义ribbon列表
    listOfServers: localhost:8082

# ...
backend/libraries/common/group.idealworld.dew.devops.it.todo.common.TodoParentApplication.java
/**
 * 空实现,做为所有组件启动类的父类
 */
// 启用 Spring Boot 能力
@SpringBootApplication
public class TodoParentApplication {

}
backend/services/kernel/group.idealworld.dew.devops.it.todo.kernel.TodoKernelApplication.java
// 继承自TodoParentApplication
public class TodoKernelApplication extends TodoParentApplication {

    // 启动类
    public static void main(String[] args) {
        new SpringApplicationBuilder(TodoKernelApplication.class).run(args);
    }

}
backend/services/kernel/group.idealworld.dew.devops.it.todo.kernel.controller.TodoController.java
@RestController
// Swagger文档注解
@Api("TODO示例")
@RequestMapping("/api")
public class TodoController {

    @Autowired
    private TodoService todoService;

    /**
     * Add int.
     *
     * @param content the content
     * @return the int
     */
    @PostMapping("")
    @ApiOperation(value = "添加Todo记录")
    public Todo add(@RequestBody String content) {
        return todoService.add(content);
    }

    // ...

}
backend/services/kernel/group.idealworld.dew.devops.it.todo.kernel.service.TodoService.java
@Service
public class TodoService {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * Add int.
     *
     * @param content the content
     * @return id int
     */
    public Todo add(String content) {
        if (content.trim().startsWith("=")) {
            // 去掉 = 号
            content = content.trim().substring(1);
            // 此为幂等修改操作,故使用 put 方法
            // restTemplate 的 put 方法没有返回值,只能使用此方式
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.TEXT_PLAIN);
            HttpEntity<String> entity = new HttpEntity<>(content, headers);
            // 使用Spring的 restTemplate 实现服务间 rest 调用
            // computeService 为配置的服务地址,在Kubernetes下为service name + port
            content = restTemplate
                    .exchange(computeService + "/compute", HttpMethod.PUT, entity, String.class)
                    .getBody();
        }
        // ...
        // 使用Dew的集群MQ功能实现消息点对点发送
        Dew.cluster.mq.request(Constants.MQ_NOTIFY_TODO_ADD, $.json.toJsonString(todo));
        return todo;
    }

    // ...
}
backend/services/notifier/group.idealworld.dew.devops.it.todo.notifier.controller.NotifierController.java
@RestController
public class NotifierController {

    private static final Logger LOGGER = LoggerFactory.getLogger(NotifierController.class);

    @PostConstruct
    public void processTodoAddEvent() {
        // 使用Dew的集群MQ功能实现消息点对点接收
        Dew.cluster.mq.response(Constants.MQ_NOTIFY_TODO_ADD, todo -> {
            LOGGER.info("Received add todo event :" + todo);
        });
    }

    // ...
}

3.2. 框架使用手册

Dew 框架部分是对 Spring Boot 的扩展,使用之前务必了解相关框架的基础知识。
本手册只介绍 Dew 框架部分的扩展功能!

3.2.1. 引入方式

此章节关联示例:examples/bone-example

Dew 所有模块均为Maven结构,使用如下:

<!--引入Dew父依赖,也可以使用import方式-->
<parent>
    <groupId>group.idealworld.dew</groupId>
    <artifactId>parent-starter</artifactId>
    <!--生产环境请选择合适的版本!-->
    <version>${dew.version}</version>
</parent>
...
<dependencies>
    <!--引入需要的模块-->
    <dependency>
        <groupId>group.idealworld.dew</groupId>
        <artifactId>对应模块名,见下文</artifactId>
    </dependency>
</dependencies>
...
<!--开发者介绍-->
<developers>
    <developer>
        <name>...</name>
        <email>...</email>
    </developer>
</developers>
<!--SCM信息-->
<scm>
    <connection>...</connection>
    <developerConnection>...</developerConnection>
    <url></url>
</scm>
...
parent-starter 中已包含各模块的版本,引用模块依赖时可省略版本号。
Table 1. 功能模块
模块名 核心功能

parent-starter

父Pom模块

boot-starter

核心模块,包含Spring Boot Web相关依赖

cluster-common

集群能力接口

cluster-hazelcast

Hazelcast集群能力实现

cluster-rabbit

RabbitMQ集群能力实现

cluster-redis

Redis集群能力实现

cluster-mqtt

MQTT集群能力实现

hbase-starter

HBase Spring Boot 实现

idempotent-starter

幂等处理模块

notification

通知处理模块

3.2.2. Dew类介绍

Dew类包装了一些常用的功能,是Dew的能力的主要输出口。

Dew功能说明
// 获取集群操作能力,见下文
Dew.cluster.xx
// 获取通知操作能力,见下文
Dew.notify.xx
// 获取Spring上下文
Dew.applicationContext.xx
// 获取Dew配置,说见 框架配置速查
Dew.dewConfig.xx
// 获取认证处理能力,见下文
Dew.auth.xx

//  ============ 获取请求上下文信息  ============
// 当次请求的ID
Dew.context().getId()
// 请求来源IP
Dew.context().getSourceIP()
// 请求最初的URL
Dew.context().getRequestUri()
// 请求对应的token,详见下文
Dew.context().getToken()
// 请求对应的操作者信息,详见下文
Dew.context().optInfo()

// ============ 获取当前组件基础信息 ============
// 应用名称,对应为 spring.application.name
Dew.Info.name
// 应用环境,对应为 spring.profiles.active
Dew.Info.profile
// 应用主机Web端口,对应为 server.port
Dew.Info.webPort
// 应用主机IP
Dew.Info.ip
// 应用主机Host
Dew.Info.host
// 应用实例,各组件实例唯一
Dew.Info.instance

// ============ 定时任务操作 ============
// 此类下的操作会自动带入Dew.context()
/**
* 设定一个周期性调度任务.
*
* @param initialDelaySec 延迟启动的秒数
* @param periodSec       周期调度秒数
* @param fun             调度方法
*/
Dew.Timer.periodic(long initialDelaySec, long periodSec, VoidExecutor fun)
/**
* 设定一个定时任务.
*
* @param delaySec 延迟启动的秒数
* @param fun      定时任务方法
*/
Dew.Timer.timer(long delaySec, VoidExecutor fun)

// ============ 常用工具 ============
/**
* 获取真实IP.
*
* @param request 请求信息
* @return 真实的IP
*/
Dew.Util.getRealIP(HttpServletRequest request)
**
* 获取真实IP.
*
* @param requestHeader     请求头信息
* @param defaultRemoteAddr 缺省的IP地址
* @return 真实的IP
*/
Dew.Util.getRealIP(Map<String, String> requestHeader, String defaultRemoteAddr)
/**
* 创建一个新的线程.
* <p>
* 自动带入Dew.context()
*
* @param fun 执行的方法
*/
Dew.Util.newThread(Runnable fun)

/**
* 统一异常处理.
* <p>
* 封装任意异常到统一的格式,见下文
*
* @param <E>            上抛的异常类型
* @param code           异常编码
* @param ex             上抛的异常对象
* @param customHttpCode 自定义Http状态码
* @return 上抛的异常对象
*/
Dew.E.e(String code, E ex, int customHttpCode)

3.2.3. 常用工具集

Dew 的常用工具由 Dew-Common 包提供( https://github.com/gudaoxuri/dew-common ),功能如下:

  1. Json与Java对象互转,支持泛型

  2. Java Bean操作,Bean复制、反射获取/设置注解、字段、方法等

  3. Java Class扫描操作,根据注解或名称过滤

  4. Shell脚本操作,Shell内容获取、成功捕获及进度报告等

  5. 安全(加解密、信息摘要等)操作,Base64、MD5/BCrypt/AES/SHA等对称算法和RSA等非对称算法

  6. Http操作,包含Get/Post/Put/Delete/Head/Options/Patch操作

  7. 金额操作,金额转大写操作

  8. 通用拦截器栈,前/后置、错误处理等

  9. 定时器操作,定时和周期性任务

  10. 常用文件操作,根据不同情况获取文件内容、Glob匹配等

  11. 常用字段操作,各类字段验证、身份证提取、UUID创建等

  12. 常用时间处理,常规时间格式化模板

  13. 主流文件MIME整理,MIME分类

  14. 服务降级处理

  15. 脚本处理

  16. 响应处理及分页模型

Dew Common 的使用
// Dew Common 功能均以 $ 开始,如:

//Json转成Java对象:
$.json.toObject(json,JavaModel.class)
//Json字符串转成List对象
$.json.toList(jsonArray, JavaModel.class)
//Bean复制
$.bean.copyProperties(ori, dist)
//获取Class的注解信息
$.bean.getClassAnnotation(IdxController.class, TestAnnotation.RPC.class)
//非对称加密
$.encrypt.Asymmetric.encrypt(d.getBytes("UTF-8"), publicKey, 1024, "RSA")
//Http Get
$.http.get("https://httpbin.org/get")
//验证手机号格式是否合法
$.field.validateMobile("18657120000")
//...

3.2.4. 集群功能

此章节关联示例:cluster-example

Dew 的集群支持 分布式缓存 分布式Map 分布式锁 MQ 领导者选举, 并且做了接口抽象以适配不同的实现,目前支持 Redis Hazelcast Rabbit

各实现对应的支持如下:

功能 Redis Hazelcast Rabbit MQTT

分布式缓存

*

分布式Map

*

*

分布式锁

*

*

MQ

*

*

*

*(只支持pub-sub)

领导者选举

*

各实现的差异
  • Redis实现了所有功能,但其MQ上不适用于高可用场景

  • 只有Rabbit的MQ支持跟踪日志(见跟踪日志章节)

  • MQTT多用于IoT环境

启用方式
依赖
<dependency>
    <groupId>group.idealworld.dew</groupId>
    <artifactId>boot-starter</artifactId>
</dependency>
<!--引入集群依赖,可选redis/hazelcast/rabbit-->
<dependency>
    <groupId>group.idealworld.dew</groupId>
    <artifactId>cluster-spi-redis</artifactId>
</dependency>
<dependency>
    <groupId>group.idealworld.dew</groupId>
    <artifactId>cluster-spi-hazelcast</artifactId>
</dependency>
<dependency>
    <groupId>group.idealworld.dew</groupId>
    <artifactId>cluster-spi-rabbit</artifactId>
</dependency>
<dependency>
    <groupId>group.idealworld.dew</groupId>
    <artifactId>cluster-spi-mqtt</artifactId>
</dependency>
增加配置
dew:
    cluster:                            # 集群功能
        cache:                          # 分布式缓存实现,默认为 redis
        map:                            # 分布式Map实现,默认为 redis
        lock:                           # 分布式锁实现,默认为 redis
        mq:                             # MQ实现,默认为 redis
        election:                       # 领导者选举实现,默认为 redis

spring:
    redis:
        host:                           # redis主机
        port:                           # redis端口
        database:                       # redis数据库
        password:                       # redis密码
        lettuce:
          pool:                         # 连接池配置
    rabbitmq:
      host:                             # rabbit主机
      port:                             # rabbit端口
      username:                         # rabbit用户名
      password:                         # rabbit密码
      virtual-host:                     # rabbit VH
    hazelcast:
        addresses: []                   # hazelcast地址,端口可选
dew:
  mw:                                   # 中间件
    mqtt:                               # MQTT集群实现
      broker:                           # Broker地址,e.g. tcp://127.0.0.1:1883
      clientId: Dew_Cluster_<Cluster.instanceId>
                                        # 连接客户端ID,注意一个集群内客户端ID必须唯一
      persistence:                      # 存储,默认为File,可选 memory
      userName:                         # 用户名
      password:                         # 密码
      timeoutSec:                       # 连接超时时间
      keepAliveIntervalSec:             # 心跳间隔时间
      cleanSession: true                # 是否消除Session
集群服务的使用入口统一为: Dew.cluster.XX
分布式缓存
API
package group.idealworld.dew.core.cluster;

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 缓存服务.
 *
 * @author gudaoxuri
 */
public interface ClusterCache {

    /**
     * key是否存在.
     *
     * @param key key
     * @return 是否存在
     */
    boolean exists(String key);

    /**
     * 获取字符串值.
     *
     * @param key key
     * @return 值
     */
    String get(String key);

    /**
     * 设置字符串值.
     *
     * @param key   key
     * @param value value
     */
    void set(String key, String value);

    /**
     * 设置字符串值.
     *
     * @param key   key
     * @param value value
     */
    void setIfAbsent(String key, String value);

    /**
     * 设置字符串值,带过期时间.
     *
     * @param key       key
     * @param value     value
     * @param expireSec 过期时间(seconds),0表示永不过期
     */
    void setex(String key, String value, long expireSec);

    /**
     * 字符串不存在时设置值,带过期时间.
     *
     * @param key       key
     * @param value     value
     * @param expireSec 过期时间(seconds),0表示永不过期
     * @return true 设置成功 , false 设置失败(key已存在)
     */
    boolean setnx(String key, String value, long expireSec);

    /**
     * 设置字符串值,并返回其旧值,不存在时返回null.
     *
     * @param key   key
     * @param value value
     * @return 旧值
     */
    String getSet(String key, String value);

    /**
     * 删除key.
     *
     * @param key key
     */
    void del(String key);

    /**
     * 添加列表值.
     *
     * @param key   key
     * @param value value
     */
    void lpush(String key, String value);

    /**
     * 添加列表.
     *
     * @param key       key
     * @param values    values
     * @param expireSec 过期时间(seconds),0表示永不过期
     */
    void lmset(String key, List<String> values, long expireSec);

    /**
     * 添加列表.
     *
     * @param key    key
     * @param values values
     */
    void lmset(String key, List<String> values);

    /**
     * 弹出栈顶的列表值.
     * <p>
     * 注意,Redis的列表是栈结构,先进后出
     *
     * @param key key
     * @return 栈顶的列表值
     */
    String lpop(String key);

    /**
     * 获取列表值的长度.
     *
     * @param key key
     * @return 长度
     */
    long llen(String key);

    /**
     * 获取列表中的所有值.
     *
     * @param key key
     * @return 值列表
     */
    List<String> lget(String key);

    /**
     * 添加Set集合.
     *
     * @param key    key
     * @param values values
     */
    void smset(String key, List<String> values);

    /**
     * 添加Set集合.
     *
     * @param key       key
     * @param values    values
     * @param expireSec 过期时间(seconds),0表示永不过期
     */
    void smset(String key, List<String> values, long expireSec);

    /**
     * 设置Set集合.
     *
     * @param key   key
     * @param value value
     */
    void sset(String key, String value);

    /**
     * 返回一个随机的成员值.
     *
     * @param key key
     * @return 返回值
     */
    String spop(String key);

    /**
     * 获取Set集合的长度.
     *
     * @param key key
     * @return 长度
     */
    long slen(String key);

    /**
     * 删除Set集合对应的values.
     *
     * @param key    key
     * @param values values
     * @return 影响的行数
     */
    long sdel(String key, String... values);

    /**
     * 返回set集合.
     *
     * @param key key
     * @return 值集合
     */
    Set<String> sget(String key);

    /**
     * 设置Hash集合.
     *
     * @param key       key
     * @param items     items
     * @param expireSec 过期时间(seconds),0表示永不过期
     */
    void hmset(String key, Map<String, String> items, long expireSec);

    /**
     * 设置Hash集合.
     *
     * @param key   key
     * @param items items
     */
    void hmset(String key, Map<String, String> items);

    /**
     * 设置Hash集合field对应的value.
     *
     * @param key   key
     * @param field field
     * @param value value
     */
    void hset(String key, String field, String value);

    /**
     * 设置Hash集合field对应的value.
     *
     * @param key   key
     * @param field field
     * @param value value
     */
    void hsetIfAbsent(String key, String field, String value);

    /**
     * 获取Hash集合field对应的value.
     *
     * @param key   key
     * @param field field
     * @return field对应的value,不存在时返回null
     */
    String hget(String key, String field);

    /**
     * 获取Hash集合的所有items.
     *
     * @param key key
     * @return 所有items
     */
    Map<String, String> hgetAll(String key);

    /**
     * 判断Hash集合field是否存在.
     *
     * @param key   key
     * @param field field
     * @return 是否存在
     */
    boolean hexists(String key, String field);

    /**
     * 获取Hash集合的所有keys.
     *
     * @param key key
     * @return 所有keys
     */
    Set<String> hkeys(String key);

    /**
     * 获取Hash集合的所有values.
     *
     * @param key key
     * @return 所有values
     */
    Set<String> hvalues(String key);

    /**
     * 获取Hash集合的长度.
     *
     * @param key key
     * @return 长度
     */
    long hlen(String key);

    /**
     * 删除Hash集合是对应的field.
     *
     * @param key   key
     * @param field field
     */
    void hdel(String key, String field);

    /**
     * 原子加操作.
     *
     * @param key       key,key不存在时会自动创建值为0的对象
     * @param incrValue 要增加的值,必须是Long Int Float 或 Double
     * @return 操作后的值
     */
    long incrBy(String key, long incrValue);

    /**
     * Hash原子加操作.
     *
     * @param h         h
     * @param hk        hk
     * @param incrValue 要增加的值,必须是Long Int Float 或 Double
     * @return 操作后的值
     */
    long hashIncrBy(String h, String hk, long incrValue);

    /**
     * 原子减操作.
     *
     * @param key       key不存在时会自动创建值为0的对象
     * @param decrValue 要减少的值,必须是Long 或 Int
     * @return 操作后的值
     */
    long decrBy(String key, long decrValue);

    /**
     * 原子减操作.
     *
     * @param h         h
     * @param hk        hk
     * @param decrValue 要减少的值,必须是Long 或 Int
     * @return 操作后的值
     */
    long hashDecrBy(String h, String hk, long decrValue);

    /**
     * 设置过期时间.
     *
     * @param key       key
     * @param expireSec 过期时间(seconds),0表示永不过期
     */
    void expire(String key, long expireSec);

    /**
     * 获取过期时间(秒).
     *
     * @param key key
     * @return -2 key不存在,-1 对应的key永不过期,正数 过期时间(seconds)
     */
    long ttl(String key);

    /**
     * 删除当前数据库中的所有Key.
     */
    void flushdb();

    /**
     * 设置bit.
     *
     * @param key    key
     * @param offset offset
     * @param value  值
     * @return 原来的值
     */
    boolean setBit(String key, long offset, boolean value);

    /**
     * 获取指定偏移bit的值.
     *
     * @param key    key
     * @param offset offset
     * @return 指定偏移的值
     */
    boolean getBit(String key, long offset);

}
示例
// 清空DB
Dew.cluster.cache.flushdb();
// 删除key
Dew.cluster.cache.del("n_test");
// 判断是否存在
Dew.cluster.cache.exists("n_test");
// 设置值
Dew.cluster.cache.set("n_test", "{\"name\":\"jzy\"}", 1);
// 获取值并转成Json
$.json.toJson(Dew.cluster.cache.get("n_test"));
Dew的缓存默认只实现了String、List、Set、Hash等结构常用的、时间复杂度低的操作, 如需要的操作Dew没有提供可使用Spring Boot Data Redis原生的RedisTemplate<String,String>
多实例支持
Redis的Cache实现支持多连接实例,目前只支持Lettuce client。
# 配置
spring
    redis:
        host:       # Redis主机
        port:       # Redis端口
        ...
        multi:      # <- 多实例支持
          <key>:    # 实例名称
                    # 可用 Dew.cluster.caches.instance(<key>) 获取
                    # 同时可以用 @Autowired <Key>RedisTemplate 获取Bean
            host:   # Redis主机
            port:   # Redis端口
            ...

# 使用
Dew.cluster.caches.instance("<key>").XXX

# 示例
# 使用key为auth的连接
Dew.cluster.caches.instance("auth").set("token", "xxxxx");
# 使用默认连接
Dew.cluster.cache.set("name", "xxxxx")
分布式Map
API
package group.idealworld.dew.core.cluster;

import java.util.Map;
import java.util.function.Consumer;

/**
 * 分布式Map服务.
 *
 * @param <M> 值的类型
 * @author gudaoxuri
 */
public interface ClusterMap<M> {

    /**
     * 添加Item,同步实现.
     *
     * @param key   key
     * @param value value
     */
    void put(String key, M value);

    /**
     * 添加Item,异步实现.
     *
     * @param key   key
     * @param value value
     */
    void putAsync(String key, M value);

    /**
     * 添加不存在的Item,同步实现.
     *
     * @param key   key
     * @param value value
     */
    void putIfAbsent(String key, M value);

    /**
     * 指定Key是否存在.
     *
     * @param key key
     * @return 是否存在 boolean
     */
    boolean containsKey(String key);

    /**
     * 获取所有Item.
     *
     * @return 所有Item all
     */
    Map<String, M> getAll();

    /**
     * 获取指定key的value.
     *
     * @param key key
     * @return 对应的value m
     */
    M get(String key);

    /**
     * 删除指定key的Item,同步实现.
     *
     * @param key key
     */
    void remove(String key);

    /**
     * 删除指定key的Item,异步实现.
     *
     * @param key key
     */
    void removeAsync(String key);

    /**
     * 清空Map.
     */
    void clear();

    /**
     * 注册新增Item时要执行的函数.
     * <p>
     * 目前只支持Hazelcast实现
     *
     * @param fun 执行的函数
     * @return the cluster map
     */
    default ClusterMap<M> regEntryAddedEvent(Consumer<EntryEvent<M>> fun) {
        return this;
    }

    /**
     * 注册删除Item时要执行的函数.
     * <p>
     * 目前只支持Hazelcast实现
     *
     * @param fun 执行的函数
     * @return the cluster map
     */
    default ClusterMap<M> regEntryRemovedEvent(Consumer<EntryEvent<M>> fun) {
        return this;
    }

    /**
     * 注册更新Item时要执行的函数.
     * <p>
     * 目前只支持Hazelcast实现
     *
     * @param fun 执行的函数
     * @return the cluster map
     */
    default ClusterMap<M> regEntryUpdatedEvent(Consumer<EntryEvent<M>> fun) {
        return this;
    }

    /**
     * 注册清空Map时要执行的函数.
     * <p>
     * 目前只支持Hazelcast实现
     *
     * @param fun 执行的函数
     * @return the cluster map
     */
    default ClusterMap<M> regMapClearedEvent(VoidProcessFun fun) {
        return this;
    }

    /**
     * Entry event.
     *
     * @param <V> the type parameter
     */
    class EntryEvent<V> {
        private String key;
        private V oldValue;
        private V value;

        /**
         * Gets key.
         *
         * @return the key
         */
        public String getKey() {
            return key;
        }

        /**
         * Sets key.
         *
         * @param key the key
         */
        public void setKey(String key) {
            this.key = key;
        }

        /**
         * Gets old value.
         *
         * @return the old value
         */
        public V getOldValue() {
            return oldValue;
        }

        /**
         * Sets old value.
         *
         * @param oldValue the old value
         */
        public void setOldValue(V oldValue) {
            this.oldValue = oldValue;
        }

        /**
         * Gets value.
         *
         * @return the value
         */
        public V getValue() {
            return value;
        }

        /**
         * Sets value.
         *
         * @param value the value
         */
        public void setValue(V value) {
            this.value = value;
        }

    }
}
示例
// 创建指定名为test_obj_map的分布Map实例
ClusterMap<TestMapObj> mapObj = Dew.cluster.map.instance("test_obj_map", TestMapObj.class);
// 清空记录
mapObj.clear();
TestMapObj obj = new TestMapObj();
obj.a = "测试";
// 添加一条记录
mapObj.put("test", obj);
// 获取记录
mapObj.get("test");
分布式锁
API
package group.idealworld.dew.core.cluster;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 分布式锁服务.
 *
 * @author gudaoxuri
 */
public interface ClusterLock {

    /**
     * The Logger.
     */
    Logger LOGGER = LoggerFactory.getLogger(ClusterLock.class);

    /**
     * 尝试加锁,加锁成功后执行对应的函数,执行完成自动解锁.
     * <p>
     * 推荐使用
     * {@link #tryLockWithFun(long waitMillSec, long leaseMillSec, VoidProcessFun fun)}
     *
     * @param fun 加锁成功后执行的函数
     */
    void tryLockWithFun(VoidProcessFun fun) throws Exception;

    /**
     * 尝试加锁,加锁成功后执行对应的函数,执行完成自动解锁.
     * <p>
     * 推荐使用
     * {@link #tryLockWithFun(long waitMillSec, long leaseMillSec, VoidProcessFun fun)}
     *
     * @param waitMillSec 等待时间,单位毫秒
     * @param fun         加锁成功后执行的函数
     */
    void tryLockWithFun(long waitMillSec, VoidProcessFun fun) throws Exception;

    /**
     * 尝试加锁,加锁成功后执行对应的函数,执行完成自动解锁.
     *
     * @param waitMillSec  等待毫秒数
     * @param leaseMillSec 锁释放毫秒数
     * @param fun          加锁成功后执行的函数
     */
    void tryLockWithFun(long waitMillSec, long leaseMillSec, VoidProcessFun fun) throws Exception;

    /**
     * 尝试加锁.
     * <p>
     * 推荐使用 {@link #tryLock(long waitMillSec, long leaseMillSec)}
     *
     * @return 是否加锁成功
     */
    boolean tryLock();

    /**
     * 尝试加锁.
     * <p>
     * 推荐使用 {@link #tryLock(long waitMillSec, long leaseMillSec)}
     *
     * @param waitMillSec 等待毫秒数
     * @return 是否加锁成功
     */
    boolean tryLock(long waitMillSec) throws InterruptedException;

    /**
     * 尝试加锁.
     *
     * @param waitMillSec  等待毫秒数
     * @param leaseMillSec 锁释放毫秒数,估计值,实际释放时间视各中间件的配置及执行环境会有一定偏差
     * @return 是否加锁成功
     */
    boolean tryLock(long waitMillSec, long leaseMillSec) throws InterruptedException;

    /**
     * 解锁操作,只有加锁的实例及线程才能解锁.
     *
     * @return 是否解锁成功
     */
    boolean unLock();

    /**
     * 强制解锁,不用匹配加锁的实例与线程.
     * <p>
     * 谨慎使用
     */
    void delete();

    /**
     * 判断是否有锁.
     *
     * @return true:有锁,false:没有锁
     */
    boolean isLocked();

}
示例
// 创建指定名为test_lock的分布锁实例
ClusterLock lock = Dew.cluster.lock.instance("test_lock");
// tryLock 示例,等待0ms,忘了手工unLock或出异常时1s后自动解锁
if (lock.tryLock(0, 1000)) {
    try {
        // 已加锁,执行业务方法
    } finally {
        // 手工解锁
        lock.unLock();
    }
}
// 上面的示例可用 tryLockWithFun 简化
lock.tryLockWithFun(0, 1000, () -> {
    // 已加锁,执行业务方法,tryLockWithFun会将业务方法包裹在try-finally中,无需手工解锁
});
MQ
API
package group.idealworld.dew.core.cluster;

import group.idealworld.dew.core.cluster.dto.MessageWrap;

import java.util.Map;
import java.util.function.Consumer;

/**
 * MQ服务.
 *
 * @author gudaoxuri
 */
public interface ClusterMQ {

    /**
     * MQ 发布订阅模式 之 发布.
     * <p>
     * 请确保发布之前 topic 已经存在
     *
     * @param topic   主题
     * @param message 消息内容
     * @param header  消息头
     * @param confirm 是否需要确认
     * @return 是否发布成功 ,此返回值仅在 confirm 模式下才能保证严格准确!
     */
    boolean publish(String topic, String message, Map<String, Object> header, boolean confirm);

    /**
     * MQ 发布订阅模式 之 发布.
     * <p>
     * 请确保发布之前 topic 已经存在
     *
     * @param topic   主题
     * @param message 消息内容
     * @param header  消息头
     */
    default void publish(String topic, String message, Map<String, Object> header) {
        publish(topic, message, header, false);
    }

    /**
     * MQ 发布订阅模式 之 发布.
     * <p>
     * 请确保发布之前 topic 已经存在
     *
     * @param topic   主题
     * @param message 消息内容
     */
    default void publish(String topic, String message) {
        publish(topic, message, null);
    }

    /**
     * MQ 发布订阅模式 之 订阅.
     * <p>
     * 非阻塞方式
     *
     * @param topic    主题
     * @param consumer 订阅处理方法
     */
    void subscribe(String topic, Consumer<MessageWrap> consumer);

    /**
     * MQ 请求响应模式 之 请求.
     *
     * @param address 请求地址
     * @param message 消息内容
     * @param header  消息头
     * @param confirm 是否需要确认
     * @return 是否发布成功 ,此返回值仅在 confirm 模式下才能保证严格准确!
     */
    boolean request(String address, String message, Map<String, Object> header, boolean confirm);

    /**
     * MQ 请求响应模式 之 请求.
     *
     * @param address 请求地址
     * @param message 消息内容
     * @param header  消息头
     */
    default void request(String address, String message, Map<String, Object> header) {
        request(address, message, header, false);
    }

    /**
     * MQ 请求响应模式 之 请求.
     *
     * @param address 请求地址
     * @param message 消息内容
     */
    default void request(String address, String message) {
        request(address, message, null);
    }

    /**
     * MQ 请求响应模式 之 响应.
     * <p>
     * 非阻塞方式
     *
     * @param address  请求对应的地址
     * @param consumer 响应处理方法
     */
    void response(String address, Consumer<MessageWrap> consumer);

    /**
     * Gets mq header.
     *
     * @param name the name
     * @return the mq header
     */
    default Map<String, Object> getMQHeader(String name) {
        return Cluster.getMQHeader(name);
    }

    /**
     * Sets mq header.
     *
     * @param name   the name
     * @param header the header
     * @return the mq header
     */
    default Map<String, Object> setMQHeader(String name, Map<String, Object> header) {
        return Cluster.setMQHeader(name, header);
    }

    /**
     * Is header support.
     *
     * @return the result
     */
    boolean supportHeader();
}
示例
// pub-sub
Dew.cluster.mq.subscribe("test_pub_sub", message ->
        logger.info("pub_sub>>" + message.getBody()));
Dew.cluster.mq.publish("test_pub_sub", "msgA",new HashMap<String, Object>() {
      {
          put("h", "001");
      }
  });
Dew.cluster.mq.publish("test_pub_sub", "msgB");
// req-resp
Dew.cluster.mq.response("test_rep_resp", message ->
        logger.info("req_resp>>" + message.getBody()));
Dew.cluster.mq.request("test_rep_resp", "msg1",new HashMap<String, Object>() {
      {
          put("h", "001");
      }
  });
Dew.cluster.mq.request("test_rep_resp", "msg2");
发布订阅模式时,发布前 topic 必须已经存在,可先使用 subscribe 订阅,此操作会自动创建 topic
Rabbit 实现支持单条 confirm 模式。
MQ的HA功能

MQ的HA(高可用)支持,默认为禁用,可通过dew.cluster.config.ha-enabled=true启用。

Dew的MQ仅在数据处理完成后才做commit,这限制了对同一个队列只能串行处理, MQ的HA开启后,您可以以多线程的方式消费消息,处理过程中如发生服务宕机重启后仍可从未处理完成的消息开始消费。

领导者选举
API
package group.idealworld.dew.core.cluster;

/**
 * 领导者选举服务.
 *
 * @author gudaoxuri
 */
public interface ClusterElection {

    /**
     * 当前工程是否是领导者.
     *
     * @return 是否是领导者
     */
    boolean isLeader();

}
示例
// 实例化fun1类型的领导者选举,Redis的实现支持多类型领导者
ClusterElection electionFun1 = Dew.cluster.election.instance("fun1");
// ...
if (electionFun1.isLeader()) {
   // 当前节点是fun1类型的领导者
   // ...
}

3.2.5. 统一响应

Dew 推荐使用 协议无关的响应格式,此格式在 方法间调用 非HTTP协议RPC MQ 等数据交互场景做到真正的 统一响应格式。 要求返回的格式为Resp对象,格式为:

{
    "code": "", // 响应编码,与http状态码类似,200表示成功
    "message":"", // 响应附加消息,多有于错误描述
    "body": // 响应正文
}
示例
public Resp<String> test(){
    return Resp.success("enjoy!");
    // OR return Resp.notFound("…")/conflict("…")/badRequest("…")/…
}

Resp类提供了常用操作:详见 https://gudaoxuri.github.io/dew-common/#true-resp

Dew使用返回格式中的code表示操作状态码,此状态码与HTTP状态码无关,一般情况下HTTP状态码均为200,如需要降级处理时返回500。

500 Http状态码说明

500 状态码仅用于告诉 Hystrix 或其它熔断器这次请求是需要降级的错误,对于 Resp 中的 code 没有影响。

dew 框架会把所有 5xx(服务端错误,需要降级) 的异常统一转换成 500 的Http状态码返回给调用方。

Resp.xxx.fallback() 用于显式声明当前返回需要降级, 比如 Resp.serverError("some message") 不会降级,返回http状态码为200,body为 {"code":"500","message":"some message","body":null}, 但 Resp.serverError("some message").fallback() 会降级,返回http状态码为500,body为 同上。

消息通知

Dew 支持发送消息到钉钉、邮件或自定义HTTP地址,默认支持对未捕获异常通知。

通知配置
# 格式
dew:
  notifies:
    "": # 通知的标识
      type: DD # 通知的类型,DD=钉钉 MAIL=邮件,邮件方式需要有配置spring.mail下相关的smtp信息 HTTP=自定义HTTP Hook
      defaultReceivers: # 默认接收人列表,钉钉为手机号,邮件为邮箱
      dndTimeReceivers: # 免扰时间内的接收人列表,只有该列表中的接收人才能在免扰时间内接收通知
      args: # 不同类型的参数,邮件不需要设置
        url: # type=DD表示钉钉的推送地址
             # 说明详见:https://open-doc.dingtalk.com/microapp/serverapi2/qf2nxq
             # type=HTTP表示HTTP Hook的地址
        msgType: # 仅用于type=DD,支持 text/markdown            strategy: # 通知策略
        minIntervalSec: 0 # 最小间隔的通知时间,0表示不设置,如为10则表示10s内只会发送一次
        dndTime: # 开启免扰时间,HH:mm-HH:mm 如,18:00-06:00
        forceSendTimes: 3 # 同一免扰周期间通知调用达到几次后强制发送

# 示例
dew:
  notifies:
    __DEW_ERROR__:
      type: DD
      defaultReceivers: xxxx
      args:
        url: https://oapi.dingtalk.com/robot/send?access_token=8ff65c48001c1981df7d3269
      strategy:
        minIntervalSec: 5
    sendMail:
      type: MAIL
      defaultReceivers: x@y.z
    custom:
      type: HTTP
      defaultReceivers: x@y.z
      args:
        url: https://...
通知使用
# 最简单的调用
Resp<Void> result = Dew.notify.send("<通知的标识>", "<通知的内容或Throwable>");
# 带通知标题,标题会加上``Dew.Info.instance``
Resp<Void> result = Dew.notify.send("<通知的标识>", "<通知的内容或Throwable>", "<通知标题>");
# 加上特殊接收人列表,非免扰时间内的接收人=配置默认接收人列表+特殊接收人列表,免扰时间内的接收人=配置的免扰时间内的接收人列表
Resp<Void> result = Dew.notify.send("<通知的标识>", "<通知的内容或Throwable>", "<通知标题>", "<特殊接收人列表>");
# 上述三个方法都有异步的重载方法,如
Dew.notify.sendAsync("<通知的标识>", "<通知的内容或Throwable>");
默认通知标识
  1. 未捕获异常: DEW_ERROR,所有未捕获异常(ErrorController)调用此标识发送错误,可通过dew.basic.format.error-flag 修改

要启用以上两个通知请确保dew.notifies下有相应的配置。

HTTP自定义通知格式

POST请求,Body格式为:

{ "title": "", // 标题 "content": "", // 内容 "receivers": [] // 接收人列表 }

调用正常需要返回200状态码

消息通知由 notification 模块提供,boot-starter 集成了此模块,开发中也可以单独引用 notification

3.2.6. 异常处理

Dew 会把程序没有捕获的异常统一上抛,同时框架也支持上文统一响应的异常处理:

自定义异常以支持统一响应API
/**
* 统一异常处理.
* <p>
* 封装任意异常到统一的格式,见下文
*
* @param <E>            上抛的异常类型
* @param code           异常编码
* @param ex             上抛的异常对象
* @param customHttpCode 自定义Http状态码
* @return 上抛的异常对象
*/
Dew.E.e(String code, E ex, int customHttpCode)
自定义异常以支持统一响应示例
// 业务代码捕获了一个异常
Exception someError = new IOException("xxx不存在")
// 使用统一异常处理封装
throw Dew.E.e("NBE00123",someError,200)
// 请求方得到的结果为 http状态=200,响应体:
{
    "code": "NBE00123",
    "message": "xxx不存在",
    "body": null
}

上面介绍的是编码的方式将某些异常封装处理,我们也可以用配置解决:

自定义异常配置,启用后此类异常均使用此模块
dew:
  basic:
    error-mapping:
      "[<异常类名>]":
        http-code: # http状态码,不存在时使用实例级http状态码
        business-code: # 业务编码,不存在时使用实例级业务编码
        message: # 错误描述,不存在时使用实例级错误描述
自定义异常配置示例
dew:
  basic:
    error-mapping:
      "java.io.IOException":
        http-code: 200
        business-code: "NBE00123"

3.2.7. 数据验证

Dew集成了Spring validate 机制,支持针对 URLBean 的验证。

  • 在 java bean 中添加各项validation,支持标准javax.validation.constraints包下的诸如:NotNull ,同时框架扩展了几个检查,如: IdNumber、Phone

  • 在Controller中添加 @Validated 注解 ( Spring还支持@Vaild,但这一注解不支持分组 )

  • 支持Spring原生分组校验

  • URL 类型的验证必须在类头添加 @Validated 注解

  • Dew 框架内置了 CreateGroup UpdateGroup 两个验证组,验证组仅是一个标识,可为任何java对象

数据验证示例
@RestController
@Api(value = "测试")
@RequestMapping(value = "/test/")
@Validated
public class WebController {

    /**
     * Valid create user dto.
     *
     * @param userDTO the user dto
     * @return the user dto
     */
    @PostMapping(value = "valid-create")
    public UserDTO validCreate(@Validated(CreateGroup.class) @RequestBody UserDTO userDTO) {
        return userDTO;
    }

    /**
     * Valid update user dto.
     *
     * @param userDTO the user dto
     * @return the user dto
     */
    @PostMapping(value = "valid-update")
    public UserDTO validUpdate(@Validated(UpdateGroup.class) @RequestBody UserDTO userDTO) {
        return userDTO;
    }

    /**
     * Valid in method.
     *
     * @param age the age
     * @return the string
     */
    @GetMapping(value = "valid-method-spring/{age}")
    public String validInMethod(@Min(value = 2, message = "age必须大于2") @PathVariable("age") int age) {
        return String.valueOf(age);
    }

    /**
     * Valid in method.
     *
     * @param phone the phone
     * @return the string
     */
    @GetMapping(value = "valid-method-own/{phone}")
    public String validInMethod(@Phone @PathVariable("phone") String phone) {
        return phone;
    }

    public static class UserDTO {

        @NotNull(groups = CreateGroup.class)
        @IdNumber(message = "身份证号错误", groups = {CreateGroup.class, UpdateGroup.class})
        private String idCard;

        @Min(value = 10, groups = {CreateGroup.class, UpdateGroup.class})
        private Integer age;

        @NotNull(groups = CreateGroup.class)
        @Phone(message = "手机号错误", groups = {CreateGroup.class, UpdateGroup.class})
        private String phone;

        // Get & Set ...
    }

}

3.2.8. CORS支持

配置
dew:
  security:
    cors:
      allow-origin: # 允许来源,默认 *
      allow-methods: # 允许方法,默认 POST,GET,OPTIONS,PUT,DELETE,HEAD
      allow-headers: # 允许头信息 x-requested-with,content-type

3.2.9. 权限认证

此章节关联示例:auth-example

支持 认证缓存 ,即支持将鉴权系统生成的登录信息缓存到业务系统中方便即时调用,并提供三方适配。

配置认证缓存
dew:
  security:
    token-flag: X-Dew-Token             # Token 标识
    token-kind-flag: X-Dew-Token-Kind   # Token类型 标识
    token-in-header: true               # true:token标识在 header 中,反之在url参数中
    token-hash: false                   # Token值是否需要hash,用于解决token值有特殊字符的情况
    router:                             # 路由功能
      enabled: false                    # 是否启用
      block-uri:                        # URL阻止单,支持ant风格
        <Http Method>: [<URLs>]         # URL阻止单列表,Method = all 表示所有方法
      role-auth:                        # 角色认证
        <Role Code>:                    # 角色编码,角色可以带租户,使用.分隔,e.g. tenant1.admin / tenant2.admin
          <Http Method>: [<URLs>]       # 只有该角色才能访问的URL,支持ant风格,支持继承与重写,Method = all 表示所有方法
    token-kinds:                        # Token类型,可为不同的Token类型设置不同的过期时间、保留的版本
      <Kind>:                           # Token类型名称,比如 PC/Android/... ,默认会创建名为 DEFAULT 的类型
        expire-sec: 86400               # Token 过期时间(秒)
        revision-history-limit: 0       # Token 保留的历史版本数量, 0表示不保留历史版本,即有新的登录时会删除此类型下之前所有的Token
认证缓存需要 集群缓存 服务支持,请引入相关的依赖并配置对应的连接信息等。
basic 认证缓存接口
// 添加登录信息,optInfo封装自鉴权系统过来的登录信息
// 一般在登录认证后操作
Dew.auth.setOptInfo(OptInfo optInfo);
// 获取登录信息,要求在http请求加上token信息
Dew.context().optInfo();
// 删除登录信息
// 一般在注销登录OptInfo后操作
Dew.auth.removeOptInfo();

// 登录信息
public class OptInfo {
    // Token
    String token;
    // 账号编码
    String accountCode;
}
OptInfo 为认证缓存信息的基类,使用时可以继承并扩展自己的属性。
使用 OptInfo 扩展类型时需要在工程启动时指定扩展类: DewContext.setOptInfoClazz(<扩展类型>)
basic 认证缓存示例
/**
 * 模拟用户注册.
 */
@PostMapping(value = "user/register")
public Resp<Void> register(@RequestBody User user) {
    // 实际注册处理
    user.setId($.field.createUUID());
    MOCK_USER_CONTAINER.put(user.getId(), user);
    return Resp.success(null);
}

/**
 * 模拟用户登录.
 */
@PostMapping(value = "auth/login")
public Resp<String> login(@RequestBody LoginDTO loginDTO) {
    // 实际登录处理
    User user = MOCK_USER_CONTAINER.values().stream().filter(u -> u.getIdCard().equals(loginDTO.getIdCard())).findFirst().get();
    String token = $.field.createUUID();
    Dew.auth.setOptInfo(new OptInfoExt()
            .setIdCard(user.getIdCard())
            .setAccountCode($.field.createShortUUID())
            .setToken(token)
            .setName(user.getName())
            .setMobile(user.getPhone())
            .setRoleInfo(new HashSet<>() {
                {
                    add(new OptInfo.RoleInfo()
                            .setCode(userDTO.getRole())
                            .setName("..")
                    );
                }
            }));
    return Resp.success(token);
}

/**
 * 模拟业务操作.
 */
@GetMapping(value = "business/someopt")
public Resp<Void> someOpt() {
    // 获取登录用户信息
    Optional<OptInfoExt> optInfoExtOpt = Dew.auth.getOptInfo();
    if (!optInfoExtOpt.isPresent()) {
        return Resp.unAuthorized("用户认证错误");
    }
    // 登录用户的信息
    optInfoExtOpt.get();
    return Resp.success(null);
}

/**
 * 模拟用户注销.
 */
@DeleteMapping(value = "auth/logout")
public Resp<Void> logout() {
    // 实际注册处理
    Dew.auth.removeOptInfo();
    return Resp.success(null);
}

上述操作的核心是认证适配器,其接口如下:

AuthAdapter
package group.idealworld.dew.core.auth;

import group.idealworld.dew.Dew;
import group.idealworld.dew.core.auth.dto.OptInfo;

import java.util.Optional;

/**
 * 登录鉴权适配器.
 *
 * @author gudaoxuri
 */
public interface AuthAdapter {

    /**
     * 获取当前登录的操作用户信息.
     *
     * @param <E> 扩展操作用户信息类型
     * @return 操作用户信息
     */
    default <E extends OptInfo> Optional<E> getOptInfo() {
        return Dew.context().optInfo();
    }

    /**
     * 根据Token获取操作用户信息.
     *
     * @param <E>   扩展操作用户信息类型
     * @param token 登录Token
     * @return 操作用户信息
     */
    <E extends OptInfo> Optional<E> getOptInfo(String token);

    /**
     * 设置操作用户信息类型.
     *
     * @param <E>     扩展操作用户信息类型
     * @param optInfo 扩展操作用户信息
     */
    <E extends OptInfo> void setOptInfo(E optInfo);

    /**
     * 删除当前登录的操作用户信息.
     * <p>
     * 对指注销登录
     */
    default void removeOptInfo() {
        getOptInfo().ifPresent(optInfo -> removeOptInfo(optInfo.getToken()));
    }

    /**
     * 根据Token删除操作用户信息.
     *
     * @param token 登录Token
     */
    void removeOptInfo(String token);
}

Dew 默认实现了基于 Dew.cluster.cache 的适配器以支持上述功能, 对于Redis的Cache实现了 多实例支持 ,默认优先获取key = auth 的连接配置,不存在时使用默认连接配置。 ,项目中也可以实现自己的适配器。

自定义认证适配器
// 自定义适配器
public class CustomAuthAdapter implements AuthAdapter {

 // ...

}

// 注册为Bean(可选,如果自定义适配器用到Spring功能时必须)
@Bean
public CustomAuthAdapter customAuthAdapter() {
    return new CustomAuthAdapter();
}

// 注册自定义适配器
Dew.auth = new CustomAuthAdapter() // 或 Bean实例

3.2.10. 测试支持

良好的单元测试可以保证代码的高质量,单测的重要原则是内聚、无依赖,好的单测应该是"函数化"的——结果的变化只与传入参数有关。 但实际上我们会的代码往往会与数据库、缓存、MQ等外部工具交互,这会使单测的结果不可控,通常的解决方案是使用Mock,但这无行中引入了单测撰写的成本, Dew使用"内嵌式"工具解决,数据库使用 H2 ,Redis使用 embedded redis ,由于 Dew 集群的 Cache Map Lock MQ 都支持 Redis 实现,所以可以做到对主流操作的全覆盖。

依赖
<dependency>
    <groupId>group.idealworld.dew</groupId>
    <artifactId>test-starter</artifactId>
</dependency>
配置
dew:
  cluster: #所有集群操作都使用reids模拟
    cache: redis
    lock: redis
    map: redis
    mq: redis

spring:
  redis:
    host: 127.0.0.1
    port: 6379
  datasource:
    url: jdbc:sqlite:sample.db

3.2.11. 幂等处理

此章节关联示例:idempotent-example

支持HTTP和非HTTP幂等操作,对于HTTP操作,要求请求方在请求头或URL参数中加上操作ID标识,非HTTP操作由可自由指定操作类型和操作ID标识的来源。

依赖
<!--引入幂等支持-->
<dependency>
    <groupId>group.idealworld.dew</groupId>
    <artifactId>idempotent-starter</artifactId>
</dependency>
配置
dew:
  cluster:
    cache: redis # 启用Redis支持
  idempotent:
    default-expire-ms: 3600000 # 设置默认过期时间,1小时
    default-strategy: item # 设置默认策略,支持 bloom(Bloom Filter)和item(逐条记录),目前只支持item
    default-opt-id-flag: __IDEMPOTENT_OPT_ID__ # 指定幂等操作ID标识,可以位于HTTP Header或请求参数中
HTTP操作
@GetMapping(xxx)
// 启用幂等支持
// 请求头部或参数加上__IDEMPOTENT_OPT_ID__ = xx
@Idempotent
public void test(xxx) {
    // 业务操作
    // ...
    // 业务失败,在保证业务操作的原子性的情况下,在catch中取消幂等,并抛出异常
    DewIdempotent.cancel();
    // 手工确认
    DewIdempotent.confirm();
}

Idempotent注解说明:

  • optIdFlag:指定幂等操作ID标识,可以位于HTTP Header或请求参数中

  • expireMs:设置过期时间,单位毫秒

  • strategy:设置默认策略

  • needConfirm:设置是否需要显式确认,true时,需要进行显式确认操作: DewIdempotent.confirm() 或 DewIdempotent.confirm(String optType, String optId) 前者要求与请求入口在同一线程中

非HTTP操作
// 初始化类型为transfer_a的幂等操作,需要手工确认,过期时间为1秒
DewIdempotent.initOptTypeInfo("transfer_a", true, 1000, StrategyEnum.ITEM);
// 第一次请求transfer_a类型下的xxxxxxx这个ID,返回不存在,表示可以下一步操作
Assert.assertEquals(StatusEnum.NOT_EXIST, DewIdempotent.process("transfer_a", "xxxxxxx"));
// 第二次请求transfer_a类型下的xxxxxxx这个ID,返回未确认,表示上次操作还在进行中
Assert.assertEquals(StatusEnum.UN_CONFIRM, DewIdempotent.process("transfer_a", "xxxxxxx"));
// 确认操作完成
DewIdempotent.confirm("transfer_a", "xxxxxxx");
// 第三次请求transfer_a类型下的xxxxxxx这个ID,返回已确认,但未过期,仍不能操作
Assert.assertEquals(StatusEnum.CONFIRMED, DewIdempotent.process("transfer_a", "xxxxxxx"));
// 延时1秒
Thread.sleep(1000);
// 再次请求transfer_a类型下的xxxxxxx这个ID,返回不存在(上次请求已过期),表示可以下一步操作
Assert.assertEquals(StatusEnum.NOT_EXIST, DewIdempotent.process("transfer_a", "xxxxxxx"));

3.2.12. Dew SDK 插件

本插件为Dew微服务体系的组成部分,用于SDK的自动生成并上传到Maven仓库。

使用
Dew体系下的使用
  1. 使用OpenAPI V3规范(By Swagger)添加API注解

  2. 执行 mvn deploy -P release

非Dew体系下的使用
  1. 使用OpenAPI V3规范(By Swagger)添加API注解

  2. 在项目POM中添加本插件

  3. 执行项目自定义的 mvn deploy xx (需要指定部署的仓库地址)

配置
名称 默认值 说明

dew_sdk_gen

false

是否生成SDK

dew_sdk_release_skip

false

是否跳过SDK发布

dew_main_class

Main方法所在Class(包名+类名),为空时启用自动发现功能

dew_sdk_gen_openapi_path

自定义http的open API路径,为空时通过单元测试自动生成

dew_sdk_gen_lang

java

SDK的语言

目前仅支持java,后续会集成更多语言。
核心逻辑
OpenAPI文件生成
  1. 在 maven phase = validate 时触发

  2. 如果 dew_sdk_gen_openapi_path 存在,则下载 OpenAPI 文件到 dew-sdkgen 目录,结束

  3. 如果 dew_main_class 不存在,则查找 src 下 main class 文件,返回 main class 所在的包

  4. 以 testFile.mustache 为模板,注入 main class 等变量生成名为 DewSDKGenTest 的测试类到 main class 所在的同名包下

  5. 在 maven phase = test 时触发单元测试,执行 DewSDKGenTest 生成 OpenAPI 文件到 dew-sdkgen 目录

SDK工程生成
  1. 在 maven phase = deploy 时触发

  2. 自定义 swagger-codegen-maven-plugin ,生成对应的SDK工程,包含了对JDK11支持、统一入口支持等自定义内容

SDK发布
  1. 在 maven phase = deploy 时触发

  2. 在 SDK工程生成 后调用 maven-invoker-plugin 插件,执行 deploy goal,并传入主工程的Maven仓库地址到 -DaltDeploymentRepository 参数,完成SDK发布

3.2.13. 代码质量检查

Dew 已集成 Sonar 插件,只需要在maven中配置 sonar.host.url 为目标地址, 然后执行 mvn clean verify sonar:sonar -P qa -Dsonar.login=<用户名> -Dsonar.password=<密码> 即可。

也可以设置 sonar.forceAuthentication=false ,但要注意安全管控。
使用 <maven.test.skip>true</maven.test.skip> 可跳过特定模块的测试,<sonar.skip>true</sonar.skip> 可跳过特定模块的Sonar检查。
Spring Boot HBase

在集成 HBase 客户端能力的基础之上,支持 Spring Boot 配置管理、支持 Kerberos 认证。

依赖
<dependency>
    <groupId>group.idealworld.dew</groupId>
    <artifactId>hbase-starter</artifactId>
</dependency>
配置
spring:
  hbase:
    zkQuorum: localhost  # zookeeper url
    znodeParent: /hbase-secure # zookeeper znode parent
    auth:
      type: kerberos # 认证类型,默认是 simple,可选:simple 和 kerberos
      principal: # kerberos 下 principal
      keytab: # kerberos 下 keytab 路径
      hbaseMasterPrincipal: # kerberos 下 hbase master principal
      hbaseRegionServerPrincipal: # kerberos 下 hbase region server principal
      hbaseClientRetriesNumber: # hbase 客户端重试次数,默认:5
      hbaseClientOperationTimeout: # hbase 客户端超时时间,默认:300000
      hbaseClientScannerTimeoutPeriod: # hbase 客户端 scan 超时时间,默认:60000
      hbaseClientPause: # hbase 重试的休眠时间,默认:30
使用
@Autowired
private HBaseTemplate hbaseTemplate;

hbaseTemplate.get("table_hbase", "0002093140000000",
                "0", "reg_platform", (result, row) -> Bytes.toString(result.value()));
HBaseTemplate 其他使用方法可以详见 hbase-starter 模块下的 test 内容。

3.3. 框架配置速查

3.3.1. Dew 参数

dew:                                    # Dew 参数key前缀
  basic:                                # 基础配置
    name:                               # 服务名称,用于API文档显示等
    version: 1.0                        # 服务版本号,用于API文档显示等
    desc:                               # 服务描述,用于API文档显示等
    webSite:                            # 官网,用于API文档显示等
    doc:                                # 文档配置
      enabled: true                     # 是否启用默认文档配置,关闭后可自定义文档管理,默认为true
      base-package:                     # API文档要扫描的根包,多指定到 Controller 包中
      contact:                          # 联系人信息
        name:                           # 联系人姓名
        url:                            # 联系人URL
        email:                          # 联系人邮箱
    format:                             # 格式化配置
      use-unity-error: true             # 是否启用统一响应
      auto-trim-from-req: false         # 是否自动去掉请求中字符串类型的前后空格
      error-flag: __DEW_ERROR__         # 默认的通知标识
    error-mapping:                      # 自定义错误映射
      "[<>]":                           # 异常类名
        http-code:                      # http状态码,不存在时使用实例级http状态码
        business-code:                  # 业务编码,不存在时使用实例级业务编码
        message:                        # 错误描述,不存在时使用实例级错误描述
  cluster:                              # 集群功能
    cache: redis                        # 缓存实现
    lock: redis                         # 分布式锁实现,可选 redis/hazelcast,默认redis
    map: redis                          # 分布式Map实现,可选 redis/hazelcast,默认redis
    mq: redis                           # MQ实现,可选 redis/hazelcast/rabbit,默认redis
    election: redis                     # 领导者选举实现,可选 redis,默认redis
    config:                             # 集群相关配置
      election-period-sec: 60           # 领导者选举时间区间,默认60秒
      ha-enabled: false                 # 是否启用HA,默认为false
  notifies:                             # 通知功能
    "":                                 # 通知的标识
      type: DD                          # 通知的类型,DD=钉钉 MAIL=邮件,邮件方式需要有配置spring.mail下相关的smtp信息 HTTP=自定义HTTP Hook
      defaultReceivers:                 # 默认接收人列表,钉钉为手机号,邮件为邮箱
      dndTimeReceivers:                 # 免扰时间内的接收人列表,只有该列表中的接收人才能在免扰时间内接收通知
      args:                             # 不同类型的参数,邮件不需要设置
        url:                            # type=DD表示钉钉的推送地址
                                        # 说明详见:https://open-doc.dingtalk.com/microapp/serverapi2/qf2nxq
                                        # type=HTTP表示HTTP Hook的地址
        msgType:                        # 仅用于type=DD,支持 text/markdown
      strategy:                         # 通知策略
        minIntervalSec: 0               # 最小间隔的通知时间,0表示不设置,如为10则表示10s内只会发送一次
        dndTime:                        # 免扰时间,HH:mm-HH:mm 如,18:00-06:00
                                        # HH:mm-HH:mm,如果两个时间相等表示全天免扰,如果后者大于前者表示跨天免扰
        forceSendTimes: 3               # 同一免扰周期间通知调用达到几次后强制发送
  security:                             # 安全功能
    cors:                               # 跨域设置
      allow-origin: *                   # 允许的来源,建议修改
      allow-methods: POST,GET,OPTIONS,PUT,DELETE,HEAD
                                        # 允许的方法
      allow-headers: x-requested-with,content-type
                                        # 允许的头信息
    token-flag: X-Dew-Token             # Token 标识
    token-kind-flag: X-Dew-Token-Kind   # Token类型 标识
    token-in-header: true               # true:token标识在 header 中,反之在url参数中
    token-hash: false                   # Token值是否需要hash,用于解决token值有特殊字符的情况
    un-ident-urls:                      # 不需要认证的URL列表,英文逗号分隔
    router:                             # 路由功能
      enabled: false                    # 是否启用
      block-uri:                        # URL阻止单,支持ant风格
        <Http Method>: [<URLs>]         # URL阻止单列表,Method = all 表示所有方法
      role-auth:                        # 角色认证
        <Role Code>:                    # 角色编码,角色可以带租户,使用.分隔,e.g. tenant1.admin / tenant2.admin
          <Http Method>: [<URLs>]       # 只有该角色才能访问的URL,支持ant风格,支持继承与重写,Method = all 表示所有方法
    token-kinds:                        # Token类型,可为不同的Token类型设置不同的过期时间、保留的版本
      <Kind>:                           # Token类型名称,比如 PC/Android/... ,默认会创建名为 DEFAULT 的类型
        expire-sec: 86400               # Token 过期时间(秒)
        revision-history-limit: 0       # Token 保留的历史版本数量, 0表示不保留历史版本,即有新的登录时会删除此类型下之前所有的Token
    error:                              # 错误配置
      enabled: false                    # 启用降级邮件通知,默认为false
      notify-event-types: FAILURE,SHORT_CIRCUITED,TIMEOUT,THREAD_POOL_REJECTED,SEMAPHORE_REJECTED
                                        # 通知的事件类型
      notify-include-keys:              # 需监控的方法key值,与notify-exclude-keys互斥,client类名+#+方法名,for example:  ExampleClient#deleteExe(int,String)
      notify-exclude-keys:              # 不需要监控的方法key值,与notify-include-keys互斥,client类名+#+方法名,for example:  ExampleClient#deleteExe(int,String)
  idempotent:                           # 需要引入 idempotent-starter 模块
    default-expire-ms: 3600000          # 设置默认过期时间,1小时
    default-strategy: item              # 设置默认策略,目前支持item(逐条记录)
    default-opt-id-flag: __IDEMPOTENT_OPT_ID__
                                        # 指定幂等操作ID标识,可以位于HTTP Header或请求参数中
  mw:                                   # 中间件
    mqtt:                               # MQTT集群实现
      broker:                           # Broker地址,e.g. tcp://127.0.0.1:1883
      clientId: Dew_Cluster_<Cluster.instanceId>
                                        # 连接客户端ID,注意一个集群内客户端ID必须唯一
      persistence:                      # 存储,默认为File,可选 memory
      userName:                         # 用户名
      password:                         # 密码
      timeoutSec:                       # 连接超时时间
      keepAliveIntervalSec:             # 心跳间隔时间
      cleanSession: true                # 是否消除Session

spring:                                 # 常用 Spring 配置
  application:
    name:                               # 项目名称,若使用Dew,请配置
  mail:                               # Mail配置
    host: smtp.163.com
    username:
    password:
    properties:
      mail:
        smtp:
          auth: true
            starttls:
              enable: true
              required: true
  redis:
    host:                           # Redis主机
    port:                           # Redis端口
    database:                       # Redis数据库
    password:                       # Redis密码
    lettuce:
      pool:                         # 连接池配置
    multi:                          # 多实例支持(Dew功能)
      <key>:                        # 实例名称
                                    # 可用 Dew.cluster.caches.instance(<key>) 获取
                                    # 同时可以用 @Autowired <Key>RedisTemplate 获取Bean
        host:                       # Redis主机
        port:                       # Redis端口
        ...
  rabbitmq:
    host:                           # Rabbit主机
    port:                           # Rabbit端口
    username:                       # Rabbit用户名
    password:                       # Rabbit密码
    virtual-host:                   # Rabbit VH
  hazelcast:
    username:
    password:
    addresses: ["127.0.0.1"]

server:
  port: 8081                          # 服务端口

management:
  endpoints:
    web:
      base-path: /management          # 管理路径前缀

logging:
  level:
    ROOT: INFO
    group.idealworld.dew: DEBUG                     # Dew目录日志配置
    org.springframework.jdbc.core: TRACE
                                      # Jdbc目录日志配置

3.4. 框架最佳实践

3.4.1. 项目模块设计

Dew推荐的项目结构为:

X Build Project                 // 构建工程
|- sources                      // 源码目录
|-  |- basics                   // 基础源码,为各服务工程的依赖,要求deploy到Maven仓库
|-  |-  |- parent               // 父工程,所有服务端应用的根POM
|-  |-  |- common               // 公共工程,所有服务端应用的基础依赖
|-  |-  |- common-service       // 公共服务工程,所有服务工程的根POM,自身依赖于common
|-  |-  |- <...>                // 其它基础工程,比如common-spark,大数据服务的基础依赖
|-  |- services                 // 服务源码,对应于一个个微服务
|-  |-  |- <service 1>          // 服务1工程
|-  |-  |- <service ...>        // 其它服务工程
|-  |- sdk                      // SDK源码,如项目需要提供SDK时建立此目录
|-  |-  |- <java>               // Java版本的SDK工程
|-  |-  |- <js>                 // JS版本的SDK工程
|-  |-  |- <rest>               // REST版本的SDK工程
|-  |-  |- <...>                // 其它语言版本的SDK工程
|-  |- terminals                // 终端源码
|-  |-  |- <android>            // Android APP工程
|-  |-  |- <ios>                // IOS APP工程
|-  |-  |- <wechat>             // Wechat工程
|-  |-  |- <...>                // 其它终端工程
|- docs                         // 文档工程,所有团队共同维护,Asciidoc方案,后文会介绍
|-  |- src
|-  |-  |- main
|-  |-  |-  |- asciidoc
|-  |-  |-  |-  |- book.adoc    // 文档主目录
|-  |-  |-  |-  |- <...>        // 分模块建立不同的文档文件
|-  |-  |- resources            // 文档引用资源目录,如图片、CSS
|-  |-  |-  |- images
|-  |-  |-  |-  |- <...>
|-  |-  |- pom.xml              // 文档工程使用Maven管理
|-  |-  |- .gitignore
|- env                          // 环境配置工程,以Spring Cloud Config为例,配置存放于Git
|-  |- application.yml
|-  |- <...>
|-  |- .gitignore
|- pom.xml                      // 构建工程的POM
|- .gitmodules                  // Git子模块定义,所有工程都注册到此文件中
|- .gitignore
|- README.adoc                  // 构建工程使用说明

推荐各工程使用独立Git库,详见: 微服务架构设计-代码管理

3.4.2. 代码包设计

Dew推荐的包结构为:

|- X service
|-  |- src
|-  |-  |- main
|-  |-  |-  |- java
|-  |-  |-  |-  |- src
|-  |-  |-  |-  |-  |- x.y.z
|-  |-  |-  |-  |-  |-  |- XApplication (1)
|-  |-  |-  |-  |-  |-  |- XConfig (2)
|-  |-  |-  |-  |-  |-  |- XInitiator (3)
|-  |-  |-  |-  |-  |-  |- controller
|-  |-  |-  |-  |-  |-  |-  |- EventController (4)
1 XApplication : 服务启动类
2 XConfig:当前服务的配置文件(Spring Cloud格式)
3 XInitiator:当前服务启动初始化器,原则上所有与数据库、缓存、定时任务相关的初始化操作都应该从此类发起,以方便排错
4 EventController:事件处理器,名称可以更具象,原则上所有与MQ receive相关的操作都被视为交互接口,应该放到controller包下
.示例
----
@PostConstruct
public void processTodoAddEvent() {
    // 使用Dew的集群MQ功能实现消息点对点接收
    Dew.cluster.mq.response(Constants.MQ_NOTIFY_TODO_ADD, todo -> {
        logger.info("Received add todo event :" + todo);
    });
}
----

3.4.3. 代码质量管理

推荐使用 checkstyle 管理代码风格,使用方式:

checkstyle使用
# 在Maven中配置自定义的 checkstyle 文件的路径
<properties>
    <checkstyle.config.path>../../checkstyle/checkstyle.xml</checkstyle.config.path>
</properties>

# 使用Maven命令
mvn -P qa compile

# 也可以IDE的checkstyle插件实现

3.4.4. @Validated 注解

  • 在Spring Controller类里,@Validated 注解初使用会比较不易上手,在此做下总结

    1. 对于基本数据类型和String类型,要使校验的注解生效,需在该类上方加 @Validated 注解

    2. 对于抽象数据类型,需在形式参数前加@Validated注解

Spring对抽象数据类型校验抛出异常为MethodArgumentNotValidException,http状态码为400,对基本数据类型校验抛出异常为ConstraintViolationException,http状态码为500,dew对这两种异常做了统一处理,http状态码均返回200,code为400

3.4.5. jackson 对于 Java8 时间转换( SpringMVCjackson 接收 json 数据)

  1. 对于 LocalDateTime 类型,需在参数上加 @JsonFormat 注解,如下:@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")

  2. LocalDate,LocalTime,Instant 等,无需配置可自行转换

jackson 对于 LocalDateTime 类型的支持与其他三种类型不具有一致性,这是 jackson 需要优化的一个点

3.4.6. 缓存处理

Spring Cache 提供了很好的注解式缓存,但默认没有超时,需要根据使用的缓存容器特殊配置

Redis缓存过期时间设置
@Bean
RedisCacheManager cacheManager() {
    final RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
    redisCacheManager.setUsePrefix(true);
    redisCacheManager.setDefaultExpiration(<过期秒数>);
    return redisCacheManager;
}

3.4.7. jdbc 批量插入性能问题

如果不开启rewriteBatchedStatements=true,那么jdbc会把批量插入当做一行行的单条处理,也就没有达到批量插入的效果

jdbc配置示例
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/dew?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true
    username: root
    password: 123456