第五课 Spring Cloud分布式微服务实战-文章服务开发RabbitMQ和静态化

第五课 Spring Cloud分布式微服务实战-文章服务开发RabbitMQ和静态化

tags:

  • Java
  • 慕课网

第一节 文章服务的开发

1.1 文章服务的构建

  1. 创建新moduleimooc-news-dev-service-article
  2. pom文件加上api的依赖。
  3. resource文件夹下复制一份配置文件并修改。端口8001
  4. 创建启动类com.imooc.article.Application
  5. 创建mapper文件夹和controller文件夹,并创建HelloController。
  6. 启动访问测试。http://127.0.0.1:8001/hello
  7. 看下aritcle的数据库表
    在这里插入图片描述

1.2 minIO多图片上传

  1. minIO http://192.168.44.128:6090/login 账号: admin 密码:123456789
  2. 测试地址:http://admin.imoocnews.com:9090/imooc-news/writer/createArticle.html
  3. api添加在controller的files中
    @ApiOperation(value = "上传多个文件", notes = "上传多个文件", httpMethod = "POST")
    @PostMapping("/uploadSomeFiles")
    public GraceJSONResult uploadSomeFiles(@RequestParam String userId,
                                      MultipartFile[] files) throws Exception;
  1. 实现这个接口。FileUploaderController#uploadSomeFiles
   @Override
    public GraceJSONResult uploadSomeFiles(String userId, MultipartFile[] files) throws Exception {
        // 声明list,用于存放多个图片的地址路径,返回到前端
        List<String> imageUrlList = new ArrayList<>();
        if (files != null && files.length > 0){
            for (MultipartFile file : files){
                String finalpath = "";
                if (file != null) {
                    // 获取文件上传的名称
                    String filename = file.getOriginalFilename();
                    // 判断文件名不能为空
                    if (StringUtils.isNotBlank(filename)){
                        String fileNameArr[] = filename.split("\\.");
                        // 获得后缀
                        String suffix = fileNameArr[fileNameArr.length - 1];
                        // 判断后缀是否符合预定义规范 防止黑客乱传脚本攻击
                        if (!suffix.equalsIgnoreCase("jpg") &&
                                !suffix.equalsIgnoreCase("png") &&
                                !suffix.equalsIgnoreCase("jpeg")){
                            continue;
                        }
                        // 执行上传
                        finalpath = uploaderService.uploadMinIO(file);
                        // FIXME: 添加之前可以做一个图片审核
                        imageUrlList.add(finalpath);
                    }
                } else {
                    continue;
                }
            }
        }
        return GraceJSONResult.ok(imageUrlList);
    }
  1. api拦截器配置。api.config.InterceptorConfig#addInterceptors
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserInfo")
                .addPathPatterns("/user/uploadFace")
                .addPathPatterns("/user/uploadSomeFiles");

        registry.addInterceptor(userActiveInterceptor())
                .addPathPatterns("/fs/uploadSomeFiles")
  1. 测试图片上传功能。

1.3 用户端查询分类列表

  1. Api的controller.admin.CategoryMngControllerApi添加
    @GetMapping("getCats")
    @ApiOperation(value = "用户端查询分类列表", notes = "用户端查询分类列表", httpMethod = "GET")
    public GraceJSONResult getCats();
  1. admin模块实现接口。admin.controller.CategoryMngController#getCats
    @Override
    public GraceJSONResult getCats() {
        // 先从redis中查询,如果有,则返回,如果没有,则查询数据库库后先放缓存,放返回
        String allCatJson = redis.get(REDIS_ALL_CATEGORY);

        List<Category> categoryList = null;
        if (StringUtils.isBlank(allCatJson)) {
            categoryList = categoryService.queryCategoryList();
            redis.set(REDIS_ALL_CATEGORY, JsonUtils.objectToJson(categoryList));
        } else {
            categoryList = JsonUtils.jsonToList(allCatJson, Category.class);
        }

        return GraceJSONResult.ok(categoryList);
    }
  1. com.imooc.api.service.BaseService添加常量。
   public static final String REDIS_ALL_CATEGORY = "redis_all_category";
  1. 先登录首页进行测试:http://admin.imoocnews.com:9090/imooc-news/portal/index.html
  2. 安装测试文章分类。http://admin.imoocnews.com:9090/imooc-news/writer/createArticle.html
  3. 到redis中查看缓存。redis_all_category

1.4 构建定时任务

  1. 定时表达式生成网站:https://cron.qqe2.com/
  2. 创建com.imooc.article.task.TaskPublishArticles
