单体项目改造微服务

2024 年 10 月 18 日 星期五(已编辑)
/ , , , , , ,
19
摘要
单体项目改造微服务详细步骤
这篇文章上次修改于 2024 年 10 月 19 日 星期六,可能部分内容已经不适用,如有疑问可询问作者。

单体项目改造微服务

ps:将一个 springboot 项目改造为微服务项目所需要的详细步骤。

技术栈:

  • SpringBoot 2.6.13
  • Spring Cloud Alibaba 2021.0.5.0
  • Nacos
  • OpenFeign
  • Knife4j

注意!!!版本一定要对应

spring cloudspring boot
2023.0.22023.0.33.3.x3.2.x
2021.0.52.6.13

服务划分

  1. 依赖模块:
    1. 注册中心:Nacos
    2. 微服务网关 gateway:Gateway 聚合所有的接口,统一接受处理前端的请求
  2. 公共模块:
    1. common模块:全局异常处理器、请求响应封装类、公用的工具类等
    2. model 模型模块:实体类
    3. service-client 公共接口模块(OpenFeign):只存放接口,不存放实现(多个服务之间要共享)
  3. 业务模块:
    1. 用户服务(端口:8101)
    2. 题目服务(端口:8102)
    3. … …

路由划分

用 springboot 的 context-path 统一修改各项目的接口前缀,比如:

用户服务:

  • /api/user
  • /api/user/inner(内部调用,网关层面要做限制)

题目服务:

  • /api/question(也包括题目提交信息)
  • /api/question/inner(内部调用,网关层面要做限制)

Nacos 启动

安装后,进入bin目录执行命令启动:

linux:

sh startup.sh -m standalone

ubuntu:

bash startup.sh -m standalone

windows:

startup.cmd -m standalone

新建项目

创建微服务项目

推荐:

补充:(待定)

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>xxx</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
  • 依次使用 new modules 和 spring boot Initializr 创建各模块
  • 给各模块之间绑定子父依赖关系

同步代码和依赖

  • 公共服务:
    • 无脑搬运
  • 业务服务:

    • 无脑搬运(原 controller,service)
    • 引入公共模块(model 服务,commone 服务,service-client 服务)
  • service-client 公共接口服务:

    • 无脑搬运所有的 service 接口
    • 引入 OpenFeign 依赖
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
        <version>x.x.x</version>
    </dependency>

启动类的注解:(业务服务+网关,公共接口服务不需要启动类)

@SpringBootApplication
@MapperScan("com.yannqing.yanojbackendquestionservice.mapper")
@EnableScheduling
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
@ComponentScan("com.yannqing")

服务内部调用

Open Feign:Http 调用客户端,提供了更方便的方式来让你远程调用其他服务,不用关心服务的调用地址

  • 解决找不到 Bean 的问题
  1. 梳理调用关系:(什么服务依赖了什么服务,要体现到具体代码)
  2. 确认要提供哪些服务
  3. 实现 client 方法
    1. 实现较为简单直接使用默认实现,通过 default 关键字
    2. 其余的,在业务服务的 controller 中,新建 inner 包,表示内部调用服务。实现对应 feignClient 接口即可

示例代码:

/**
 * 用户服务
 *
 */
@FeignClient(name = "yuoj-backend-user-service", path = "/api/user/inner")
public interface UserFeignClient {

    /**
     * 根据 id 获取用户
     * @param userId
     * @return
     */
    @GetMapping("/get/id")
    User getById(@RequestParam("userId") long userId);

    /**
     * 根据 id 获取用户列表
     * @param idList
     * @return
     */
    @GetMapping("/get/ids")
    List<User> listByIds(@RequestParam("idList") Collection<Long> idList);

    /**
     * 获取当前登录用户
     *
     * @param request
     * @return
     */
    default User getLoginUser(HttpServletRequest request) {
        // 先判断是否已登录
        Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
        User currentUser = (User) userObj;
        if (currentUser == null || currentUser.getId() == null) {
            throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
        }
        return currentUser;
    }

    /**
     * 是否为管理员
     *
     * @param user
     * @return
     */
    default boolean isAdmin(User user) {
        return user != null && UserRoleEnum.ADMIN.getValue().equals(user.getUserRole());
    }

    /**
     * 获取脱敏的用户信息
     *
     * @param user
     * @return
     */
    default UserVO getUserVO(User user) {
        if (user == null) {
            return null;
        }
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(user, userVO);
        return userVO;
    }

}
  1. 修改各业务服务的调用代码为 feignClient,(例如:UserService -> UserFeignClient)
  2. 编写 feignClient 服务的实现类,注意要和之前定义的客户端保持一致
