Skip to content

Gauss-Boot 微服务基础框架

Gauss后端微服务框架是基于Spring Boot 2.7.x, Spring Cloud 2020.X和Spring Cloud Alibaba 2021.X的一套后端快速开发框架,用于快速的业务开发。

2.0 版本概述

Gauss Boot 2.x升级后将使用JDK17作为默认编译运行环境,气泡鱼平台已经将新集群编译环境和运行环境修改为JDK17。

Gauss Boot 2.x是Gauss Boot 1.x的升级版本,以下是升级内容:

  • 升级Spring Boot框架到2.7.x
  • 升级默认的Java到JDK17, 默认启用ZGC
  • Flyway默认支持TiDB
  • Redis Starter封装了更多的方法,如锁,序列生成等。
  • 升级Spring Cloud Alibaba 2021.0.5
  • 支持三层结构的脚手架生成(2.0.0-CSA-SNAPSHOT)
  • 支持从Gauss Boot 1.x升级
  • 升级MyBatis Plugs至3.5.3.2
  • 升级POI至4.1.2

架构图

全新架构图

相关技术

名称Gauss Boot 1.x版本Gauss Boot 2.x版本描述
JDK版本8.x17.x气泡鱼运行环境JDK版本在2.x后将使用JDK17
GC算法G1ZGC气泡鱼运行环境默认将使用ZGC, JDK8不支持该算法
Spring Boot2.5.52.7.14
Spring Cloud2020.42021.0.5
Spring Cloud Alibaba Nacos2021.12021.0.5配置中心+注册中心
Swagger Bootstrap UI1.9.6接口文档
Knife4j3.0.3接口文档
MyBatis Plus3.4.33.4.3数据库持久层框架
MyBatis3.5.73.5.7数据库持久层框架
Spring Cloud OpenFeign3.0.43.1.3服务间调用
Spring Cloud LoadBalancer3.0.43.1.3客户端负载均衡
Spring Data Elasticsearch4.2.54.4.14Elasticsearch 持久层框架
Spring Data Redis2.5.52.7.14缓存框架
Xxljob2.3.02.3.0任务调度框架

快速开始

要使用Gauss框架,可以使用maven快速构建

安装maven

可以从官网下载maven的二进制包,或者直接点击这里

配置settings.xml

为了使用内部的maven仓库,需要配置内部仓库地址, 具体参考settings.xml

创建项目

命令行创建

可以通过mvn的命令行创建项目

三层结构(client+service+app)
bash
mvn archetype:generate -DarchetypeGroupId=com.tineco.gauss -DarchetypeArtifactId=tineco-basic-archetype -DarchetypeVersion=2.0.0-CSA-SNAPSHOT -DinteractiveMode=false -DarchetypeRepository=https://nexus3.tineco.com -Dversion=1.0.0-SNAPSHOT -DgroupId=cn.sevenyuan -DartifactId=sevenyuan
两层结构(client+service)
bash
mvn archetype:generate -DarchetypeGroupId=com.tineco.gauss -DarchetypeArtifactId=tineco-basic-archetype -DarchetypeVersion=2.0.0-SNAPSHOT -DinteractiveMode=false -DarchetypeRepository=https://nexus3.tineco.com -Dversion=1.0.0-SNAPSHOT -DgroupId=cn.sevenyuan -DartifactId=sevenyuan

使用idea创建项目

使用idea也可以创建想,参考以下图片 image

模块列表

gauss-boot-dependencies

gauss-boot-client-parent

gauss-boot-parent

gauss-boot-common

gauss-boot-starter

Excel导入导出

since: 2.0.2

导入

通过@ExcelRequest注解可以实现EasyExcel的导入,具体参考EasyExcel的文档。

@RestController
@RequestMapping("/excel")
@Api("上传测试")
public class ExcelController {
    @PostMapping("/upload")
    @ApiOperation("上传文件")
    public Long upload(@ExcelRequest(value = "file",targetClass = DemoExcel.class) Stream<DemoExcel> stream){
        AtomicLong total = new AtomicLong();
        BatchOperate<DemoExcel> excelBatchOperate = BatchOperate.of((data)->{
            System.out.println(data);
            total.addAndGet(data.size());
        });
        // 批量操作
        stream.forEach(excelBatchOperate::addBatch);
        excelBatchOperate.execute();
        return total.get();
    }
}