package com.imooc.article.task;

import com.imooc.article.service.ArticleService;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

import java.time.LocalDateTime;

@Configuration      // 1. 标记配置类,使得springboot容器扫描到
@EnableScheduling   // 2. 开启定时任务
public class TaskPublishArticles {

    @Autowired
    private ArticleService articleService;

    // 添加定时任务,注明定时任务的表达式
    @Scheduled(cron = "0/3 * * * * ?")
    private void publishArticles() {
        System.out.println("执行定时任务:" + LocalDateTime.now());

        // 4. 调用文章service,把当前时间应该发布的定时文章,状态改为即时
        articleService.updateAppointToPublish();
    }
}

1.5 阿里AI智能审核

  1. 阿里云首页:->内容安全->内容检测api。这里根据阿里官方的api对接既可。
  2. 剩下看下源码即可。

第二节 文章评论业务的开发

2.1 自定义Mapper完成评论关联查询

  1. 数据库关联查询sql语句编写。自己关联自己。
SELECT
	c.id as commentId,
	c.father_id as fatherId,
	c.article_id as articleId,
	c.comment_user_id as commentUserId,
	c.comment_user_nickname as commentUserNickname,
	c.content as content,
	c.create_time as createTime,
	f.comment_user_nickname as quoteUserNickname,
	f.content as quoteContent
FROM
	comments c
LEFT JOIN
	comments f
on
	c.father_id = f.id
WHERE
	c.article_id = '2006117B57WRZGHH'
order by
	c.create_time
desc
  1. 自定义Mapper编写com.imooc.article.mapper.CommentsMapperCustom自定义映射。
package com.imooc.article.mapper;

import com.imooc.pojo.vo.CommentsVO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Map;

@Repository
public interface CommentsMapperCustom {

    /**
     * 查询文章评论
     */
    public List<CommentsVO> queryArticleCommentList(@Param("paramMap") Map<String, Object> map);

}
  1. 上面第一步查询出来的对象作为CommentsVO写到model中。
  2. resources/mapper/CommentsMapperCustom.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.article.mapper.CommentsMapperCustom" >

  <select id="queryArticleCommentList"
          resultType="com.imooc.pojo.vo.CommentsVO"
          parameterType="Map">

    SELECT
        c.id as commentId,
        c.father_id as fatherId,
        c.comment_user_id as commentUserId,
        c.comment_user_nickname as commentUserNickname,
        c.article_id as articleId,
        c.content as content,
        c.create_time as createTime,
        f.comment_user_nickname as quoteUserNickname,
        f.content as quoteContent
    FROM
        comments c
    LEFT JOIN
        comments f
    ON
        c.father_id = f.id
    WHERE
        c.article_id = #{paramMap.articleId}
    ORDER BY
        c.create_time
    DESC

  </select>

</mapper>

2.2 Freemarker基本配置

  1. 静态化技术:JSP、Freemarker和Thymeleaf(官方推荐的)、Velocity
  2. 官方文档: http://freemarker.foofun.cn/index.html
  3. 引入依赖:https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-freemarker
  4. 找到对应的版本号。复制到顶级pom的依赖中。
        <springboot-freemarker.version>2.2.5.RELEASE</springboot-freemarker.version>

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-freemarker</artifactId>
                <version>${springboot-freemarker.version}</version>
            </dependency>

  1. 在使用的子项目article中使用依赖,引入freemarker。
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
  1. application.yml配置。
  freemarker:
    charset: UTF-8
    content-type: text/html
    suffix: .ftl
    template-loader-path: classpath:/templates/
  1. 编写控制器。这里使用@Controller而不是@RestController。因为@RestController会把返回值变成字符串不是把对应的stu.ftl模板展示出来。
@Controller
@RequestMapping("free")
public class FreemarkerController {

    @GetMapping("/hello")
    public String hello(Model model) {

        // 定义输出到模板的内容
        // 输入字符串
        String stranger = "慕课网 imooc.com";
        model.addAttribute("there", stranger);

        makeModel(model);

        // 返回的stu是freemarker模板坐在的目录位置 classpath:/templates/
        // 匹配 *.ftl
        return "stu";
    }
}
  1. 访问: localhost:8001/free/hello

2.3 Freemarker生成静态html

  1. 配置生成路径。application.yml
# 定义freemarker生成的html位置
freemarker:
  html:
    target: /workspace/freemarker_html
    article: /Users/leechenxiang/Desktop/apache-tomcat-9.0.22/webapps/imooc-news/portal/a
  1. 定义静态生产html的函数。
