往期开发(已经完成)

  • OJ 判题项目-介绍(一)
  • OJ 判题项目-初始化(二)
  • OJ 判题项目-前后端联调(三)
  • OJ 判题项目-简易判题机(四)
  • OJ 判题项目-原生代码沙箱(五)
  • OJ 判题项目-Docker 代码沙箱(六)
  • OJ 判题项目-微服务改造(七)

  • 今日后端开发

    • 微服务定义

      什么是微服务?服务是什么意思?

      微服务:专注于提供某类特定功能的代码,而不是把所有的功能代码都放在一个项目中,把大的项目按照一定的功能逻辑进行拆分,拆分为多个子模块,每个子模块可以独立运行,独立负责一类功能,子模块之间可以进行相互调用、互不影响
      服务:提供某类功能的代码

    • 微服务的技术选型

      Spring Cloud Alibaba
      文档:https://sca.aliyun.com/zh-cn/

    • 微服务介绍

      1. Spring Cloud GateWay:网关
      2. Nacos:注册中心
      3. Sentinel:熔断限流
      4. Seata:分布式事务
      5. RocketMQ:消息队列,削峰填谷
      6. Docker:使用 Docker 进行容器化部署
      7. Kubernetes:使用 K8s 进行容器化部署

      结构图

      微服务的执行流程
      流程

    • 微服务改造

      1. 先将项目登录改造为分布式登录使用 redisson 进行改造
        引入依赖

        1
        2
        3
        4
        5
        6
        7
        8
        9
        <!-- redis -->
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
        </dependency>

        修改配置文件:

        1
        2
        3
        4
        5
        6
        7
        8
        # session 配置
        session:
        store-type: redis
        redis:
        database: 2
        host: localhost
        port: 6379
        timeout: 5000
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        @Configuration
        @ConfigurationProperties(prefix = "spring.redis")
        @Data
        public class RedissonConfig {

        private Integer database;

        private String host;

        private Integer port;

        private String password;

        @Bean
        public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
        .setDatabase(database)
        .setAddress("redis://" + host + ":" + port);
        RedissonClient redisson = Redisson.create(config);
        return redisson;
        }
        }
      2. 项目结构划分
        先进行子工程划分结构,结构如下
        结构图
        然后给父级工程引入注册中心,微服务,网关等依赖

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        78
        79
        80
        81
        82
        83
        84
        85
        86
        87
        88
        89
         <dependencies>
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
        <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-collections4</artifactId>
        <version>4.4</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
        <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.2</version>
        </dependency>
        <!-- https://hutool.cn/docs/index.html#/-->
        <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.8</version>
        </dependency>
        <!-- redis -->
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>2021.0.5</version>
        <type>pom</type>
        <scope>import</scope>
        </dependency>
        <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-loadbalancer</artifactId>
        <version>3.1.5</version>
        </dependency>
        <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
        <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
        </dependency>

        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
        </dependency>
        <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
        </dependency>
        <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
        </dependency>
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        </dependency>
        </dependencies>

        抽离出各个服务之间需要调用的 service 使用 open feign 内部进行调用暴露 service 服务(名称和路径一定要写对)
        例如:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        /**
        * 用户服务
        */
        @FeignClient(name = "chenoj-backend-user-service", path = "/api/user/inner")
        public interface UserFeignClient {


        /**
        * 通过ids 来获取用户列表
        *
        * @param ids
        * @return
        */
        @GetMapping("/get/ids")
        List<User> listByIds(@RequestParam("ids") Collection<Long> ids);

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

        /**
        * 获取当前登录用户 写成默认方法 不再远程调用服务 提升性能
        *
        * @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;
        }

        }

        按照这样抽离出所有内部服务需要暴露的 service

        然后给需要暴露 service 服务的模块编写内部调用 controller,例如用户服务

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        /**
        * 该服务只提供服务内部之间进行调用
        */
        @RestController
        @RequestMapping("/inner")
        public class UserInnerController implements UserFeignClient {

        @Autowired
        private UserService userService;

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

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

        }

        依次给需要的服务编写 innerController 即可

        为什么需要这样去调用?
        由于每个模块都有一个独立的启动入口例如:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        @SpringBootApplication
        @MapperScan("com.chen.userservice.mapper")
        @EnableScheduling
        @EnableDiscoveryClient
        @EnableFeignClients(basePackages = {"com.chen.serviceclient.service"})
        @ComponentScan("com.chen")
        @EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
        public class ChenojBackendUserServiceApplication {

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

        }

        自己当前模块使用了其他模块的 service 服务,但是没有其实现类,就会导致 bean 注入失败,所以通过微服务提供的 open feign 来暴露各个服务需要给其他内部进行调用的 service,然后来实现跨模块的内部服务调用

        其他模块需要导入***FeignClient 即可调用需要的服务

        改造完调用方式后,这个时候就需要微服务的网关来进行服务路由,路由到调用的服务地址,达到服务聚合,统一分发,这样前端只需要访问网关地址即可,
        配置文件如下:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        spring:
        cloud:
        nacos:
        discovery:
        server-addr: 127.0.0.1:8848
        gateway:
        routes:
        - id: chenoj-backend-judge-service
        uri: lb://chenoj-backend-judge-service
        predicates:
        - Path=/api/judge/**
        - id: chenoj-backend-question-service
        uri: lb://chenoj-backend-question-service
        predicates:
        - Path=/api/question/**
        - id: chenoj-backend-user-service
        uri: lb://chenoj-backend-user-service
        predicates:
        - Path=/api/user/**
        application:
        name: chenoj-backend-gateway
        main:
        web-application-type: reactive

        注意网关启动时候,需要禁用掉应用的 web,网关只负责去转发一些服务的调用,不做 web 应用处理: web-application-type: reactive

      3. 统一的文档聚合
        给有启动类的模块全部引入文档依赖(knife4j)

        1
        2
        3
        4
        5
        <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>knife4j-gateway-spring-boot-starter</artifactId>
        <version>4.3.0</version>
        </dependency>

        然后进行配置开启文档
        网关的配置(特殊)

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        knife4j:
        gateway:
        # ① 第一个配置,开启gateway聚合组件
        enabled: true
        # ② 第二行配置,设置聚合模式采用discover服务发现的模式
        strategy: discover
        discover:
        # ③ 第三行配置,开启discover模式
        enabled: true
        # ④ 第四行配置,聚合子服务全部为Swagger2规范的文档
        version: swagger2

        服务的配置

        1
        2
        knife4j:
        enable: true
      4. 微服务改造后模块化导致分布式登录失效需要指定 cookie 的配置
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        server:
        address: 0.0.0.0
        port: 8093
        servlet:
        context-path: /api/judge
        # cookie 30 天过期
        session:
        cookie:
        max-age: 2592000
        path: /api
      5. 全局跨域

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        package com.chen.gateway.config;


        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;
        import org.springframework.web.cors.CorsConfiguration;
        import org.springframework.web.cors.reactive.CorsWebFilter;
        import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
        import org.springframework.web.util.pattern.PathPatternParser;

        import java.util.Arrays;

        /**
        * 全局跨域配置
        */
        @Configuration
        public class CorsConfig {

        @Bean
        public CorsWebFilter corsWebFilter() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setAllowCredentials(true);
        // todo 实际为上线真实域名
        corsConfiguration.setAllowedOriginPatterns(Arrays.asList("*"));
        corsConfiguration.addAllowedHeader("*");
        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsWebFilter(urlBasedCorsConfigurationSource);
        }

        }

      6. 权限校验

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        package com.chen.gateway.filter;

        import com.alibaba.nacos.common.packagescan.resource.AntPathMatcher;
        import org.springframework.cloud.gateway.filter.GatewayFilterChain;
        import org.springframework.cloud.gateway.filter.GlobalFilter;
        import org.springframework.core.Ordered;
        import org.springframework.core.io.buffer.DataBuffer;
        import org.springframework.core.io.buffer.DataBufferFactory;
        import org.springframework.http.HttpStatus;
        import org.springframework.http.server.reactive.ServerHttpRequest;
        import org.springframework.http.server.reactive.ServerHttpResponse;
        import org.springframework.stereotype.Component;
        import org.springframework.web.server.ServerWebExchange;
        import reactor.core.publisher.Mono;

        import java.nio.charset.StandardCharsets;

        @Component
        public class GlobalAuthFilter implements GlobalFilter, Ordered {

        private final AntPathMatcher antPathMatcher = new AntPathMatcher();

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        String path = serverHttpRequest.getURI().getPath();
        if (antPathMatcher.match("/**/inner/**", path)) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.FORBIDDEN);
        DataBufferFactory dataBufferFactory = response.bufferFactory();
        DataBuffer wrap = dataBufferFactory.wrap("无权限".getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Mono.just(wrap));
        }
        // todo 做统一校验 使用JWT Token做验证校验
        return chain.filter(exchange);
        }

        /**
        * 过滤优先级
        *
        * @return
        */
        @Override
        public int getOrder() {
        return 0;
        }
        }

    • 消息队列引入(上一个项目已经学习过)

      1. 引入依赖

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-amqp -->
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
        <version>2.7.2</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
        <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.17.0</version>
        </dependency>
      2. 引入配置

        1
        2
        3
        4
        5
        rabbitmq:
        host: localhost
        port: 5672
        password: guest
        username: guest
      3. 初始化 mq
        在启动项目时候调用

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        package com.chen.judgeservice.rabbitmq;

        import com.chen.common.constant.OJMqConstant;
        import com.rabbitmq.client.Channel;
        import com.rabbitmq.client.Connection;
        import com.rabbitmq.client.ConnectionFactory;
        import lombok.extern.slf4j.Slf4j;

        @Slf4j
        public class RabbitMQInit {

        /**
        * RabbitMQ初始化
        */
        public static void Init() {
        try {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        // 声明交换机 类型为直接交换机
        channel.exchangeDeclare(OJMqConstant.OJ_EXCHANGE_NAME, "direct");
        // 声明队列
        channel.queueDeclare(OJMqConstant.OJ_QUEUE_NAME, true, false, false, null);
        // 绑定队列到交换机
        channel.queueBind(OJMqConstant.OJ_QUEUE_NAME, OJMqConstant.OJ_EXCHANGE_NAME, OJMqConstant.OJ_ROUTING_KEY);
        // 限制任务数
        channel.basicQos(1);
        log.info("RabbitMQ初始化完成!");
        } catch (Exception e) {
        log.error("RabbitMQ初始化失败!错误信息为:{0}", e);
        }
        }
        }

      4. 生产者代码

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        package com.chen.questionservice.rabbitmq;

        import com.chen.common.constant.OJMqConstant;
        import org.springframework.amqp.rabbit.core.RabbitTemplate;
        import org.springframework.stereotype.Component;

        import javax.annotation.Resource;

        @Component
        public class OJMessageProducer {

        @Resource
        private RabbitTemplate rabbitTemplate;

        /**
        * 发送消息
        *
        * @param message
        */
        public void sendMessage(String message) {
        rabbitTemplate.convertAndSend(OJMqConstant.OJ_EXCHANGE_NAME, OJMqConstant.OJ_ROUTING_KEY, message);
        }
        }

      5. 消费者代码

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        package com.chen.judgeservice.rabbitmq;

        import com.chen.model.entity.Question;
        import com.chen.model.entity.QuestionSubmit;

        import cn.hutool.json.JSONUtil;
        import com.chen.common.common.ErrorCode;
        import com.chen.common.constant.OJMqConstant;
        import com.chen.common.exception.BusinessException;
        import com.chen.judgeservice.codesandbox.service.CodeSandBoxService;
        import com.chen.model.dto.codesandbox.CodesandBoxRequest;
        import com.rabbitmq.client.Channel;
        import lombok.extern.slf4j.Slf4j;
        import org.apache.commons.lang3.StringUtils;
        import org.springframework.amqp.rabbit.annotation.RabbitListener;
        import org.springframework.amqp.support.AmqpHeaders;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.messaging.handler.annotation.Header;
        import org.springframework.stereotype.Component;

        @Component
        @Slf4j
        public class OJMessageConsumer {

        @Autowired
        private CodeSandBoxService codeSandBoxService;

        /**
        * 监听消息消费
        *
        * @param message
        * @param channel
        * @param deliverTag
        */
        @RabbitListener(queues = {OJMqConstant.OJ_QUEUE_NAME}, ackMode = "MANUAL")
        public void receiveMessage(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliverTag) {
        try {
        log.info("receiveMessage: {}", message);
        if (StringUtils.isBlank(message)) {
        // 参数 消息的tag 是否要全部拒绝 false为只拒绝当前 是否要重入队列 false为不入
        channel.basicNack(deliverTag, false, false);
        throw new BusinessException(ErrorCode.SYSTEM_ERROR);
        }
        CodesandBoxRequest codesandBoxRequest = JSONUtil.toBean(message, CodesandBoxRequest.class);
        Question question = codesandBoxRequest.getQuestion();
        QuestionSubmit questionSubmit = codesandBoxRequest.getQuestionSubmit();
        try {
        codeSandBoxService.doCodeSandBoxJudge(questionSubmit, question);
        } catch (Exception e) {
        channel.basicNack(deliverTag, false, false);
        }
        channel.basicAck(deliverTag, false);
        } catch (Exception e) {
        log.error("消息消费失败!");
        throw new BusinessException(ErrorCode.SYSTEM_ERROR);
        }
        }

        }

      6. 项目改造

        1
        2
        3
        4
        5
        // todo 执行判题 异步执行 改造为RabbitMQ消息队列
        CompletableFuture.runAsync(() -> {
        // 向消息队列发送消息
        ojMessageProducer.sendMessage(JSONUtil.toJsonStr(codesandBoxRequest));
        });