基于SpringBoot的哈士奇博客项目(三)

(一)和(二)部分前面已有提供,可自行查阅。

2、前台博客

2.19、AOP实现日志记录

2.19.1、需求分析

需要通过日志记录接口调用信息。便于后期调试排查。并且可能有很多接口都需要进行日志的记录。

2.19.2、思路分析

相当于是对原有的功能进行增强,并且是批量的增强,这个时候就非常适合用AOP来进行实现。

2.19.3、代码实现

日志打印格式【可以将此处作为一个表,即日志表】 【ctrl+alt+鼠标:找出接口实现类】

公共子模块里的annotation包

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SystemLog {

    // default 后面指定默认值
    String businessName();
}

公共子模块里的aspect包

import com.alibaba.fastjson.JSON;
import com.hashiqi.annotation.SystemLog;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

@Component
@Aspect
@Slf4j
public class LogAspect {

    @Pointcut("@annotation(com.hashiqi.annotation.SystemLog)")
    public void pt() {

    }

    @Around("pt()")
    public Object printLog(ProceedingJoinPoint joinPoint) throws Throwable {

        Object ret;
        try {
            handleBefore(joinPoint);
            ret = joinPoint.proceed();
            handleAfter(ret);
        } finally {
            // 结束后执行【System.lineSeparator() 即换行符】
            log.info("执行结束:" + System.lineSeparator());
        }
        return null;
    }

    // 处理前
    private void handleBefore(ProceedingJoinPoint joinPoint) {
        // 这个方法能够在大量数据进入也不会发生错乱,底层可能使用了ThreadLocal
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        // 获取被增强方法上的注解对象
        SystemLog systemLog =  getSystemLog(joinPoint);

        // 这里可以将执行SQL也进行打印并保存【还没有尝试过】
        log.info("日志开始打印");
        // 请求的URL
        log.info("URL=============:{}", request.getRequestURL());
        // 描述信息
        log.info("BusinessName====:{}", systemLog.businessName());
        // 请求方式
        log.info("HTTP Method=====:{}", request.getMethod());
        // 调用controller的全路径以及执行方法 (joinPoint.getSignature()).getName();
        log.info("Class Method====:{},{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
        // 请求的IP
        log.info("IP==============:{}", request.getRemoteHost());
        // 请求输入的参数
        log.info("Request Args====:{}", JSON.toJSONString(joinPoint.getArgs()));
    }

    // 获取被增强方法上的注解对象
    private SystemLog getSystemLog(ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        return methodSignature.getMethod().getAnnotation(SystemLog.class);
    }

    // 处理后
    private void handleAfter(Object ret) {
        // 打印输出的参数
        log.info("响应体===========:{}", JSON.toJSONString(ret));
    }
}

在controller层

方法上加上对应的@SystemLog(businessName = "xxx")即可。

2.20、更新浏览次数

2.20.1、需求分析

在用户浏览博客的文章时需要实现对应的博客文章浏览量的新增。

2.20.2、思路分析

只需要在每次用户浏览博客时更新对应的浏览数即可。

需要思考的是,如果直接操作博客表的浏览量的话,在并发量大的情况下,又要怎样去实现优化。

解决方式

首先,在应用启动时把博客的浏览量存储到redis中;

接着,更新浏览量时区更新redis中的数据;

然后,每隔一段时间把redis中的数据更新到数据库里;

最后,读取文章浏览量就从redis中去读即可。

2.20.3、前提条件

2.20.3.1、CommandLineRunner实现项目启动时预处理

如果希望在SpringBoot应用启动时进行一些初始化操作可以选择使用CommandLineRunner来进行处理。

只需要实现CommandLineRunner接口,并且把对应的bean注入容器。把相关初始化的代码放到需要重新的方法中。

这样就会在应用启动的时候执行对应的代码。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

@Component
public class TestRunner implements CommandLineRunner {

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public void run(String... args) throws Exception {
        redisTemplate.opsForValue().set("start", "项目启动");
    }
}

先进行测试。

2.20.3.2、定时任务

定时任务的实现方式有很多,比如XXL-Job等。但是其实核心功能和概念都是类似的,很多情况下只是调用的API不同而已。

这里就先用SpringBoot提供的定时任务的API来实现一个简单的定时任务。

实现步骤如下:

使用@EnableScheduling注解开启定时任务功能

可以在启动类上加上@EnableScheduling,当然也可以在配置类上加上。

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@MapperScan("com.hashiqi.mapper")
@EnableScheduling
public class BlogApplication {
    public static void main(String[] args) {
        SpringApplication.run(BlogApplication.class, args);
    }
}

确定定时任务执行代码,并配置任务执行时间

使用@Scheduled注解标识需要定时执行的代码。

注解的cron属性相当于是任务的执行时间。

这里使用 0/5 * * * * ? 进行测试,代表从0秒开始,每隔5秒执行一次。

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

@Component
public class TestJob {

