5.文章详情、使用线程池,更新阅读次数

文章详情:

接口url:/articles/view/{id}

请求方式:POST

请求参数:

参数名称参数类型说明
idlong文章id(路径参数)

返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": "token"
}
ArticleController:
package com.example.blog.controller;

import com.example.blog.service.ArticleService;
import com.example.blog.vo.Result;
import com.example.blog.vo.params.PageParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RequestMapping("/articles")
@RestController
public class ArticleController
{

    @Autowired
    private ArticleService articleService;
    /*如果参数时放在请求体中,application/json传入后台的话,那么后台要用@RequestBody才能接收到;
     如果不是放在请求体中的话,那么后台接收前台传过来的参数时,要用@RequestParam来接收。
     或者形参前 什么也不写也能接收。*/

    /**
     * 文章详情
     */
    @PostMapping("/view/{id}")
    public Result findArticleById(@PathVariable("id") Long articleId)
    {
        return articleService.findArticleById(articleId);
    }
}
ArticleServiceImpl:
package com.example.blog.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.blog.dao.mapper.ArticleBodyMapper;
import com.example.blog.dao.mapper.ArticleMapper;
import com.example.blog.dao.mapper.CategoryMapper;
import com.example.blog.dos.Archives;
import com.example.blog.entity.Article;
import com.example.blog.entity.ArticleBody;
import com.example.blog.entity.Category;
import com.example.blog.service.ArticleService;
import com.example.blog.service.CategoryService;
import com.example.blog.service.SysUserService;
import com.example.blog.service.TagService;
import com.example.blog.vo.ArticleBodyVo;
import com.example.blog.vo.ArticleVo;
import com.example.blog.vo.CategoryVo;
import com.example.blog.vo.Result;
import com.example.blog.vo.params.PageParams;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.sql.Date;
import java.util.ArrayList;
import java.util.List;

@Service
public class ArticleServiceImpl implements ArticleService
{

    @Autowired
    private ArticleMapper articleMapper;

    @Autowired
    private TagService tagService;

    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private ArticleBodyMapper articleBodyMapper;

    @Autowired
    private CategoryService categoryService;
    /**
     * 分页查询 article数据库表
     * @param pageParams
     * @return
     */
    @Override
    public Result listArticle(PageParams pageParams)
    {
        Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
        LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();/*查询器*/
        queryWrapper.orderByDesc(Article::getWeight);/*是否置顶进行排序*/
        queryWrapper.orderByDesc(Article::getCreateDate);/*根据创建时间进行降序排序 order by create_date desc*/
        Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);/*等同于编写一个普通list查询,mybatis-plus自动替你分页*/
        List<Article> records = articlePage.getRecords();
        List<ArticleVo> articleVoList = copyList(records,true,true);
        return Result.success(articleVoList);
    }

    /**
     * 将Article列表封装为ArticleVo列表,以便把数据传给前端
     * @param records
     * @param isTag
     * @param isAuthor
     * @return
     */
    private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor)
    {
        ArrayList<ArticleVo> articleVoList = new ArrayList<>();
        for(Article record:records)
        {
            articleVoList.add(copy(record,isTag,isAuthor,false,false));
        }
        return articleVoList;
    }


    /*并不是所有接口都需要标签和作者信息*/
    private ArticleVo copy(Article article,boolean isTag,boolean isAuthor,boolean isBody,boolean isCategory)
    {
        ArticleVo articleVo = new ArticleVo();
        BeanUtils.copyProperties(article,articleVo);
        articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
        /*因为Article中的createDate(Long)与ArticleVo(String)中的createDate类型不同,所以需要进行手动赋值*/
        if(isTag)
        {
            Long articleId = article.getId();
            articleVo.setTags(tagService.findTagsById(articleId));
        }
        if(isAuthor)
        {
            Long authorId = article.getAuthorId();
            articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());
        }
        if(isBody)
        {
            Long bodyId = article.getBodyId();
            articleVo.setBody(findArticleBodyById(bodyId));
        }
        if(isCategory)
        {
            Long categoryId = article.getCategoryId();
            articleVo.setCategory(categoryService.findCategoryById(categoryId));
        }
        return articleVo;
    }

    /**
     * 通过文章id 查询文章详情表中的文章详情
     * @param bodyId
     * @return
     */
    private ArticleBodyVo findArticleBodyById(Long bodyId)
    {
        ArticleBody articleBody = articleBodyMapper.selectById(bodyId);
        ArticleBodyVo articleBodyVo = new ArticleBodyVo();
        articleBodyVo.setContent(articleBody.getContent());
        return articleBodyVo;
    }


    /**
     * 文章详情
     * @param articleId
     * @return
     */
    @Override
    public Result findArticleById(Long articleId)
    {
        /**
         * 1.把ArticleVo对应的ArticleBodyVo 和categories注释去掉,也就是现在需要使用这两个成员,并添加对应的Vo类
         * 2.根据articleId查询文章article
         * 3.根据bodyId 和categoryId 去关联查询对应的文章详情和文章标签
         * 4.将查到的文章详情和文章标签注入到articleVo中
         */
        Article article = articleMapper.selectById(articleId);

        ArticleVo articleVo = copy(article, true, true,true,true);
        return Result.success(articleVo);
    }
}

