📌 术语定义

Spring Boot

Spring Boot 是启动 Spring 项目的工具,是一些库的集合,能够被任意项目的构建系统所使用,用于简化 Spring 应用的初始搭建以及开发过程,使研发不再需要定义样板化的配置。更多详情请参见 Spring 官网

starter

starterSpring Boot 提供的一种管理依赖的方式,借助 Spring Boot 中自动配置的能力,各个功能组件都直接以 starter 的形式存在,使用时只需要引入一个 starter 依赖即可。
Spring Boot 提供了很多官方的 starter 内容。
Shoulder 中,各个功能也是以 starter 的形态存在的,starter 中包含了该模块所需的所有 jar 与相关完整依赖、组件的初始化逻辑以及一些默认配置参数等。更多关于 starter 的描述和逻辑关系请参见 Spring Boot Starters 内容。

本文档讲述内容与代码示例可参见 Shoulder-Demo1

能力激活方式:引入 Shoulder 任意一个 starter

Maven
<dependency>
    <groupId>cn.itlym</groupId>
    <artifactId>shoulder-starter</artifactId>
    <version>0.8.1<version>
</dependency>
Gradle
compile 'cn.itlym:shoulder-starter:0.8.1'

全局ID生成器

Shoulder 提供了两类开箱即用的全局id生成器:StringGuidGeneratorLongGuidGenerator,您可以像这样使用它们。

    @Autowired
    private StringGuidGenerator stringGuidGenerator;

    @Autowired
    private LongGuidGenerator longGuidGenerator;

    public void testGuid() {
        long guidLong = longGuidGenerator.nextId();
        String guidString = stringGuidGenerator.nextId();
        // 批量获取
        long[] guidLongArr = longGuidGenerator.nextIds(10);
        String[] guidStringArr = stringGuidGenerator.nextIds(10);
    }

动态字典

Shoulder 提供了一套开箱即用的字典管理能力,极大的降低了字典模块的编码成本。如:

  • 枚举 ←→ String/Number 自动转换

  • 可定制的存储模式,并提供多种默认的存储模式实现:

    • 适用于集群部署模式的DB模式

    • 适用于高并发场景的缓存查询能力

    • 适用于单机部署的无DB模式

  • 结合 Shoulder-Web 获得更丰富的功能:

    • 支持 JSR 303 规范的校验注解

    • 开箱即用的字典搜索、管理接口

    • 可视化的查询页面

核心接口

  • 字典类型(配置类型):DictionaryType

  • 字典项(配置项)DictionaryItem

如果是变化较少的场景,可采用以枚举代码为存储,枚举直接继承 DictionaryItemEnum 便能获得大量通用方便的类型转换工具代码(id → Enum、name → Enum、enum.order → Enum、compareWithXXX…​)。

静态字典-枚举模式

轻松实现枚举 & 字典转换,无需再为编写大量转换代码而担忧,保留代码原本的高可读性。

如对以下枚举可以直接在 Controller 的参数中接收多种格式

字典枚举示例
import org.shoulder.core.dictionary.model.NameAsIdDictionaryItemEnum;

public enum MyColorEnum implements NameAsIdDictionaryItemEnum<DictionaryTestEnum> {

    BLUE, YELLOW, GREEN, RED, BLACK, PINK, GOLDEN,
    ;
    @Override
    public String getName() {
        return name();
    }
    @Override
    public Integer getDisplayOrder() {
        return ordinal();
    }
}

这两种访问都能正确访问到接口

  • /color/updateFavorite?color=BLUE

  • /color/updateFavorite?color=0

初尝甜点
@RestController
public class MyColorController {

    @RequestMapping("/color/updateFavorite")
    public String updateFavorite(MyColorEnum color) {
        // /color/updateFavorite?color=BLUE
        // /color/updateFavorite?color=0
        return color.name();
    }
}
更多能力-校验
     /**
     * 接口中不使用枚举,使用DTO
     * 还支持设置 allowCodes、forbiddenCodes 来实现更有意思的设计,如 GOLDEN 只能由系统发放,而不能由用户设置。
     */
     @RequestMapping("/color/updateFavorite2")
     public String updateFavorite2(@DictionaryEnumItem(value=MyColorEnum.class, forbiddenCodes={"GOLDEN"}) DictionaryItemDTO colorDto) {
         return color.name();
     }
 }

