spring boot3 / spring cloud遇到的一系列问题记录(二) —— 努力成为优秀的架构师

2023-11-16 20:30:27 浏览数 (2)

Spring Cloud

注:本章内容承接 spring boot / spring cloud遇到的一系列问题记录(一) —— 努力成为优秀的架构师

由于数据库字段有限,特此进行拆分。 完整源码参考 https://github.com/ShyZhen/scd

搭建配置中心 spring-cloud-config-server

目前我们的项目是微服务架构,如果每个项目都有自己的配置文件,首先管理起来麻烦,其次不够高端,于是需要搭建一个统一管理配置的服务,也就是我们的config模块。

  • (1)在config模块的pom文件中引入依赖
代码语言:javascript复制
        <!--  在config模块中引入spring-cloud-config-server依赖,搭建一个配置服务器  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
  • (2)在启动类标注@EnableConfigServer注解

/scd/config/src/main/java/com/litblc/config/ConfigApplication.java

代码语言:javascript复制
// 标注@EnableConfigServer搭建配置服务器
@EnableConfigServer
@SpringBootApplication
public class ConfigApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigApplication.class, args);
    }
}
  • (3)配置文件设置本地文件读取配置,也可以设置其他方式,比如github读取、数据库读取等

更多配置参考文档 https://springdoc.cn/spring-cloud-config/

代码语言:javascript复制
# 配置服务器的端口,通常设置为8888:
server:
  port: ${APP_PORT:8888}

spring:
  application:
    name: config-server
  profiles:
    # 从本地文件读取配置时,Config Server激活的profile必须设定为native:
    active: native
  cloud:
    config:
      server:
        # 禁用 JdbcEnvironmentRepository 的自动配置。
        jdbc:
          enabled: false
        native:
          # 设置配置文件的搜索路径:
          search-locations: file:./config-repo, file:../config-repo, file:../../config-repo
 
  # 参考 `步骤(7)`,可以删掉这段配置项
  # 依赖库中有spring-boot-starter-jdbc,必须配置本项目的DataSource
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/fmock
    username: root
    password: root
  • (4)配置本地配置文件目录 在上面search-locations中我们配置了config-repo文件夹,根据我们项目名字的不同,创建几个配置文件(文件名和项目名要对应上),代码结构如下

一些示例配置源码如下:

/scd/config-repo/application.yml

代码语言:javascript复制
# 通用common configuration
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/fmock
    username: root
    password: root

  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      #password:
      database: ${REDIS_DATABASE:0}
      database1: ${REDIS_DATABASE1:1}

storage:
  local:
    # 文件存储根目录:
    root-dir: ${STORAGE_LOCAL_ROOT:/var/storage}
    max-size: ${STORAGE_LOCAL_MAX_SIZE:102400}
    allow-empty: false
    allow-types: jpg, png, gif

mybatis-plus:
  mapper-locations: classpath:/mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true

/scd/config-repo/fmock.yml

代码语言:javascript复制
# fmock configuration
# http://localhost:8888/fmock/default
server:
  port: ${APP_PORT:8081}
  servlet:
    context-path: /api

/scd/config-repo/push.yml

代码语言:javascript复制
# push configuration
# http://localhost:8888/push/default
server:
  port: ${APP_PORT:8083}
  servlet:
    context-path: /api
  • (5)让其他项目使用配置中心的服务

完成上一步我们已经配置好了配置中心,启动项目输入http://localhost:8888/fmock/default即可看到生效的配置,application.yml的配置是会一起返回的(访问地址跟文件名对应)

接下来我们要做的是让我们的fmock模块、push模块使用上配置中心服务

首先需要在fmock/pom.xml添加客户端依赖,否则无法解析本地配置的import: configserver:xxx参数

代码语言:javascript复制
        <!-- 使用配置中心,需要依赖SpringCloud Config客户端 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>

然后修改模块的fmock/src/main/resources/application.yml配置文件

