Eisen's Blog

© 2024. All rights reserved.

集成 spotless gradle intellij

2022 February-07

之前一直使用 google-java-format intellij plugin 与 git commit hook google-java-format-git-pre-commit-hook 配合实现 java 代码的格式化。不过最近在处理 spring security 需要一些自定义格式的时候发现 intellij 的 google-java-format 插件居然不支持 @formatter:off 这样的语法,让我非常头痛。毕竟 web security 的那种「链式调用」如果没有自己的一些格式化会非常难看:

// @formatter:off
http
    .authorizeRequests()
    .antMatchers(HttpMethod.OPTIONS).permitAll()
    .and()
    .httpBasic()
    .and()
    .authorizeRequests()
    .antMatchers("/graphiql", "/graphql").permitAll() // 如果不做自定义,这里的 permitAll 会强制换行
// @formatter:on

如下所示,这样的设置对 google-java-format 插件无效:

intellij config ignore by plugin

一方面由于这个问题,另一方面由于上述所说的 git-pre-commit 的工具对高版本 java 不兼容,于是决定更换格式化工具。

spotless

经过调研决定使用 spotless 有如下几个原因:

  1. 和 gradle 有很好的集成,通过其所提供的 spotless 的 gradle 插件直接跑命令就能格式化代码,这样就可以通过在 ci 执行静态检查保证格式的一致性,起到了类似于 git-pre-commit 的效果,并且不用开发者自行设置,反而更省事了
  2. 支持多种格式化标准,包括 google-java-format 并支持其自定义,这会让我之前的代码风格保持固定
  3. 通过配置可以实现对 @formatter:off 语法的支持

spotless 与 gradle 集成

增加 gradle 的插件:

  plugins {
+       id "com.diffplug.spotless" version "6.2.1"
  }

+ spotless {
+     java {
+         googleJavaFormat()
+     }
+ }

添加后就可以使用以下几个常用命令了:

  1. gradle spotlessJavaCheck: 检查是否有不符合格式的内容,gradle build 也会执行该命令
  2. gradle spotlessJavaApply: 执行格式化修改文件

设置忽略的文件

spotless {
    java {
+       target project.fileTree(project.rootDir) {
+           include '**/*.java'
+           exclude 'build/generated/**/*.*', 'build/generated-examples/**/*.*'
+       }
        googleJavaFormat()
    }
}

通过上面的 include exclude 保证那些生成的文件不必进行格式化。

支持 @formatter 语法

spotless {
    java {
        target project.fileTree(project.rootDir) {
            include '**/*.java'
            exclude 'build/generated/**/*.*', 'build/generated-examples/**/*.*'
        }
+       toggleOffOn('@formatter:off', '@formatter:on')
        googleJavaFormat()
    }
}

通过 toogleOffOn 来支持 @formatter:off 这样的语法,那么通过如下注释包裹的内容将被 spotless 忽略:

// @formatter:off

code here will not be formatted any more

// @formatter:on

spotless 与 intellij 集成

spotless 如果只和 gradle 集成而没有 IDE 的集成,那么在 Intellij 中执行「Reformat」还是会忽略上述的任何格式化设置。不过好在 spotless 也有 Intellij 的插件支持。

安装插件

在插件市场搜索 spotless:

spotless plugin in intellij

修改快捷键映射

安装插件成功后会在「Code」下会多一个「Reformat Code with Spotless」:

reformat with spotless

但没有快捷键,用默认快捷键还是不能调用这个功能,需要修改下快捷键:

keymap spotless

将默认的 reformat 快捷键给 spotless reformat:

set keymap for spotless

提示是否将其他已经使用该快捷键的映射删除,选择「Remove」:

overwrite keymap for spotless


使用 github cli 加速与 github 的交互

2022 February-01

由于我目前的所有开发流程一方面和 github 有密切的关联,另一方面又大量的使用 vim iTerm 这样的工具,因此有很强烈的诉求希望可以更好的集成这些工具,最近刚刚尝试了下 github 官方的 cli 工具 感觉确实可以满足我这方面的需求。这里记录下我自己常用的一些命令。

github cli

常用的工作流程

我目前高频使用 github 如下的一些功能:

  1. 所有的仓库都在 github 里,会创建新的仓库,然后 clone 到本地,或者将本地新创建的仓库在 github 那边对应创建远程仓库
  2. 在 github 创建 issue 并尽量让自己所提交的 commit 和 issue 绑定
  3. 使用 github actions 去执行 ci 流程、构建镜像、打 tag、 做 release

针对这些场景,github cli 都有对应的功能点,下面一一罗列。

快速打开 github 仓库页面

gh browse

为当前目录的仓库创建 github repo

gh repo create <org>/<repo-name> --source . --private

快速创建 issue

个人认为 issue 就是软件开发行业 GTD 的最小单元,原则上超过 5 分钟才能做完的事情都应该有个 issue 与之对应,这样才能将实际的工作更好的反馈到整个项目里。当为了鼓励大家写 issue,issue 的编写是越快越好、越方便越好。gh 就很好的提升了 issue 编写的速度。

gh issue create --title "xxx"

这个工具做的非常细致,在创建 issue 时甚至会要求你去选择 issue template:

select issue template

然后会问你是否需要编辑内容:

create issue edit body

如果要编辑内容会自动的帮你引入模板:

issue template