    @Scheduled(cron = "0/5 * * * * ?")
    public void testJob() {
        System.out.println("定时任务开始执行");
    }
}

2.20.3.3、cron表达式

cron表达式是用来设置定时任务执行时间。

很多情况下可以使用 在线Cron表达式生成器 来帮助我们理解cron表达式和书写cron表达式。

用到的 0/5 * * * * ? *,cron表达式由七部分组成,中间由空格分隔,这七部分从左往右依次是:

秒(0~59),分钟(0~59),小时(0~23),日期(1-月最后一天),月份(1-12),星期几(1-7,1表示星期日),年份(一般该项不设置,直接忽略掉,即可为空值)。

关于cron表达式可以自行查阅资料,这里就不过多介绍,请谅解。

2.20.4、接口设计

请求方式请求地址请求头
PUT/article/updateViewCount/{id}不需要token请求头

参数:请求路径中携带文章id

响应格式:

{
    "code":200,
    "msg":"操作成功"
}

2.20.5、代码实现

在应用程序启动时把博客的各篇文章的浏览量存储到redis中

实现CommandLineRunner接口,在应用启动时初始化缓存。

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hashiqi.constants.RedisKeyConstants;
import com.hashiqi.constants.SystemConstants;
import com.hashiqi.domain.entity.Article;
import com.hashiqi.mapper.ArticleMapper;
import com.hashiqi.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Component
public class ViewCountRunner implements CommandLineRunner {

    @Autowired
    private ArticleMapper articleMapper;

    @Autowired
    private RedisCache redisCache;

    /**
     * 定时任务
     * @param args 参数
     * @throws Exception 异常
     */
    @Override
    public void run(String... args) throws Exception {
        // 查询博客中已发布的所有文章的id和浏览量
        List<Article> articles = articleMapper.selectList(new LambdaQueryWrapper<Article>()
                .eq(Article::getStatus, SystemConstants.ARTICLE_STATUS_NORMAL));
        Map<String, Integer> viewCountMap = articles.stream().collect(Collectors.toMap(
                article -> article.getId().toString(),
                article -> article.getViewCount().intValue()
        ));
        // 存储到redis中
        redisCache.setCacheMap(RedisKeyConstants.ARTICLE_VIEW_COUNT, viewCountMap);
    }
}

更新文章浏览量,即更新redis中的对应key的值

往RedisCache中自定义一个方法,用于增加文章浏览量记录

/**
 * 增加某个Map中的key的值的数据
 *
 * @param key Redis键
 * @param hKey Hash键集合
 * @param value 对相应Key的增量值
 */
public void incrementCacheMapValue(String key, String hKey, long value) {
    redisTemplate.boundHashOps(key).increment(hKey, value);
}

ArticelController

// 更新对应文章id浏览量
@PutMapping("/updateViewCount/{id}")
public ResponseResult updateViewCount(@PathVariable("id") Long id) {
    return articleService.updateViewCount(id);
}

ArticleService

// 更新对应文章id浏览量
ResponseResult updateViewCount(Long id);

ArticleServiceImpl

/**
 * 更新对应文章id浏览量
 * @param id 文章id
 * @return 返回结果集
 */
@Override
public ResponseResult updateViewCount(Long id) {
    // 更新redis中对应文章id的浏览量
    redisCache.incrementCacheMapValue(RedisKeyConstants.ARTICLE_VIEW_COUNT, id.toString(), SystemConstants.ARTICLE_ADD_VIEW_COUNT);
    return ResponseResult.okResult();
}

定时任务每隔一段时间把redis中到的文章浏览量更新到数据库中

在Article实体类中增加构造方法

public Article(Long id, long viewCount) {
    this.id = id;
    this.viewCount = viewCount;
}

在前台博客模块中的job包中

import com.hashiqi.constants.CronConstants;
import com.hashiqi.constants.RedisKeyConstants;
import com.hashiqi.domain.entity.Article;
import com.hashiqi.service.ArticleService;
import com.hashiqi.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Component
public class UpdateViewCountJob {