这里有一点复杂,理一下逻辑

1.把ArticleVo对应的ArticleBodyVo 和category注释去掉,也就是现在需要使用这两个成员(文章详情和文章分类),并添加对应的Vo类

1.1ArticleVo:

package com.example.blog.vo;


import lombok.Data;

import java.util.List;

@Data
public class ArticleVo
{

//    @JsonSerialize(using = ToStringSerializer.class)
    private String id;

    private String title;

    private String summary;

    private Integer commentCounts;

    private Integer viewCounts;

    private Integer weight;
    /**
     * 创建时间
     */
    private String createDate;

    private String author;
    /*创建该文章的作者头像*/
    private String avatar;

    private String authorId;

    private ArticleBodyVo body;

    private List<TagVo> tags;/*文章标签*/

    private CategoryVo categoryVo;
}

 1.2 ArticleBodyVo

package com.example.blog.vo;

import lombok.Data;

@Data
public class ArticleBodyVo
{
    private String content;
}

1.3  CategoryVo

package com.example.blog.vo;

import lombok.Data;

@Data
public class CategoryVo
{
    private String id;

    private String avatar;

    private String categoryName;

}

2.根据articleId查询文章article,获取对应的bodyId和categoryId

3.根据bodyId 和categoryId 去关联查询对应的文章详情和文章标签 

ArticleServiceImpl:

    /**
     * 通过文章id 查询文章详情表中的文章详情
     * @param bodyId
     * @return
     */
    private ArticleBodyVo findArticleBodyById(Long bodyId)
    {
        ArticleBody articleBody = articleBodyMapper.selectById(bodyId);
        ArticleBodyVo articleBodyVo = new ArticleBodyVo();
        articleBodyVo.setContent(articleBody.getContent());
        return articleBodyVo;
    }
CategoryServiceImpl:
package com.example.blog.service.impl;

import com.example.blog.dao.mapper.CategoryMapper;
import com.example.blog.entity.Category;
import com.example.blog.service.CategoryService;
import com.example.blog.vo.CategoryVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CategoryServiceImpl implements CategoryService
{

    @Autowired
    private CategoryMapper categoryMapper;

    @Override
    public CategoryVo findCategoryById(Long categoryId)
    {
        Category category = categoryMapper.selectById(categoryId);
        return copy(category);
    }

    private CategoryVo copy(Category category)
    {
        CategoryVo categoryVo = new CategoryVo();
        BeanUtils.copyProperties(category,categoryVo);
        return categoryVo;
    }
}

4.将查到的文章详情和文章标签注入到articleVo中