参数类型必须是List或者Stream. List参数会将数据读至内存,Stream参数将数据读至文件。

导出

通过@ExcelResponse注解可以实现EasyExcel的导出,具体参考EasyExcel的文档。

通过template可以指定导出的模板,该模板是classpath下面的路径 通过limit参数可以设置最大的导出数量

@RestController
@RequestMapping("/excel")
@Api("Excel测试")
public class ExcelController {
    @GetMapping("/download")
    @ExcelResponse(targetClass = DemoExcel.class,template = "template.xlsx")
    public Stream<DemoExcel> download(){
        return ChunkDataIterator.toStream((i)->{
            if (i<10){
                return Lists.newArrayList(new DemoExcel("1"+i,"1"+i,"1"+i),new DemoExcel("2"+i,"2"+i,"2"+i));
            }
            return Lists.newArrayList();
        });
    }
}
自定义导出

同时可以使用StreamExcelExporter类来实现自定义导出

@Service
public class UserLoginHistoryServiceImpl extends ServiceImpl<UserLoginHistoryMapper, UserLoginHistory> implements IUserLoginHistoryService {

    @Override
    @Transactional
    public void download(OutputStream outputStream) throws IOException {
        Stream<UserLoginHistory> stream =  getBaseMapper().selectStream(Wrappers.lambdaQuery());
        // 导出
        StreamExcelExporter excelExporter = new StreamExcelExporter(outputStream,stream.map(data->{
            UserLoginHistoryDTO dto = new UserLoginHistoryDTO();
            BeanUtils.copyProperties(data,dto);
            return dto;
        }),UserLoginHistoryDTO.class);
        excelExporter.export();
    }
}

该示例直接通过Mybatis Stream查询返回流,通过StreamExcelExporter直接将流返回给客户端。

操作日志集成

应用可以通过@AccessLog集成操作日志,通过配置模式推送到kafka,后续会交给日志服务组件持久化。

java
@RestController
@RequestMapping("/swagger")
@Api("文档测试")
@AccessLog(appId = "demo")
public class SwaggerTestController {

    @ApiOperation("测试")
    @GetMapping("/test")
    @AccessLog
    public String test() {
        return "test";
    }
}

日志配置如下:

yaml
spring:
    kafka:
        bootstrap-servers: 10.108.5.41:9092
        consumer:
            group-id: test
            max-poll-records: 100
            properties:
                max.poll.interval.ms: 300000
            auto-offset-reset: latest
gauss:
    log:
        type: kafka

gauss-boot-starter-jdbc

gauss-boot-starter-mybatis

Mybatis的扩展包

流式查询

Mapper类可以继承StreamSupportMapper, 可以使用selectStream方法返回流。

@Mapper
public interface UserLoginHistoryMapper extends StreamSupportMapper<UserLoginHistory> {

}

@Service
public class UserLoginHistoryServiceImpl extends ServiceImpl<UserLoginHistoryMapper, UserLoginHistory> implements IUserLoginHistoryService {

    @Override
    @Transactional
    public void download(OutputStream outputStream) throws IOException {
        Stream<UserLoginHistory> stream =  getBaseMapper().selectStream(Wrappers.lambdaQuery());
        stream.forEach(System.out::println);
    }
}

注意:Stream查询必须在事务里面使用。

gauss-boot-starter-mysql

gauss-boot-starter-oracle

gauss-boot-starter-redis

封装了Redis的基本使用场景

  • 分布式锁
  • 序列生成
  • 延时队列
  • 实时消息

对于常用的redis操作,请使用RedisTemplate实现。 如果要使用高级功能,请开启redisson.

gauss:
  redisson:
    enabled: true

以下功能均是2.x版本提供的api

声明式锁

声明式锁支持对方法进行加锁,同时key支持参数表达式,支持设置自动释放时间等参数。

该功能需要启用redisson

    @RedisLock(key = "test#{['args'][0]}")
    public void test(String abc){
        log.info("TestBean=>>>>>>>>>>>>>>{}",abc);
    }

序列生成