@Controller
@RequestMapping("free")
public class FreemarkerController {

    @Value("${freemarker.html.target}")
    private String htmlTarget;

    @GetMapping("/createHTML")
    @ResponseBody
    public String createHTML(Model model) throws Exception {

        // 0. 配置freemarker基本环境
        Configuration cfg = new Configuration(Configuration.getVersion());
        // 声明freemarker模板所需要加载的目录的位置
        String classpath = this.getClass().getResource("/").getPath();
        cfg.setDirectoryForTemplateLoading(new File(classpath + "templates"));

//        System.out.println(htmlTarget);
//        System.out.println(classpath + "templates");

        // 1. 获得现有的模板ftl文件
        Template template = cfg.getTemplate("stu.ftl", "utf-8");

        // 2. 获得动态数据
        String stranger = "慕课网 imooc.com";
        model.addAttribute("there", stranger);
        model = makeModel(model);

        // 3. 融合动态数据和ftl,生成html
        File tempDic = new File(htmlTarget);
        if (!tempDic.exists()) {
            tempDic.mkdirs();
        }

        Writer out = new FileWriter(htmlTarget + File.separator + "10010" + ".html");
        template.process(model, out);
        out.close();

        return "ok";
    }
}
  1. 把文章详情页的内容抽象出模板,生成静态文件html上传到gridfs中,等待通知前端下载到指定目录。具体实现看下源码。

2.3 RabbitMQ环境搭建

  1. 安装RabbitMQ
docker run -d --hostname my-rabbit --name rabbit -p 15672:15672 -p 5672:5672 rabbitmq
docker ps 
# http://192.168.44.128:15672 ,这里的用户名和密码默认都是guest
docker exec -it 镜像ID /bin/bash
rabbitmq-plugins enable rabbitmq_management
# 创建admin用户分配权限
  1. 引入rabbitMQ。api中pom引入。
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
  rabbitmq:
    host: 192.168.44.128
    port: 5672
    username: admin
    password: 123456
    virtual-host: imooc-news-dev
  1. 在http://192.168.44.128:15672 的admin中创建virtual-host为imooc-news-dev

2.4 RabbitMQ基本使用

  1. api的config中com.imooc.api.config.RabbitMQDelayConfig编写Rabbit的配置类。
package com.imooc.api.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RabbitMQ 的配置类
 */
@Configuration
public class RabbitMQConfig {

    // 定义交换机的名字
    public static final String EXCHANGE_ARTICLE = "exchange_article";

    // 定义队列的名字
    public static final String QUEUE_DOWNLOAD_HTML = "queue_download_html";

    // 创建交换机
    @Bean(EXCHANGE_ARTICLE)
    public Exchange exchange(){
        return ExchangeBuilder
                .topicExchange(EXCHANGE_ARTICLE)
                .durable(true)
                .build();
    }

    // 创建队列
    @Bean(QUEUE_DOWNLOAD_HTML)
    public Queue queue(){
        return new Queue(QUEUE_DOWNLOAD_HTML);
    }

    // 队列绑定交换机
    @Bean
    public Binding binding(
            @Qualifier(QUEUE_DOWNLOAD_HTML) Queue queue,
            @Qualifier(EXCHANGE_ARTICLE) Exchange exchange){
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("article.#.do")
                .noargs();      // 执行绑定
    }

}
  1. rabbitMq官网各种模式:https://www.rabbitmq.com/getstarted.html
  2. 服务端发送消息到rabbitmq测试。
package com.imooc.article.controller;

import com.imooc.api.config.RabbitMQConfig;
import com.imooc.api.config.RabbitMQDelayConfig;
import com.imooc.grace.result.GraceJSONResult;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

@RestController
@RequestMapping("producer")
public class HelloController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/hello")
    public Object hello() {

        /**
         * RabbitMQ 的路由规则 routing key
         *  display.*.* -> * 代表一个占位符
         *      例:
         *          display.do.download  匹配
         *          display.do.upload.done   不匹配
         *
         * display.# -> # 代表任意多个占位符
         *      例:
         *          display.do.download  匹配
         *          display.do.upload.done.over   匹配
         *
         *
         */

//        rabbitTemplate.convertAndSend(
//                RabbitMQConfig.EXCHANGE_ARTICLE,
//                "article.hello",
//                "这是从生产者发送的消息~");

        rabbitTemplate.convertAndSend(
                RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.publish.download.do",
                "1001");



        rabbitTemplate.convertAndSend(
                RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.success.do",
                "1002");

        // 如果先绑定过display.* 要到rabbitmq中解除绑定 否则还是会传到之前的routing key中
        rabbitTemplate.convertAndSend(
                RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.play",
                "1003");

        return GraceJSONResult.ok();
    }

}
  1. 客户端消费队列中的消息。article-html中。根据routing key的不同值处理不同的业务逻辑