增强的类型转换器

Spring 官方 ConversionService 接口基础上新增更多类型转换,如:

  • String ←→ DataSize ←→ Long

  • String ←→ Date ←→ LocalDateTime ←→ Instant

  • String ←→ Date ←→ LocalDate ←→ Instant

  • 支持多级 ConversionService 供高级用户使用已有的 ConversionService 并支持自定义优先级

且这些额外的转换代码几乎都是 JDK / Sping 代码中的标准工具类,所以也不需要担心稳定性和 BUGShoulder 拥抱复用与优雅,杜绝造轮子。

全局锁

基于 JDK 的标准 Lock 接口,提供了 ServerLock,在其基础之上增加了更丰富的并发接口:如 tryLockisHoldLock ,且支持通过配置 cluster=true/false 快速支持集群模式的锁。

ServerLock
    @Autowired
    private ServerLock serverLock;

    public void tryLock() {
        LockInfo lockInfo = new LockInfo(shareKey, HLOD_LOCK_DURATION);
        jdbcLock.lock(lockInfo);

        try {
            // 做你的事情
            String xx = "do something";
        } catch (Exception) {
            log.xxx
        } finally {
            jdbcLock.unlock(lockInfo);
        }
    }
默认为 单机模式,若想支持集群,可以引入数据库或 Redis 相关依赖,设置 shoulder.lock.type=jdbc/reedis 即可,无需修改任何带代码。
基于此,即使开始为单机项目,也可以不需要修改任何代码支持集群模式的全局锁。Shoulder 默认提供了以下几个实现:
  • MemoryLock:基于内存的锁,主要用于演示工程中使用,不支持集群。

  • JdkLock: 基于 JDKReentrantLock,不支持集群。

  • FileSystemLock: 基于 操作系统-文件系统 的锁实现,支持同一机器的多个应用共享资源控制,适用于小型项目,无数据库场景。

  • JdbcLock: 基于 DB + JDBC 的分布式锁实现,支持集群,高可用。

  • RedisLock: 基于 Redis 的分布式锁实现,支持集群,高并发下与 JdbcLock 相比性能更佳,实现借鉴了 Redisson

MemoryLock 仅在 Shoulder Demo、测试使用!演示 Shoulder 代码,不要在生产环境使用!

运行上下文 AppContext

Shoulder 提供了基于 ThreadLocalAppContext,用于存储单次业务处理信息的上下文,可据此实现 tracer 追踪流量染色压测识别流量血缘识别 等高级能力。

字典枚举示例
public void createUser(CreateUserRequest req, HttpHeaders httpHeaders) {
    // 操作上下文
    AppContext.setUserId(httpHeaders.get("userId"));
    AppContext.setUserName(httpHeaders.get("userName"));
    AppContext.setTenantCode(httpHeaders.get("tenant"));
    AppContext.setLocale(httpHeaders.get("language"));
    AppContext.setTraceId(httpHeaders.get("x-trace-id"));

    testB(req);
}

public void testB(CreateUserRequest req) {
    // 从上下文获取
    AppContext.getUserId();
    AppContext.getUserName();
    AppContext.getTenantCode();
    AppContext.getLocale();
    AppContext.getTraceId();
}

日志

Shoulder 使用 Slf4j 操作日志系统,故您可以自行选择 LogBackLog4JCommonns-LoggingJUL…​ 默认日志是 Spring Boot 的默认值:LogBack

扩展的 Logger

Shoulder 内置的 org.shoulder.core.log.LoggerSlf4j 基础上,额外扩展了关于 org.shoulder.core.exception.ErrorCode 的打印方法。

部分代码法示例:

public interface Logger extends org.slf4j.Logger {
    void error(ErrorCode errorCode);
    void error(ErrorCode errorCode, Throwable t);
    void error(String errorCode, String msg);
    //...
}
请尽情享用 ShoulderLogger ,以便统一您的日志格式,更有效管理和分析日志,无需再为 日志格式 问题发愁。

默认 Logger

Shoulder 默认日志输出已经为您自动配置好一些 Logger,直接在 org.shoulder.core.log.AppLoggers 使用即可。