代码语言:javascript复制
spring:
  application:
    # 必须设置app名称,要跟config-repo中的`文件名`对应:
    name: ${APP_NAME:fmock}
  config:
    # 导入Config Server地址:
    import: configserver:${CONFIG_SERVER:http://localhost:8888}

push/src/main/resources/application.yml

代码语言:javascript复制
spring:
  application:
    # 必须设置app名称,要跟config-repo中的`文件名`对应:
    name: ${APP_NAME:push}
  config:
    # 导入Config Server地址:
    import: configserver:${CONFIG_SERVER:http://localhost:8888}
  • (6)启动fmock或者push项目即可自动匹配
  • (7)想区分环境,只需要更改模块的名字,并添加一个对应的配置文件即可

比如添加config-repo/fmock-dev.yml开发环境配置文件,然后更改fmock模块的spring.application.name=fmock-dev即可。

  • (8)优化补充

我们在(3)步骤中存在依赖库中有spring-boot-starter-jdbc,必须配置本项目的DataSource的配置,

这个jdbc并不是spring-cloud-config-server引起,因为引入的是可选<optional>true</optional>

实际上问题是之前我们的parent模块中统一引入了mybatis-plus依赖,导致间接引入了jdbc依赖。

正确处理方案是删除parent中的mybatis-plus依赖,放在dependencyManagement中,其他模块需要的时候单独引入。

这样就可以删除配置中心配置了无用的datasource字段问题。

如何调用其他模块的服务、方法等

总结:直接引用调用是不行的,毕竟不是一个jar包,想要访问其他模块的服务,只能通过http请求,使用类似openfeign的包;common模块或者其他模块能使用,是因为它就是单独的代码,并没有启动类,没启动服务所以,所以没有进入spring容器也无法使用注解,也不涉及IP和端口之类的。

参数接收@PathVariable@RequestParam@RequestBody的使用

  • @PathVariable是path路径参数,在路由中直接体现出来
  • @RequestParam是url参数,一般形如?xx=1&xx=2
  • @RequestBody是请求体参数,也就是postman中的raw格式

这里我们重点介绍@RequestBody,在使用他之前,必须定义raw的参数结构。

这里我们在common模块中创建一个bean:

/common/src/main/java/com/litblc/common/requestBean/test/TestRaw.java

代码语言:javascript复制
package com.litblc.common.requestBean.test;

public class TestRaw {
    public long id;
    public long userId;
    public String nickname;
    public String avatar;
}

然后在使用该bean的模块中引入common依赖(我是在push模块)

代码语言:javascript复制
        <!-- 内部模块引入 -->
        <dependency>
            <groupId>com.litblc</groupId>
            <artifactId>common</artifactId>
        </dependency>

控制器使用示例

代码语言:javascript复制
    @Operation(summary = "三种接受参数测试")
    @PostMapping(value = "/raw/{path_id}/{sort_type}")
    public TestRaw raw(
            @PathVariable(value = "path_id") @Parameter(description = "path参数可以多个") long pathId,
            @PathVariable(value = "sort_type") @Parameter(description = "path参数") String sort_type,
            @RequestParam(value = "page", required = false, defaultValue = "1") @Parameter(description = "url参数可以多个") long page,
            @RequestParam(value = "page_size", required = false, defaultValue = "15") @Parameter(description = "url参数") long pageSize,
            @RequestBody TestRaw testRaw
            ) {

        System.out.println(pathId);
        System.out.println(sort_type);
        System.out.println(page);
        System.out.println(pageSize);
        System.out.println(testRaw.nickname);

        return testRaw;
    }

优化文档knife4j

我们从一开始使用的是springboot推荐的默认文档包springdoc-openapi-starter-webmvc-ui,这个包里集成了swagger-ui,但是用着不太方便,于是这里我们尝试换成knife4j。

  • 版本疑惑

我们在老项目中经常看到knife4j-spring-boot-starter或者knife4j-openapi2-spring-boot-starter这两个包,是因为该项目使用的是springboot2。

我们目前使用的是springboot3,需要使用knife4j-openapi3-jakarta-spring-boot-starter这个包。

参考官网 https://doc.xiaominfo.com/docs/quick-start/start-knife4j-version#21-spring-boot-2x

我们也可以从源码上看到一些端倪:

knife4j-spring-boot-starter引用的是旧版knife4j,其中properties规定java版本1.8

knife4j-openapi2-spring-boot-starterknife4j-openapi3-jakarta-spring-boot-starter虽然都引入的最新版knife4j,

默认的java<knife4j-java.version>1.8</knife4j-java.version>也是1.8

但是,后者覆盖了properties中的<knife4j-java.version>17</knife4j-java.version>版本为17。

感兴趣的朋友可以自己查看,这里不放图了。

  • 单个springboot项目使用

首先引入依赖knife4j-openapi3-jakarta-spring-boot-starter

代码语言:javascript复制
        <!-- 接口文档 swagger UI 本地访问 http://ip:port/swagger-ui/index.html-->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.1.0</version>
        </dependency>

        <!-- 接口文档 knife4j测试 http://ip:port/doc.html-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
            <version>4.3.0</version>
        </dependency>

现在其实就可以使用了,现在访问http://ip:port/doc.html即可访问knife4j的优化文档。

注:我们曾经引入过springdoc-openapi-starter-webmvc-ui依赖,访问http://ip:port/swagger-ui/index.html依然可以用默认的swagger。

  • 最后是可选的自定义配置

单个项目使用不需要配置,使用默认的即可,如果需要其他配置可以参考官网:

增强特性https://doc.xiaominfo.com/docs/features/enhance

代码语言:javascript复制
# http://ip:port/swagger-ui/index.html
springdoc:
  swagger-ui:
    path: /swagger-ui.html
    tags-sorter: alpha
    operations-sorter: alpha
  api-docs:
    path: /v3/api-docs
  group-configs:
    - group: 'springbootstudy'
      paths-to-match: '/**'
      packages-to-scan: com.example.springbootstudy    # packages-to-scan 默认为启动类所在的路径
    - group: 'project2'
      paths-to-match: '/**'
      packages-to-scan: com.example.springbootstudy

# knife4j的增强配置,继承springdoc的配置  http://ip:port/doc.html
knife4j:
  enable: true
  setting:
    language: ZH_CN    # EN

单个springboot项目多个模块的文档配置

网上都是springboot2然后写配置类的文档,都一个样,生气,还得靠自己多思考多尝试。

在上面的“自定义配置”中,我们有个配置group-configs并没有具体说明,参数packages-to-scan就是扫描的模块地址。

  • 有时候工程量比较大的时候会配置多个模块,用一个启动类管理,比如如下代码架构(主要是启动类提出来):
  • 然后更新配置文件,主要是设置group-configs:
代码语言:javascript复制
# http://ip:port/swagger-ui/index.html
springdoc:
  swagger-ui:
    path: /swagger-ui.html
    tags-sorter: alpha
    operations-sorter: alpha
  api-docs:
    path: /v3/api-docs
  group-configs:
    - group: 'springbootstudy模块'
      paths-to-match: '/**'
      packages-to-scan: com.example.springbootstudy    # packages-to-scan 默认为启动类所在的路径
    - group: 'push模块'
      paths-to-match: '/**'
      packages-to-scan: com.example.push

# knife4j的增强配置,继承springdoc的配置  http://ip:port/doc.html
knife4j:
  enable: true
  setting:
    language: ZH_CN    # EN
  • 然后访问 http://ip:port/doc.html 和 http://ip:port/swagger-ui/index.html 发现已经成功搭建分组文档

题外话:这里我们加了一个push模块,可以测试@ComponentScan注解的使用

代码语言:javascript复制
// 设置了这个就只扫描push包了,srpingbootstudy包就不会加载了,任何springbootstudy中的方法都不会生效
// @ComponentScan(value = "com.example.push") 
@SpringBootApplication
public class SpringBootStudyApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootStudyApplication.class, args);
    }
}