可以使用@RedisSequence来设置序列,支持传递格式参数,如要以一下格式20230101-000000,用一下方法即可,注意方法返回参数必须是String。

    @RedisSequence(key = "test:1")
    public String test1(Date date){
        return DateFormatUtils.format(date,"yyyyMMddHH")+"-%s";
    }

注:实现方法返回一个字符串格式, 并且用"%s"占位序列位置。

延时队列

通过延时队列可以实现延时回调的功能,注意该功能只能适用简易的延时回调场景,如果需要更可靠的延时队列,请使用rabbitmq提供的队列

public String test1(){
        Date date1 = DateUtils.addSeconds(new Date(), 10);
        Date date2 = DateUtils.addSeconds(new Date(), 5);
        Date date3 = DateUtils.addSeconds(new Date(), 8);
        redissonService.publishDelayQueueMessage("test","999999999999999111111111110000", date1);
        redissonService.publishDelayQueueMessage("test","999999999999999111111111115555", date2);
        redissonService.publishDelayQueueMessage("test","999999999999999111111111118888", date3);
        TimeUnit.SECONDS.sleep(100);
}

事件监听器

@Component
@Slf4j
public class TestDelayQueueListener extends AbstractRedisDelayQueueListener {
    @Override
    protected void onMessage(String message) {
        log.info("消息内容:{}",message);
    }

    @Override
    public String getQueueName() {
        return "test";
    }
}

实时流式消息

通过实时流式消息可以实现实时消息通知等场景,需要redis支持Stream类型

public String test1(){
        for (int i = 0; i < 10000; i++) {
            redissonService.publishStreamMessage("test", UUID.randomUUID().toString(),""+i);
        }
}

流式消息监听器

@Slf4j
public class TestStreamListener extends AbstractRedisStreamListener {

    @Override
    public String getStreamName() {
        return "test";
    }

    @Override
    public String getConsumerGroup() {
        return "group1";
    }

    @Override
    protected void onMessage(StreamMessageId id, Map<String, String> message) {
        log.info("{}:{}",id,message);
    }
}

限流器

可以使用@ReidsRateLimiter实现分布式/单机限流

    @RedisRateLimiter(key="test:rate1")
    public void testRate(){
        log.info("testRate=>>>>>>>>>>>>>>{}",count++);
    }

限流器无法修改全局参数,需要删除redis对应key后方可重新设置全局参数。

PubSub消息

通过AbstractPubSubMessageListener可以实现PubSub模式

redisTemplate.convertAndSend("topic1","test");

监听器

@Component
@Slf4j
public class TestPubSubMessageListener extends AbstractPubSubMessageListener {
    @Override
    public void onTextMessage(String topic, String context) {
        log.info("{}:{}",topic,context);
    }

    @Override
    public List<String> getTopics() {
        return Collections.singletonList("topic1");
    }
}

gauss-boot-starter-kafka

简易示例

用于集成spring kafka,基于Spring kafka拓展了@KafkaBatchListener。

注意,使用此客户端后,不能配置spring.kafka.listener.type为batch,框架已经提供了batch listener的集成方式了。

参考配置如下:

yaml
spring:
    kafka:
        bootstrap-servers: 10.108.5.41:9092
        consumer:
            group-id: test
            max-poll-records: 100
            properties:
                max.poll.interval.ms: 300000
            auto-offset-reset: earliest

参考的java实现如下:

java
@Component
@Slf4j
public class TestListener {
    @KafkaBatchListener(topics = "access_log",groupId = "test3")
    public void onMessage(List<ConsumerRecord<String,String>> records){
        log.info("{}",records.size());
    }
    @KafkaListener(topics = "access_log",groupId = "test2")
    public void onMessage(ConsumerRecord<String,String> record){
        log.info("{}",record.value());
    }

}

gauss-boot-starter-elasticsearch

Elasticsearch主要用于全文检索,框架集成了Spring Data Elasticsearch,使得对ES的操作和JPA等方式很相似。

定义一个Entity

java
package gauss.boot.based.elasticsearch.demo;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

@Document(indexName = "gauss_access_log")
@Data
public class AccessLogEntity {
    @Id
    private String id;
    @Field(type = FieldType.Keyword)
    private String userId;
    @Field(type = FieldType.Date,format= DateFormat.epoch_millis)
    private String eventDate;
}