示例代码:

    AppLoggers.APP_SERVICE_DIGEST.info("url={}, success={}, cost={}, result={}", requestUrl, true, cost, returnStr);

日志文件划分

Shoulder 默认提供了 Logback 的配置文件,无需再考虑日志内容分类与文件拆分,默认会按照以下划分日志文件:

可以在 shoulder-autoconfigurationlogback-spring.xml 查看配置内容。

该功能基于 Spring Boot,但 Spring Boot 官方文档说明该配置不能与 Logback配置扫描 一起使用。故在使用时请不要主动开启 Logback配置扫描

应用日志

打印在 ${logging.file.path}/${shoulder.application.name} 目录中

LoggerName 打印建议 文件名

APP_DEFAULT

APP启动/重启等时逻辑,默认包路径日志打在这里

default.log

APP_WARN

APP启动/重启等运维时逻辑

warn.log

APP_ERROR

所有需要关注的 error 日志都在这里出现

error.log

APP_BIZ

业务逻辑处理过程

biz.log

APP_ROUTER

业务路由,新老链路/逻辑切流日志:决策因子、规则、切流结果

router.log

APP_INTEGRATION

调用其他应用 / 服务的入参、出参、错误码等详细信息

integration.log

APP_INTEGRATION_DIGEST

调用其他应用 / 服务的接口、方法、成功、耗时等关键信息

integration-digest.log

APP_SERVICE

对外暴露接口/服务的日志详情,入参、出参等

service.log

APP_SERVICE_DIGEST

对外暴露接口/服务的接口、方法、成功、耗时等关键信息

service-digest.log

APP_SERVICE_WARN

对外暴露接口/服务的接口执行出现预期内异常记录(参数校验失败等)

service-warn.log

APP_SERVICE_ERROR

对外暴露接口/服务的接口执行失败记录

service-error.log

APP_DAL

数据访问层-详细日志

dal.log

APP_DAL_DIGEST

数据访问层-摘要日志

dal-digest.log

APP_CAL

数据访问层-缓存日志

cal.log

APP_CAL_DIGEST

数据访问层-缓存-摘要日志

cal-digest.log

APP_DAEMON

定时任务触发记录

daemon.log

APP_MSG_CONSUMER

消费消息记录,如消息内容,消息来源等

msg-consumer.log

APP_MSG_PRODUCER

发布消息记录,如消息内容等

msg-producer.log

APP_NOTIFY

向其他系统发送通知,如外部

notify.log

APP_CONFIG

收到配置变更,读取到配置中心的配置内容

config.log

APP_CODE_MAPPING

错误码映射记录,如调用 x 应用返回 errorCode_123,对于本服务接口应该返回 errorCode_567 时记录

code-mapping.log

Shoulder 默认将其他组件的日志(如 redisson)打印在 ${logging.file.path}/other/common.log 中,若您需要特殊管理,按照 Spring Boot 提供的方案自行定义 logger 即可。

Shoulder 内置了一些仅适用于框架内部使用 Logger,若您想了解,可前往 Shoulder 框架内置日志说明

错误码前缀

为了降低编码 + 维护成本,Shoulder 提供了统一的规范类,对于自定义错误码枚举/异常可用直接继承 org.shoulder.core.exception.ErrorCode 接口即可,并且 Shoulder 提供了错误码多语言、转异常等丰富功能。

并发工具

Threads 线程工具类

提供了常用并发操作的封装,确保安全、高效地执行并发任务,以简化Java并发编程。

Threads 提供了多种线程池拒绝策略实现,如 Discard, DiscardOldest, AbortBlock,以多样应对线程池任务队列满的情况。
您可以通过 setExecutorService() 方法更改默认内置线程池。
Threads 类依赖于 ContextUtilsLogHelper 进行上下文获取和日志美化,确保这些类的可用。

异步执行

您可以通过 Threads.execute() 让代码在异步执行,如希望异步打印 "hello, shoulder!"

异步执行示例
    Threads.execute(() -> System.out.printLn("hello, shoulder!"));

延迟执行

您可以通过 Threads.delay() 让代码延迟执行,如希望一分钟后打印 "hello, shoulder!"

延迟执行示例
    Threads.delay(() -> System.out.printLn("hello, shoulder!"), Duration.ofMinutes(1));

批量异步执行且等待