    @Autowired
    private RedisCache redisCache;

    @Autowired
    private ArticleService articleService;

    @Scheduled(cron = CronConstants.EVERY_FIVE_SECONDS)
    public void updateViewCount() {
        // 获取redis中的浏览量
        Map<String, Integer> viewCountMap = redisCache.getCacheMap(RedisKeyConstants.ARTICLE_VIEW_COUNT);
        List<Article> articles = viewCountMap.entrySet().stream().map(
                entry -> new Article(Long.valueOf(entry.getKey()), entry.getValue().longValue())
        ).collect(Collectors.toList());
        // 更新到数据库中
        // 方式一:应该是往Article中添加了其他属性,导致更新时产生空指针异常
        // articleService.updateBatchById(articles);
        // 方式二:直接指定属性更新
        articles.forEach(article -> {
            System.out.println("id:" + article.getId() + ",   viewCount:" + article.getViewCount());
            articleService.update(new LambdaUpdateWrapper<Article>()
                    .eq(Article::getId, article.getId())
                    .set(Article::getViewCount, article.getViewCount()));
        });
    }
}

公共子模块中constants包

/**
 * 定时任务表达式
 */
public class CronConstants {

    /**
     * 每隔五秒钟执行一次
     */
    public static final String EVERY_FIVE_SECONDS = "0/5 * * * * ?";
    /**
     * 每隔半分钟执行一次
     */
    public static final String EVERY_THIRTY_SECONDS = "0/30 * * * * ?";
}

文章浏览量从redis中读取

修改ArticleServiceImpl中获取文章详情的方法

/**
 * 通过文章id获取文章详情
 * @param id 文章id
 * @return 文章详情
 */
@Override
public ResponseResult getArticleDetail(Long id) {
    // 根据文章id获取文章,并判断是否存在
    Article article = this.getById(id);
    if (null == article) {
        throw new RuntimeException("文章不存在");
    }
    
    // 从redis中获取viewCount
    Integer viewCount = redisCache.getCacheMapValue(RedisKeyConstants.ARTICLE_VIEW_COUNT, id.toString());
    article.setViewCount(viewCount.longValue());
    
    // 将Article封装成ArticleDetailVO
    ArticleDetailVO articleDetailVO = BeanCopyUtils.copyBean(article, ArticleDetailVO.class);
    //根据分类id查询分类名
    Category category = categoryService.getById(articleDetailVO.getCategoryId());
    if (null == category) {
        throw new RuntimeException("分类不存在");
    }
    articleDetailVO.setCategoryName(category.getName());
    return ResponseResult.okResult(articleDetailVO);
}

3、Swagger2

3.1、简介

Swagger是一套基于OpenAPI规范构建的开源工具,可以帮助我们设计、构建、记录以及使用Rest API。

3.2、Swagger的优点