ArticleVo articleVo = copy(article, true, true,true,true);
    /*并不是所有接口都需要标签和作者信息*/
    private ArticleVo copy(Article article,boolean isTag,boolean isAuthor,boolean isBody,boolean isCategory)
    {
        ArticleVo articleVo = new ArticleVo();
        BeanUtils.copyProperties(article,articleVo);/*可以理解为article相同成员赋值给articleVo*/
        /*BeanUtils.copyProperties(article, articleVo);
        articleVo中的存在的属性,article中一定要有,但是article中可以有多余的属性;
        article中与articleVo中相同的属性都会被替换,不管是否有值;
        article、articleVo中的属性要名字相同,才能被赋值,不然的话需要手动赋值;
        Spring的BeanUtils的CopyProperties方法需要对应的属性有getter和setter方法;
        如果存在属性完全相同的内部类,但是不是同一个内部类,即分别属于各自的内部类,则spring会认为属性不同,不会copy*/
        articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
        /*因为Article中的createDate(Long)与ArticleVo(String)中的createDate类型不同,所以需要进行手动赋值*/
        if(isTag)
        {
            Long articleId = article.getId();
            articleVo.setTags(tagService.findTagsById(articleId));
        }
        if(isAuthor)
        {
            Long authorId = article.getAuthorId();
            articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());
        }
        if(isBody)
        {
            Long bodyId = article.getBodyId();
            articleVo.setBody(findArticleBodyById(bodyId));
        }
        if(isCategory)
        {
            Long categoryId = article.getCategoryId();
            articleVo.setCategory(categoryService.findCategoryById(categoryId));
        }
        return articleVo;
    }

5.重点!!

==>  Preparing:

SELECT id,title,summary,comment_counts,view_counts,author_id,body_id,category_id,weight,create_date FROM ms_article WHERE id=?
==> Parameters: 1405564731300831200(Long)
<==      Total: 0

发现:前端传入的值1405564731300831200丢失了两位精度,所以前端传入的articleId,后端通过该id找不到对应的article!!!

原因:由于前端传入的articleId丢失了精度,java中long数据能表示的范围比js中number大,在跟前端交互时,这样也就意味着部分数值在js中存不下(变成不准确的值)。

解决方案:ArticleVo 中的id字段使用fastjson的ToStringSerializer注解,让系统序列化时,保留相关精度。

package com.example.blog.vo;


import lombok.Data;

import java.util.List;

@Data
public class ArticleVo
{

//    @JsonSerialize(using = ToStringSerializer.class)
    private String id;

    private String title;

    private String summary;

    private Integer commentCounts;

    private Integer viewCounts;

    private Integer weight;
    /**
     * 创建时间
     */
    private String createDate;

    private String author;
    /*创建该文章的作者头像*/
    private String avatar;

    private String authorId;

    private ArticleBodyVo body;

    private List<TagVo> tags;/*文章标签*/

    private CategoryVo categoryVo;
}

 这里阅读数应该随着点击增加,所以需要使用线程池,更新阅读次数

查看完该文章之后,本应该直接返回数据,这个时候做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低。

更新肯定会增加此次接口的耗时,如果更新一旦除了问题,不能影响查看文章的操作。

这时可以增加线程池,把更新操作扔到线程池中去执行,这样就和主线程不相关了。

创建一个线程池:

package com.example.blog.cofig;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync //开启多线程
public class ThreadPoolConfig
{
    @Bean("taskExcutor")
    public Executor asyncServiceExecutor()
    {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(5);
        // 设置最大线程数
        executor.setMaxPoolSize(20);
        //配置队列大小
        executor.setQueueCapacity(Integer.MAX_VALUE);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(60);
        // 设置默认线程名称
        executor.setThreadNamePrefix("博客");
        // 等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //执行初始化
        executor.initialize();
        return executor;
    }
}