您可以通过 Threads.executeAndWait() 让代码延迟执行,如希望执行一批任务,10个线程分别打印 "shoulder-processing-task-i"(i: 1~10):

批量异步执行且等待-示例
    List<Runnable> tasks = IntStream.range(1, 11)
        .mapToObj(i -> new Runnable(() -> System.out.println("shoulder-processing-task-" + i)))
        .toList();
    Threads.executeAndWait(tasks);

全局异步增强器 ThreadEnhancer

若希望所有异步线程执行前后做一些操作,可以实现 org.shoulder.core.concurrent.enhance.ThreadEnhancer 接口,如 监控记录、自动拷贝/清理 ThreadLocal 等高级能力。

以下是实现【自动统计在线程池队列内等待时间等指标】的例子:

异步增强器-示例
/**
 * 自动统计在线程池队列内等待时间等指标
 */
public class MonitorRunnableEnhancer implements ThreadEnhancer {
    @Override
    public EnhancedRunnable doEnhance(EnhancedRunnable runnable) {
        return new DefaultMonitorableRunnable(runnable);
    }
    @Override
    public <T> EnhancedCallable<T> doEnhance(EnhancedCallable<T> callable) {
        return new DefaultMonitorableCallable<>(callable);
    }
}
//  将该类注入进 Spring 上下文
@Configuration
public class MonitorRunnableAutoConfiguration {
    @Bean
    public MonitorRunnableEnhancer enqueueTimeEnhancer() {
        return new MonitorRunnableEnhancer();
    }
}

工具类

Shoulder 提供了一些高频的工具类(基于成熟的工具包:apache-commonsSpring 内的工具类google.guavahutools

对于 JSON 处理,与 Spring 的默认选型一致:采用安全、扩展良好的 jackson

扩展:为什么选 jackson 而非 fastjson 作为默认
  • 官方默认:jacksonSpring 充分考量后的默认选择。

  • 安全方面:近些年 jackson 出现恶行漏洞较 fastjson 更少。

  • 扩展性:jackson 有更多的扩展接口,如字段转换、映射等等大量的高级功能。

  • 代码质量:jackson 代码更规范,版本升级兼容性问题更少。

  • 社区成熟与稳定性:jackson 代码活跃维护者更多,不需要担心无人维护。

  • 整体成本:考虑到两者的序列化性能几乎一致,反序列化 fastjson 性能仅略优,结合代码维护人力成本、机器成本看,jackson 远低于 fastjson

  • 当然:FastJson 由于其简单的用法在一些非开放网络环境下、工具型软件、或非长期维护的代码确实更香!请合理选择。

shoulder core util

Shoulder 在 JSON 序列化上默认使用的是 Jackson(与 Spring-Boot 一致)。

多语言/国际化工具 Translator

简化您的多语言应用开发 —— Translator,精心设计的接口,在 Spring 的国际化功能(MessageSource)基础上升级,使用更简单、便捷。

配置

在您的 resources 目录下添加 language/zh_CN/loginpage.properties

properties
i18n.page.button.login=您好,{}!

使用

i18n
@Autowried
private Translator translator;

// 返回:您好,shoulder!
// 若以上配置未完成,返回 welcome, shoulder
public String genHelloTip() {
    String msg = "i18n.page.button.login";
    String userName = "shoulder";

    // 无需传入语言,默认从上下文中取 AppContext.getLocaleOrDefault()
    return translator.getMessageOrDefault(msg, "welcome, {}", userName);
}

应用元信息 AppInfo

Shoulder 提供了获取应用元信息的统一门面:org.shoulder.core.context.AppInfo

取应用基本信息示例代码
    // 应用id:默认使用 spring.application.name
    String appId = AppInfo.appId();
    // 应用版本号:默认使用 v1
    String version = AppInfo.version();
    // 应用示例id:默认为 0
    String instanceId = AppInfo.instanceId();
    // 运行环境,如 dev、test、prod
    String env = AppInfo.env();

借助 Spring-Boot,您可以在 application.xml / yml 中配置他们,应用启动时,Shoulder 将自动从 application.properties 中获取这些配置项。

扩展:Shoulder 的理论指南-基础软件管理规范 中讲述了应用相关的概念和定义。

更多应用元信息配置请参考 应用元信息配置总览