spring cloud 配置knife4j

使用缓存@EnableCaching@Cacheable

参考https://springdoc.cn/spring-cache-redis-json/

spring的Spring Cache包可以设置多种缓存模式,我们使用redis的方式。注意前提是配置好redis能用。

  • springboot依赖是spring-boot-starter-cache
代码语言:javascript复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
  • 在配置文件中进行配置cache
代码语言:javascript复制
spring:
  # 缓存设置
  cache:
    type: redis
    redis:
      # 缓存有效的时间,默认永久有效,默认单位为毫秒,如60000=1m
      time-to-live: 5m
      # 如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
      key-prefix: "fmock:"
      use-key-prefix: true
      # 是否缓存空值,防止缓存穿透
      # cache-null-values: true
  • 现在就能使用了,只不过存的不是json,不方便阅读,需要自己设置序列化方式为 JSON
代码语言:javascript复制
package com.litblc.fmock.moduleA.config;

import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.util.StringUtils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

/**
 * 使`@Cacheable`操作存储的数据自动格式化为json,而不是默认的二进制
 * 注意与RedisTemplateConfig不要混淆,都要进行处理
 *
 * @Author zhenhuaixiu
 * @Date 2023/11/6 14:34
 * @Version 1.0
 */
@Configuration
public class CacheConfig {

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();

        // 先载入配置文件中的配置信息
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();

        // 根据配置文件中的定义,初始化 Redis Cache 配
        if (redisProperties.getTimeToLive() != null) {
            redisCacheConfiguration = redisCacheConfiguration.entryTtl(redisProperties.getTimeToLive());
        }
        if (StringUtils.hasText(redisProperties.getKeyPrefix())) {
            redisCacheConfiguration = redisCacheConfiguration.prefixCacheNameWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            redisCacheConfiguration = redisCacheConfiguration.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            redisCacheConfiguration = redisCacheConfiguration.disableKeyPrefix();
        }

        // 缓存对象中可能会有 LocalTime/LocalDate/LocalDateTime 等 java.time 段,所以需要通过 JavaTimeModule 定义其序列化、反序列化格式
        JavaTimeModule javaTimeModule = new JavaTimeModule();

        javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss.SSSSSSSSS")));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSSSS")));

        javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss.SSSSSSSSS")));
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSSSS")));

        // 基于 Jackson 的 RedisSerializer 实现:GenericJackson2JsonRedisSerializer
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();

        // 把 javaTimeModule 配置到 Serializer 中
        serializer = serializer.configure(config -> {
            config.registerModules(javaTimeModule);
        });

        // 设置 Value 的序列化方式
        return redisCacheConfiguration
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));
    }
}
  • 使用 使用就比较简单了,声明@EnableCaching,@EnableCaching为开启缓存,可以放在任何一个能被自动加载的地方。

比如放在启动类上,或者放在你的redis配置上,比如我放在redis的序列化配置上

代码语言:javascript复制
package com.litblc.fmock.moduleA.config;

import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * 使redis保存的数据支持<string,object>,并保存的数据为json字符串
 *
 * @Author zhenhuaixiu
 * @Date 2023/11/6 10:51
 * @Version 1.0
 */
@Configuration
@EnableCaching  // @EnableCaching为开启缓存,可以放在任何一个能被自动加载的地方
public class RedisTemplateConfig extends RedisTemplate<String, Object> {

    public RedisTemplateConfig(RedisConnectionFactory redisConnectionFactory) {

        // 构造函数注入 RedisConnectionFactory,设置到父类
        super.setConnectionFactory(redisConnectionFactory);

        // 使用 Jackson 提供的通用 Serializer
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
        serializer.configure(mapper -> {
            // 如果涉及到对 java.time 类型的序列化,反序列化那么需要注册 JavaTimeModule
            mapper.registerModule(new JavaTimeModule());
        });

        // String 类型的 key/value 序列化
        super.setKeySerializer(StringRedisSerializer.UTF_8);
        super.setValueSerializer(serializer);

        // Hash 类型的 key/value 序列化
        super.setHashKeySerializer(StringRedisSerializer.UTF_8);
        super.setHashValueSerializer(serializer);
    }
}
  • 控制器只需要声明@Cacheable即可
代码语言:javascript复制
    @GetMapping(value = "listDesc")
    @Operation(summary = "获取文章列表")
    @Cacheable(value = "posts")
    public List<Posts> postsListDesc() {
        System.out.println("再次访问这个接口,这句话不会输出,证明走了缓存");

        List<Posts> res = this.postsService.getAllPosts();
        this.redisTemplate.opsForValue().set("posts1", res, 60L, TimeUnit.SECONDS);  // 手动存储的是字符串
        this.redisTemplate.opsForValue().set("posts2", res);  // 永久期限,正常json格式

        return res;
    }

定时任务

  • 需要依赖spring-boot-starter
代码语言:javascript复制
<dependency>
    <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
</dependency>
  • 在启动类、或者其他能扫描到的类添加注解@EnableScheduling
  • 方法中添加@Scheduled()即可,如下示例代码
代码语言:javascript复制
package com.litblc.fmock.moduleA.crontab;

import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @Author zhenhuaixiu
 * @Date 2023/11/10 17:03
 * @Version 1.0
 */

// @Scheduled 参数可以接受两种定时的设置,一种是我们常用的`cron="*/6 * * * * ?"`,一种是 fixedRate = 6000,两种都表示每隔六秒跑一次。
// @Scheduled(fixedRate = 6000) :上一次开始执行时间点之后6秒再执行
// @Scheduled(fixedDelay = 6000) :上一次执行完毕时间点之后6秒再执行
// @Scheduled(initialDelay=1000, fixedRate=6000) :第一次延迟1秒后执行,之后按 fixedRate 的规则每6秒执行一次
// cron參考 https://blog.csdn.net/Linweiqiang5/article/details/86741258

@EnableScheduling
@Component
public class SchedulerTask {

    private int count = 0;

    @Scheduled(cron = "*/6 * * * * ?")
    private void task1() {
        System.out.println("这样就执行定时任务,第几次:"   (  this.count));
    }

    @Scheduled(fixedRate = 6000)
    public void task2() {
        System.out.println("现在时间:"   (new Date()));
    }
}

在单应用中可以这么使用,比较简单,但是在微服务架构中就不行了,比如启用了多个实例,那么定时任务也会执行多次。 所以后面我们一点一点完善升级。

#QR{padding-top:20px;} #QR a{border:0} #QR img{width:180px;max-width:100%;display:inline-block;margin:.8em 2em 0 2em} #rewardButton { border: 1px solid #ccc; /*width: 20%;*/ line-height: 53px; text-align: center; height: 70px; display: block; border-radius: 10px; -webkit-transition-duration: .4s; transition-duration: .4s; background-color: #f77b83; color: #f7f7f7; margin: 20px auto; padding: 8px 25px; } #rewardButton:hover { color: #eb5055; border-color: #f77b83; outline-style: none; background-color: floralwhite; }

本文由 litblc 创作,采用 知识共享署名4.0 国际许可协议进行许可

本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名

最后编辑时间为: Nov 10, 2023 at 05:40 pm

0 人点赞