这里我默认的编辑器是 newovim 在这里编辑可以用到 autopilot 在内的全套 vim 插件,也会很大提升我编辑 issue 的速度。

github actions 的快捷流程

github actions 用到的命令比较多,但都很简单。

查看运行的 github workflow

gh run watch

gh 会让你选择当前正在运行的 workflow 。

查看运行的 workflow 的结果

这么做的场景一般有两个:

  1. 查看列表,确认 action 是跑完了还是没跑完,成功了还是失败了

    gh run list

    gh run list

  2. 一些 action 执行完会有一些输出,比如会打个镜像并且暴露镜像的 tag,我需要获取这个 tag

    gh run view <action-id>

    gh run view

触发 workflow 执行

有些 workflow 是需要主动触发的,可以去 github 网页上触发,但是更方便的方法就是通过命令行工具触发:

# 触发 graphql.yml 的 workflow 并提供参数 `environment=prod`
gh workflow run graphql.yml -f environment=prod

# 触发 bump_version.yml 的 workflow 并提供参数 `version=patch`
gh workflow run bump_version.yml -f version=patch

使用 statefulset 实现 spring boot 项目的主从区分

2022 January-28

最近在做几个小服务的重构,希望把拆出来的小服务放回主服务里面,有这么几个方面的考虑:

  1. 小服务本身和主服务是从属关系,完全服务于主服务,合并回去完全没有什么业务上的障碍
  2. 合并之后感觉业务的内聚性更好了,可以减少一些外部接口,改为模块间直接调用,获取更好的性能
  3. 当然从部署上,部署一个东西总是比部署两个东西要好一些,而且这种小服务真的很小,没有为原来的系统增加什么负担

不过既然想到合并,那么就是回顾下当初为什么拆分成两个:

  1. 感觉有点过度工程化思维了,本来以为这个功能会变得越来越复杂,但事实并没有
  2. 技术上有点点小困难,因为这个小服务不能像主服务那样自由的创建多个副本,原则上一套服务应该只有一个运行,合并到一起似乎做不到,就干脆拿出来了,那合并回来就需要解决这个问题

这里记录的内容基本都是针对上面的多副本处理的,即如何让 spring boot 的项目在可以多副本的情况下只让其中一个副本运行额外的内容。

区分主 / 从服务

所有的服务都是部署在 k8s 里的,之前主服务是一个 Deployment 每个副本没有任何区别,既然考虑到有主从概念,那第一个想到的就是切换为 Statefulset。它每一个副本是的名字是固定的,比如服务叫 main-server 那第一个副本就是 main-server-0 第二个就是 main-server-1 依次类推。每一个副本是固定的,每次做新的部署都会从高序号开始逐个替换。因此,可以把序号 0 的认为是「主服务」,其他就是「从服务」。然后在「主服务」中运行额外的子服务,其他副本则不运行。

StatefulsetPod 启动后其 HOSTNAME 会被修改为其 Pod 的名字,那么对于 main-server-0Pod 里的每个容器里看到的 HOSTNAME 也就是这个名字了。因此 spring boot 就可以通过这个名字最后是不是以 -0 结尾来区分是不是「主服务」进而去做进一步的操作。

不过直接判断 HOSTNAME 就会让测试环境比较尴尬了,不能说每次修改 HOSTNAME 来实现不同的运行模式吧,因此这里还是在 Statefulset 启动的时候做了个处理去设置另外一个环境变量 SERVER_ROLE:

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: main-server
  labels:
    app: main-server
spec:
  serviceName: "main-server"
  replicas: 1
  selector:
    matchLabels:
      app: main-server
  template:
    metadata:
      labels:
        app: main-server
    spec:
      topologySpreadConstraints:
        - labelSelector:
            matchLabels:
              app: main-server
          maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
      containers:
      - name: main-server
        image: openbayes/main-server
+       command: ["sh"]
+       args: 
+       - "-c"
+       - "[ ${HOSTNAME##*-} = '0' ] && export SERVER_ROLE=master; exec web"

...

#{HOSTNAME##*-} 是截取最后一个 - 后面的部分,这个算是 shell 的一个黑魔法吧,相关的内容见 Advanced Bash Scripting Guide。然后如果是 "0" 就设置一个环境变量 SERVER_ROLE=master,然后在执行主程序,就是 web

SERVER_ROLE 也不是随便来的,它对应了 spring boot 项目下 application.ymlserver.role 字段。而这部分就为后面动态加载做了准备。

注意Deployment 切换到 StatefulSet 后,默认的 Rolling Update 策略会发生变化,如果 replica=1 是无法实现无缝部署的,也就是说 main-server-0 会先关掉然后再启动,所以最好还是让 replica>=2

动态加载 Spring Configuration

事实上这部分是 spring boot 的看家本领,它就是通过各种 autoconfiguration 让 spring 的使用变得非常的容易的。这里使用了 @ConditionalOnProperty 注解实现了配置的动态加载。

  @Configuration
+ @ConditionalOnProperty(value = "server.role", havingValue = "master")
  @EnableScheduling
  public class SchedulerConfig {
  
    @Service
    @Slf4j
    public static class Runner {
      @Scheduled(fixedDelay = 5000)
      public void run() {
        log.info("Scheduler is running");
      }
    }
  }

server.role == master 时,该 Configuration 才会生效,并且执行里面的 Runner。效果如下:

完整的 demo 内容见 springboot-scheduler-example