package com.imooc.article.html;

import com.imooc.api.config.RabbitMQConfig;
import com.imooc.article.html.controller.ArticleHTMLComponent;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class RabbitMQConsumer {

    @Autowired
    private ArticleHTMLComponent articleHTMLComponent;

    @RabbitListener(queues = {RabbitMQConfig.QUEUE_DOWNLOAD_HTML})
    public void watchQueue(String payload, Message message) {
        System.out.println(payload);

        String routingKey = message.getMessageProperties().getReceivedRoutingKey();
        if (routingKey.equalsIgnoreCase("article.publish.download.do")) {
            System.out.println("article.publish.download.do");
        } else if (routingKey.equalsIgnoreCase("article.success.do")) {
            System.out.println("article.success.do");
        } else if (routingKey.equalsIgnoreCase("article.download.do")) {
//            articleId + "," + articleMongoId
            String articleId = payload.split(",")[0];
            String articleMongoId = payload.split(",")[1];

            try {
                articleHTMLComponent.download(articleId, articleMongoId);
            } catch (Exception e) {
                e.printStackTrace();
            }

        } else if (routingKey.equalsIgnoreCase("article.html.download.do")) {

            String articleId = payload;
            try {
                articleHTMLComponent.delete(articleId);
            } catch (Exception e) {
                e.printStackTrace();
            }

        } else {
            System.out.println("不符合的规则:" + routingKey);
        }
    }
}

2.5 RabbitMQ延时队列插件安装使用

  1. 安装延时队列插件:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/tag/v3.8.0
  2. 上传到服务器,复制到容器中。
docker cp /home/rabbitmq_delayed_message_exchange-3.8.0.ez 773067241f96:/plugins

#启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

#查看
rabbitmq-plugins list

#重新启动容器
docker restart 773067241f96
  1. 构建延时队列rabbitmq的配置类。
package com.imooc.api.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RabbitMQ 的配置类
 */
@Configuration
public class RabbitMQDelayConfig {

    // 定义交换机的名字
    public static final String EXCHANGE_DELAY = "exchange_delay";

    // 定义队列的名字
    public static final String QUEUE_DELAY = "queue_delay";

    // 创建交换机
    @Bean(EXCHANGE_DELAY)
    public Exchange exchange(){
        return ExchangeBuilder
                .topicExchange(EXCHANGE_DELAY)
                .delayed()          // 开启支持延迟消息
                .durable(true)
                .build();
    }

    // 创建队列
    @Bean(QUEUE_DELAY)
    public Queue queue(){
        return new Queue(QUEUE_DELAY);
    }

    // 队列绑定交换机
    @Bean
    public Binding delayBinding(
            @Qualifier(QUEUE_DELAY) Queue queue,
            @Qualifier(EXCHANGE_DELAY) Exchange exchange){
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("publish.delay.#")
                .noargs();      // 执行绑定
    }
}
  1. 延时队列生产者示例。
    @GetMapping("/delay")
    public Object delay() {

        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                // 设置消息的持久
                message.getMessageProperties()
                        .setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                // 设置消息延迟的时间,单位ms毫秒
                message.getMessageProperties()
                        .setDelay(5000);
                return message;
            }
        };

        rabbitTemplate.convertAndSend(
                RabbitMQDelayConfig.EXCHANGE_DELAY,
                "delay.demo",
                "这是一条延迟消息~~",
                messagePostProcessor);

        System.out.println("生产者发送的延迟消息:" + new Date());

        return "OK";
    }
  1. 延迟队列消费者示例
package com.imooc.article;

import com.imooc.api.config.RabbitMQDelayConfig;
import com.imooc.article.service.ArticleService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class RabbitMQDelayConsumer {

    @Autowired
    private ArticleService articleService;

    @RabbitListener(queues = {RabbitMQDelayConfig.QUEUE_DELAY})
    public void watchQueue(String payload, Message message) {
        System.out.println(payload);

        String routingKey = message.getMessageProperties().getReceivedRoutingKey();
        System.out.println(routingKey);

        System.out.println("消费者接受的延迟消息:" + new Date());

        // 消费者接收到定时发布的延迟消息,修改当前的文章状态为`即时发布`
        String articleId = payload;
        articleService.updateArticleToPublish(articleId);
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值