定义Repository

java
public interface AccessLogRepository extends NativeQueryRepository<AccessLogEntity, String> {
}

操作数据

java
@SpringBootTest
class DemoApplicationTests {
    @Autowired
    private AccessLogRepository repository;

    @Test
    void contextLoads() throws Exception{
        String log = "{\"appId\":\"tineco-gauss-demo\",\"function\":\"测试\",\"ip\":\"0:0:0:0:0:0:0:1\",\"message\":\"\",\"method\":\"GET\",\"module\":\"文档测试\",\"url\":\"http://localhost:8080/swagger/test\",\"userId\":\"anonymous\"}";
        AccessLogEntity entity = JSONObject.parseObject(log,AccessLogEntity.class);
        repository.save(entity);
    }
    @Test
    void example(){
        AccessLogEntity entity = new AccessLogEntity();
        entity.setAppId("tineco-gauss-demo");
        Example<AccessLogEntity> example= Example.of(entity, ExampleMatcher.matching().withMatcher("appId", ExampleMatcher.GenericPropertyMatchers.exact()));
        List<AccessLogEntity> entityPage=repository.findAll(example);
        log.info("count:{}",entityPage.size());
    }

    @Test
    void examplePage(){
        AccessLogEntity entity = new AccessLogEntity();
        entity.setAppId("tineco-gauss-demo");
        Example<AccessLogEntity> example= Example.of(entity, ExampleMatcher.matching().withMatcher("appId", ExampleMatcher.GenericPropertyMatchers.exact()));
        Page<AccessLogEntity> entityPage=repository.findPage(example,PageRequest.of(0,100));
        log.info("count:{}",entityPage.getTotalElements());
    }


    @Test
    void exampleStream(){
        AccessLogEntity entity = new AccessLogEntity();
        entity.setAppId("tineco-gauss-demo");
        Example<AccessLogEntity> example= Example.of(entity, ExampleMatcher.matching().withMatcher("appId", ExampleMatcher.GenericPropertyMatchers.exact()));
        Stream<AccessLogEntity> entityPage=repository.findStream(example);
        log.info("count:{}",entityPage.count());
    }
}

gauss-boot-xxljob

用于集成定时任务调度,由xxl job admin作为调度中心。 在框架中使用要引入下面starter

        <dependency>
            <groupId>com.tineco.gauss</groupId>
            <artifactId>gauss-boot-starter-xxljob</artifactId>
        </dependency>

定义一个执行器

通过@XxlJob注解定义客户端执行器

java
@Component
public class SampleXxlJob {
    private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class);


    /**
     * 1、简单任务示例(Bean模式)
     */
    @XxlJob("demoJobHandler")
    public void demoJobHandler() throws Exception {
        XxlJobHelper.log("XXL-JOB, Hello World.");

        for (int i = 0; i < 5; i++) {
            XxlJobHelper.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
        // default success
    }

}

配置客户端属性

yaml
xxl:
    job:
        admin-addresses: http://10.108.5.41:8850/xxl-job-admin #服务端地址
        access-token: #执行器访问服务端的密钥(一般不需要配置)
        app-name: xxl-job-executor-sample #执行器名称,需要和服务端配置一致
        log-path: d:/logs/xxljob # 执行器日志路径
        address: #执行器注册地址,如果为空,则使用ip:port注册
        ip: # 执行器的回调ip
        port: # 执行器的回调端口
        log-retention-days: 30 # 日志保存时间

配置服务端

新增执行器 image 新增执行器的定时任务配置和参数 image 查看执行日志 image

gauss-boot-rabbitmq

队列、交换机、路由定义

普通队列定义

通过什么Bean定义主题、交换机和路由

java
public class Config{
    /**
         * 队列 起名:testDirectQueue
         */
        @Bean
        public Queue testDirectQueue() {
            //一般设置一下队列的持久化就好,其余两个就是默认false
            return new Queue("testDirectQueue", true);
        }

        /**
         * Direct交换机 起名:testDirectExchange
         */
        @Bean
        DirectExchange testDirectExchange() {
            return new DirectExchange("testDirectExchange", true, false);
        }