  • 代码变,文档变。只需要少量的注解,Swagger 就可以根据代码自动生成 API 文档,很好的保证了文档的时效性。
  • 跨语言性。
  • Swagger UI 呈现出来的是一份可交互式的 API 文档,可以直接在文档页面尝试 API 的调用,省去了准备复杂的调用参数的过程。

3.3、入门

3.3.1、引入相应依赖

<!-- swagger2 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
</dependency>
<!-- swaggerUI -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
</dependency>

3.3.2、启用Swagger2

在启动类或配置类上加上@EnableSwagger 2

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@SpringBootApplication
@MapperScan("com.hashiqi.mapper")
@EnableScheduling
@EnableSwagger2
public class BlogApplication {
    public static void main(String[] args) {
        SpringApplication.run(BlogApplication.class, args);
    }
}

3.3.3、测试

http://本机地址:端口号/swagger-ui.html

3.4、具体配置

3.4.1、控制层Controller配置

@api

@RestController
@RequestMapping("/article")
@Api(tags = "文章接口管理", description = "文章相关接口")
public class ArticleController {
}

3.4.2、接口配置

3.4.2.1、接口描述配置@ApiOperation
// 查询热门文章
@GetMapping("/hotArticleList")
@ApiOperation(value = "查询热门文章【10篇】")
public ResponseResult hotArticleList(){
    ResponseResult result =  articleService.hotArticleList();
    return result;
}

3.4.2.2、接口参数描述

@ApiImplicitParam用于描述接口的参数,但是一个接口可能有多个参数,所以一般与@ApiImplicitParams组合使用。二者一般都是放在接口方法上。

如果要放在接口参数前,则可以使用@ApiParam进行注解。

// 获取文章列表
@GetMapping("/articleList")
@ApiOperation(value = "获取文章列表")
@ApiImplicitParams({
        @ApiImplicitParam(name = "pageNum", value = "当前页"),
        @ApiImplicitParam(name = "pageSize", value = "每页大小"),
        @ApiImplicitParam(name = "categoryId", value = "文章分类id")
})
public ResponseResult articleList(
        Integer pageNum,
        Integer pageSize,
        Long categoryId) {
    return articleService.articleList(pageNum, pageSize, categoryId);
}
// 通过文章id获取文章详情
@GetMapping("/{id}")
@ApiOperation(value = "通过文章id获取文章详情")
public ResponseResult getArticleDetail(@PathVariable("id") @ApiParam("文章id") Long id) {
    return articleService.getArticleDetail(id);
}

3.4.3、实体类配置

如果是接口方法接收实体类的话,那么就要使用实体类进行配置。

3.4.3.1、@ApiModel

用于实体类

@Data
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
@NoArgsConstructor
@TableName("tb_article")
@Accessors(chain = true)
@ApiModel(description = "文章实体")
public class Article implements Serializable {
}

3.4.3.2、@ApiModelProperty

用于实体类中的属性

/**
 * 是否置顶(1:是,0:否)
 */
@ApiModelProperty(notes = "是否置顶(1:是,0:否)")
private String isTop;

除了这些,当然还有别的,例如@Max,表示在接口方法中接收参数的值的范围要最大值的范围内,否则抛出异常。

但是需要导入依赖,并且在接口参数里加上@Valid、Validated。

<!-- validation -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
出现了异常! org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.hashiqi.domain.ResponseResult com.hashiqi.controller.CommentController.addComment(com.hashiqi.domain.dto.AddCommentDTO): [Field error in object 'addCommentDTO' on field 'content': rejected value []; codes [NotBlank.addCommentDTO.content,NotBlank.content,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [addCommentDTO.content,content]; arguments []; default message [content]]; default message [评论内容不能为空]] 

这需要配置一个异常处理类

import com.crm.logistics_crm.bean.Result;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
 
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;
 
@ControllerAdvice
public class WebExceptionHandler {
 
    //处理Get请求中 使用@Valid 验证路径中请求实体校验失败后抛出的异常
    @ExceptionHandler(BindException.class)
    @ResponseBody
    public Result BindExceptionHandler(BindException e) {
        String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
        return Result.error(message);
    }
 
    //处理请求参数格式错误 @RequestParam上validate失败后抛出的异常是javax.validation.ConstraintViolationException
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseBody
    public Result ConstraintViolationExceptionHandler(ConstraintViolationException e) {
        String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining());
        return Result.error(message);
    }
 
    //处理请求参数格式错误 @RequestBody上validate失败后抛出的异常是MethodArgumentNotValidException异常。
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public Result MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
        return Result.error(message);
    }
}

3.4.4、文档信息配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

@Configuration
public class SwaggerConfig {

    // 前台博客接口文档
    @Bean
    public Docket customDocket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("blogApi")
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.hashiqi.controller"))
                .build();
    }

    private ApiInfo apiInfo() {
        Contact contact = new Contact("哈士奇Group", "http://www.xxx.com", "xxx@xxx.com");
        return new ApiInfoBuilder()
                .title("博客-Api文档")
                .description("本文档描述了博客接口定义")
                .contact(contact)   // 联系方式
                .version("1.1.0")  // 版本
                .build();
    }
}

到这里,前台博客项目的后端代码也就完成了 ,当然,你可以对此进行补充。。博客后台管理代码敬请期待~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值