@RestController
@RequestMapping("/inner")
public class UserInnerController implements UserFeignClient {

    @Resource
    private UserService userService;

    /**
     * 根据 id 获取用户
     * @param userId
     * @return
     */
    @Override
    @GetMapping("/get/id")
    public User getById(@RequestParam("userId") long userId) {
        return userService.getById(userId);
    }

    /**
     * 根据 id 获取用户列表
     * @param idList
     * @return
     */
    @Override
    @GetMapping("/get/ids")
    public List<User> listByIds(@RequestParam("idList") Collection<Long> idList) {
        return userService.listByIds(idList);
    }

}
  1. 开启 Nacos 的配置,让服务之间能够互相发现
  • 所有模块引入 Nacos 依赖,然后给业务服务(包括网关)增加配置:
spring:
    cloud:
        nacos:
          discovery:
            server-addr: 127.0.0.1:8848
  1. 给业务服务项目启动类打上注解,开启服务发现、找到对应的客户端 Bean 的位置

ps:gateway 不需要添加第二个,添加第一个即可

@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.yupi.yuojbackendserviceclient.service"})
  1. 全局引入负载均衡器依赖:(待定)
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-loadbalancer</artifactId>
    <version>3.1.5</version>
</dependency>
  1. 启动项目,测试依赖能否注入,能否完成相互调用

gateway 网关

微服务网关(gateway):Gateway 聚合所有的接口,统一接受处理前端的请求,而不是多个端口分开处理。

gateway:想自定义一些功能,需要对这个技术有比较深的理解

网关:

  • Gateway 是应用层网关:会有一定的业务逻辑(比如根据用户信息判断权限)

  • Nginx 是接入层网关:比如每个请求的日志,通常没有业务逻辑

核心依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

接口路由

ps:统一地接受前端的请求,转发请求到对应的服务

通过配置:从请求路径/api//* ,根据id(spring.application.name) 来查找对应的服务

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      routes:
        - id: yuoj-backend-user-service
          uri: lb://yuoj-backend-user-service
          predicates:
            - Path=/api/user/**
        - id: yuoj-backend-question-service
          uri: lb://yuoj-backend-question-service
          predicates:
            - Path=/api/question/**
        - id: yuoj-backend-judge-service
          uri: lb://yuoj-backend-judge-service
          predicates:
            - Path=/api/judge/**
  application:
    name: yuoj-backend-gateway
  main:
    web-application-type: reactive
server:
  port: 8101

注意:spring.main.web-application-type = reactive 配置一定要加!!

聚合文档

knife4j:https://doc.xiaominfo.com/docs/quick-start

  1. 先给所有业务服务引入依赖,同时开启接口文档的配置
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
    <version>4.3.0</version>
</dependency>
knife4j:
  enable: true
  1. 给网关服务 gateway 集中管理接口文档
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-gateway-spring-boot-starter</artifactId>
    <version>4.3.0</version>
</dependency>
knife4j:
  gateway:
    # ① 第一个配置,开启gateway聚合组件
    enabled: true
    # ② 第二行配置,设置聚合模式采用discover服务发现的模式
    strategy: discover
    discover:
      # ③ 第三行配置,开启discover模式
      enabled: true
      # ④ 第四行配置,聚合子服务全部为Swagger2规范的文档
      version: swagger2
  1. 访问地址即可查看聚合接口文档:http://localhost:8101/doc.html#/home

跨域问题

新建config包,添加跨域配置

// 处理跨域
@Configuration
public class CorsConfig {

    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.setAllowCredentials(true);
        // todo 实际改为线上真实域名、本地域名
        config.setAllowedOriginPatterns(Arrays.asList("*"));
        config.addAllowedHeader("*");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);
        return new CorsWebFilter(source);
    }
}

权限校验

新建 fileter 包

@Component
public class GlobalAuthFilter implements GlobalFilter, Ordered {

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        String path = serverHttpRequest.getURI().getPath();
        // 判断路径中是否包含 inner,只允许内部调用
        if (antPathMatcher.match("/ **/inner/** ", path)) {
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.FORBIDDEN);
            DataBufferFactory dataBufferFactory = response.bufferFactory();
            DataBuffer dataBuffer = dataBufferFactory.wrap("无权限".getBytes(StandardCharsets.UTF_8));
            return response.writeWith(Mono.just(dataBuffer));
        }
        // todo 统一权限校验,通过 JWT 获取登录用户信息
        return chain.filter(exchange);
    }

    /**
     * 优先级提到最高
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}
  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...