        /**
         *  绑定  将队列和交换机绑定, 并设置用于匹配键:bindingDirect
         */
        @Bean
        Binding bindingDirect() {
            return BindingBuilder.bind(testDirectQueue()).to(testDirectExchange()).with("testDirectRouting");
        }
}

发送消息

java
@SpringBootTest
class DemoApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void contextLoads() throws Exception{
        Book book =new Book("111");
        this.rabbitTemplate.convertAndSend("testDirectExchange", "testDirectRouting", book);
        Thread.sleep(100000);
    }

}

消费消息

java
@Component
@Slf4j
public class TestListener {

    @RabbitListener(queues = "testDirectQueue")
    @RabbitHandler
    public void listen(BookVO book){
        log.info("{}",book);
    }
}
延时队列定义

通过指定队列的属性x-dead-letter-exchange定义转发交换机名称,x-dead-letter-routing-key定义转发的路由名称,x-message-ttl定义消息过期时间

java
public class DelayConfig{
    /**
     *  队列 起名:testDelayQueue
     */
    @Bean
    public Queue testDelayQueue() {
        Map<String, Object> params = new HashMap<>();
        // x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称,
        params.put("x-dead-letter-exchange", "testDirectExchange");
        // x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。
        params.put("x-dead-letter-routing-key", "testDirectRouting");
        // 声明过期时间5秒,注意不同过期时间的消息请声明不同的延时队列
        params.put("x-message-ttl", 5000);
        return new Queue("testDelayQueue", true, false,false,params);
    }

    /**
     *  Direct交换机 起名:testDelayExchange
     */
    @Bean
    DirectExchange testDelayExchange() {
        return new DirectExchange("testDelayDirectExchange", true, false);
    }

    /**
     * 绑定  将队列和交换机绑定, 并设置用于匹配键:TestDirectRouting
     */
    @Bean
    Binding bindingDelayDirect() {
        return BindingBuilder.bind(testDelayQueue()).to(testDelayExchange()).with("testDelayDirectRouting");
    }
}

2.x升级指南

Gauss Boot支持版本升级,如果低版本升级请参考下面的文档

修改pom版本

修改client和service里面的parent版本

client版本

    <parent>
        <groupId>com.tineco.gauss</groupId>
        <artifactId>gauss-boot-client-parent</artifactId>
        <version>2.0.2-PREVIEW</version>
        <relativePath />
    </parent>

service版本

    <parent>
        <groupId>com.tineco.gauss</groupId>
        <artifactId>gauss-boot-parent</artifactId>
        <version>2.0.2-PREVIEW</version>
        <relativePath />
    </parent>

修改Dockerfile

FROM nexus3.tineco.com/openjdk:17-jdk-slim-sw
...
ENV JAVA_OPTS "-Xmx2048m -Xss512k -XX:+UseZGC"
...

修改.rancher-pipleline.yml

...
  - runScriptConfig:
      image: nexus3.tineco.com/maven:3.8-openjdk-17-slim
...

升级常见问题

启动时报错Connection is unregistered.或Client not connected,current status:STARTING.

原因是客户端gRPC无法和服务端创建连接,请先使用telnet ${nacos.server.address}😒{nacos.server.grpc.port}进行测试,查看网络是否畅通,服务端端口是否已经正确监听。

若服务端没有问题,查看配置是否有误,服务端和客户端的所配置的端口应一致。

若配置也没有问题,查看是否有防火墙或VIP端口转发问题,Nacos2.0的gRPC端口均通过主端口的偏移量计算产生,因此端口转发也需要满足该偏移量。

遇到错误:java.lang.invoke.SerializedLambda.capturingClass accessible: module java.base does not "opens java.lang.invoke" to unnamed module

这个错误是由于MyBatis Plus 3.4.3不支持JDK17, 需要升级MyBatis Plus版本至3.5.3.2或者增加JVM参数

--add-opens java.base/java.lang.invoke=ALL-UNNAMED

升级2.x后Mybatis Plus的一些类和方法报错

由于3.4.3的Mybatis Plus不支持JDK17,故我们升级了版本至3.5.3.2, 主要的不兼容如下:

  • selectCount返回Long而不是Integer
  • PageDto变成PageDTO