(一)和(二)部分前面已有提供,可自行查阅。
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();
}
}
到这里,前台博客项目的后端代码也就完成了 ,当然,你可以对此进行补充。。博客后台管理代码敬请期待~~