ArticleServiceImpl中的显示文章详情的方法findArticleById中,添加一个线程池,用于增加阅读量

    @Autowired
    private ThreadService threadService;
    @Override
    public Result findArticleById(Long articleId)
    {
        /**
         * 1.把ArticleVo对应的ArticleBodyVo 和categories注释去掉,也就是现在需要使用这两个成员,并添加对应的Vo类
         * 2.根据articleId查询文章article
         * 3.根据bodyId 和categoryId 去关联查询对应的文章详情和文章标签
         * 4.将查到的文章详情和文章标签注入到articleVo中
         */
        Article article = articleMapper.selectById(articleId);
        ArticleVo articleVo = copy(article, true, true,true,true);
        //查看完该文章之后,本应该直接返回数据,这个时候做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低
        //更新肯定会增加此次接口的耗时,如果更新一旦除了问题,不能影响 查看文章的操作
        //这时可以增加线程池,把更新操作扔到线程池中去执行,这样就和主线程不相关了
        threadService.updateArticleViewCount(articleMapper,article);
        return Result.success(articleVo);
    }
ThreadService:
package com.example.blog.service;

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.example.blog.dao.mapper.ArticleMapper;
import com.example.blog.entity.Article;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class ThreadService
{
    //期望此次操作在线程池执行 不会影响原有的主线程
    @Async("taskExcutor")//将该任务丢到线程池中
    public void updateArticleViewCount(ArticleMapper articleMapper, Article article)
    {
        int viewCounts = article.getViewCounts();

        Article articleUpdate = new Article();
        articleUpdate.setViewCounts(viewCounts+1);
        LambdaUpdateWrapper<Article> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(Article::getId,article.getId());
        //设置一个 为了在多线程的环境下 线程安全
        //乐观锁的一个思想 如果操作的时候发现阅读数与期望的阅读数不一致,修改失败
        updateWrapper.eq(Article::getViewCounts,viewCounts);

        articleMapper.update(articleUpdate,updateWrapper);
        try {
            //睡眠 ThredService中的方法 5秒,不会影响主线程的使用,即文章详情会很快的显示出来,不受影响
            Thread.sleep(5000);
            System.out.println("更新完成了~~~");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这里有一个Bug

由于article类中的commentCounts,viewCounts,weight 字段为int

package com.example.blog.entity;

import lombok.Data;

@Data
public class Article {

    public static final int Article_TOP = 1;

    public static final int Article_Common = 0;

    private Long id;

    private String title;

    private String summary;

    private int commentCounts;

    private int viewCounts;

    /**
     * 作者id
     */
    private Long authorId;
    /**
     * 内容id
     */
    private Long bodyId;
    /**
     *类别id
     */
    private Long categoryId;

    /**
     * 置顶
     */
    private int weight = Article_Common;


    /**
     * 创建时间
     */
    private Long createDate;
}

发现:会造成更新阅读次数的时候,将commentCounts,viewCounts,weight 字段设为初始值0,也就是这条语句

articleMapper.update(articleUpdate,updateWrapper);

原因:在更新阅读次数的时候,只要该成员值不为null,mybatisPlus都会把该成员赋值进去更新,也就是把commentCounts,viewCounts,weight这三个int类型的赋值进去

解决:如果设置为integer,该成员值就为null,就不会被更新

总结:需要把与数据库对应字段的成员设置为integer,而不是int类型

修改:

package com.example.blog.entity;

import lombok.Data;

@Data
public class Article {

    public static final int Article_TOP = 1;

    public static final int Article_Common = 0;

    private Long id;

    private String title;

    private String summary;

    private Integer commentCounts;

    private Integer viewCounts;

    /**
     * 作者id
     */
    private Long authorId;
    /**
     * 内容id
     */
    private Long bodyId;
    /**
     *类别id
     */
    private Long categoryId;

    /**
     * 置顶
     */
    private Integer weight;


    /**
     * 创建时间
     */
    private Long createDate;
}

以及对应vo

package com.example.blog.vo;


import lombok.Data;

import java.util.List;

@Data
public class ArticleVo
{

//    @JsonSerialize(using = ToStringSerializer.class)
    private String id;

    private String title;

    private String summary;

    private Integer commentCounts;

    private Integer viewCounts;

    private Integer weight;
    /**
     * 创建时间
     */
    private String createDate;

    private String author;
    /*创建该文章的作者头像*/
    private String avatar;

    private String authorId;

    private ArticleBodyVo body;

    private List<TagVo> tags;/*文章标签*/

    private CategoryVo categoryVo;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值