写文章
写文章需要 三个接口:
-
获取所有文章类别
-
获取所有标签
-
发布文章
1. 所有文章分类
1.1 接口说明
接口url:/categorys
请求方式:GET
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
返回数据:
{
"success":true,
"code":200,
"msg":"success",
"data":
[
{"id":1,"avatar":"/category/front.png","categoryName":"前端"},
{"id":2,"avatar":"/category/back.png","categoryName":"后端"},
{"id":3,"avatar":"/category/lift.jpg","categoryName":"生活"},
{"id":4,"avatar":"/category/database.png","categoryName":"数据库"},
{"id":5,"avatar":"/category/language.png","categoryName":"编程语言"}
]
}
1.2 Controller
package com.cherriesovo.blog.controller;
import com.cherriesovo.blog.service.CategoryService;
import com.cherriesovo.blog.vo.Result;
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;
@RestController
@RequestMapping("categorys")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@GetMapping
public Result listCategory() {
return categoryService.findAll();
}
}
1.3 Service
public interface CategoryService {
Result findAll(); //类别查询
}
@Service
public class CategoryServiceImpl implements CategoryService {
public CategoryVo copy(Category category){
CategoryVo categoryVo = new CategoryVo();
BeanUtils.copyProperties(category,categoryVo);
return categoryVo;
}
public List<CategoryVo> copyList(List<Category> categoryList){
List<CategoryVo> categoryVoList = new ArrayList<>();
for (Category category : categoryList) {
categoryVoList.add(copy(category));
}
return categoryVoList;
}
@Override
public Result findAll() {
//SELECT * FROM category;
List<Category> categories = this.categoryMapper.selectList(new LambdaQueryWrapper<>());
return Result.success(copyList(categories));
}
}
2. 所有文章标签
2.1 接口说明
接口url:/tags
请求方式:GET
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"id": 5,
"tagName": "springboot"
},
{
"id": 6,
"tagName": "spring"
},
{
"id": 7,
"tagName": "springmvc"
},
{
"id": 8,
"tagName": "11"
}
]
}
2.2 Controller
@RestController
@RequestMapping("tags")
public class TagsController {
@Autowired
private TagService tagsService;
@GetMapping
public Result findAll(){
return tagsService.findAll();
}
}
2.3 Service
public interface TagService {
Result findAll(); //查询所有的文章标签
}
TagServiceImpl:
@Override
public Result findAll() {
//SELECT * FROM tag;
List<Tag> tags = this.tagMapper.selectList(new LambdaQueryWrapper<>());
return Result.success(copyList(tags));
}
3. 发布文章
3.1 接口说明
接口url:/articles/publish
请求方式:POST
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
title | string | 文章标题 |
id | long | 文章id(编辑有值) |
body | object({content: “ww”, contentHtml: “ ww ↵”}) | 文章内容 |
category | {id: 2, avatar: “/category/back.png”, categoryName: “后端”} | 文章类别 |
summary | string | 文章概述 |
tags | [{id: 5}, {id: 6}] | 文章标签 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": {"id":12232323}
}
3.2 Controller
package com.cherriesovo.blog.vo.params;
import com.cherriesovo.blog.vo.CategoryVo;
import com.cherriesovo.blog.vo.TagVo;
import lombok.Data;
import java.util.List;
@Data
public class ArticleParam {
private Long id;
private ArticleBodyParam body;
private CategoryVo category;
private String summary;
private List<TagVo> tags;
private String title;
}
package com.cherriesovo.blog.vo.params;
import lombok.Data;
@Data
public class ArticleBodyParam {
private String content;
private String contentHtml;
}
//json数据进行交互
@RestController
@RequestMapping("articles")
public class ArticleController {
/*
* 发布文章
* */
@PostMapping("publish")
public Result publish(@RequestBody ArticleParam articleParam){
return articleService.publish(articleParam);
}
}
3.3 Service
public interface ArticleService {
//文章发布
Result publish(ArticleParam articleParam);
}
ArticleServiceImpl:
ArticleServiceImpl共需要经历如下步骤:
创建一个
Article
对象,并设置其属性,最后将文章对象插入到数据库中。Article article = new Article(); article.setAuthorId(sysUser.getId()); article.setCategoryId(articleParam.getCategory().getId()); article.setCreateDate(System.currentTimeMillis()); article.setCommentCounts(0); article.setSummary(articleParam.getSummary()); //摘要 article.setTitle(articleParam.getTitle()); article.setViewCounts(0); article.setWeight(Article.Article_Common); //设置了文章的 bodyId 属性为 -1L。通常情况下,-1L 通常被用作一个特殊的标记,表示某个值无效或未设置 article.setBodyId(-1L); //内容id //插入之后会自动生成一个文章id this.articleMapper.insert(article);
将文章id与标签id进行关联——获取文章的标签列表,遍历标签列表,对每个标签执行以下操作:
- 创建一个 ArticleTag 对象,并设置其文章ID和标签ID。
- 将 ArticleTag 对象插入到数据库中(article_tag表)。
List<TagVo> tags = articleParam.getTags(); if (tags != null) { for (TagVo tag : tags) { ArticleTag articleTag = new ArticleTag(); articleTag.setArticleId(article.getId()); articleTag.setTagId(tag.getId()); this.articleTagMapper.insert(articleTag); } }
文章内容存储(article_body表)
ArticleBody articleBody = new ArticleBody(); articleBody.setContent(articleParam.getBody().getContent()); articleBody.setContentHtml(articleParam.getBody().getContentHtml()); articleBody.setArticleId(article.getId()); articleBodyMapper.insert(articleBody);
更新article表中的body属性
article.setBodyId(articleBody.getId()); articleMapper.updateById(article);
设置返回值
ArticleVo articleVo = new ArticleVo(); articleVo.setId(article.getId()); return Result.success(articleVo);
@Override
@Transactional
public Result publish(ArticleParam articleParam) {
/*
* 1、发布文章目的是构建article对象
* 2、作者id——当前的登录用户
* 3、要将标签加入到关联列表
* 4、body 内容存储 要的是bodyId
* */
//此接口要加入到登录拦截中,否则会造成空指针异常
SysUser sysUser = UserThreadLocal.get();
Article article = new Article();
article.setAuthorId(sysUser.getId());
article.setCategoryId(articleParam.getCategory().getId());
article.setCreateDate(System.currentTimeMillis());
article.setCommentCounts(0);
article.setSummary(articleParam.getSummary()); //摘要
article.setTitle(articleParam.getTitle());
article.setViewCounts(0);
article.setWeight(Article.Article_Common);
//设置了文章的 bodyId 属性为 -1L。通常情况下,-1L 通常被用作一个特殊的标记,表示某个值无效或未设置
article.setBodyId(-1L); //内容id
//插入之后会自动生成一个文章id
this.articleMapper.insert(article);
List<TagVo> tags = articleParam.getTags();
if (tags != null) {
for (TagVo tag : tags) {
ArticleTag articleTag = new ArticleTag();
articleTag.setArticleId(article.getId());
articleTag.setTagId(tag.getId());
this.articleTagMapper.insert(articleTag);
}
}
//body内容存储(article_body表)
ArticleBody articleBody = new ArticleBody();
articleBody.setContent(articleParam.getBody().getContent());
articleBody.setContentHtml(articleParam.getBody().getContentHtml());
articleBody.setArticleId(article.getId());
articleBodyMapper.insert(articleBody);
//更新article表中的body
article.setBodyId(articleBody.getId());
articleMapper.updateById(article);
//设置返回值
ArticleVo articleVo = new ArticleVo();
articleVo.setId(article.getId());
return Result.success(articleVo);
}
package com.cherriesovo.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cherriesovo.blog.dao.pojo.ArticleTag;
public interface ArticleTagMapper extends BaseMapper<ArticleTag> {
}
package com.cherriesovo.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cherriesovo.blog.dao.pojo.ArticleBody;
public interface ArticleBodyMapper extends BaseMapper<ArticleBody> {
}
package com.cherriesovo.blog.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.util.List;
@Data
public class ArticleVo {
//一定要记得加 要不然 会出现精度损失
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private String title;
private String summary;
private Integer commentCounts;
private Integer viewCounts;
private Integer weight;
/**
* 创建时间
*/
private String createDate;
private String author;
private ArticleBodyVo body;
private List<TagVo> tags;
private CategoryVo category;
}
package com.cherriesovo.blog.dao.pojo;
import lombok.Data;
@Data
public class ArticleTag {
private Long id;
private Long articleId;
private Long tagId;
}
当然登录拦截器中,需要加入发布文章的配置:
WebMVCConfig:
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截test接口,后续实际遇到需要拦截的接口时,在配置为真正的拦截接口
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/test")
.addPathPatterns("/comments/create/change")
.addPathPatterns("/articles/publish");
}
3.4 测试
4. AOP日志
定义一个自定义注解
LogAnnotation
,用于在方法上添加日志相关的注解信息:
@Target(ElementType.METHOD)
:这个注解指定了LogAnnotation
注解可以被应用于方法上。@Retention(RetentionPolicy.RUNTIME)
:这个注解指定了LogAnnotation
注解在运行时可见。@Documented
:这个注解指定了LogAnnotation
注解将被包含在 Javadoc 中。String module() default "";
:这个注解定义了一个module
属性,用于指定日志的模块,默认值为空字符串。String operator() default "";
:这个注解定义了一个operator
属性,用于指定执行操作的操作者,默认值为空字符串。这个自定义注解可以用于方法上,用于标记需要记录日志的方法,并且可以通过
module
和operator
属性指定日志的模块和操作者。(Javadoc 是 Java 语言中用于生成 API 文档的工具。它能够根据源代码中的特定标记,自动生成与代码相关的文档。Javadoc 工具会扫描 Java 源代码中特定格式的注释,并根据这些注释生成 HTML 格式的 API 文档。这些注释通常以
/**
开头,以*/
结尾,位于类、方法、字段等代码元素的前面。Javadoc 工具会解析这些注释中的标签和内容,并生成易于阅读和导航的 API 文档。)
package com.cherriesovo.blog.common.aop;
import java.lang.annotation.*;
/**
* 日志注解
*/
//TYPE代表可以放在类上面,METHOD代表可以放在方法上
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
String module() default "";
String operator() default "";
}
LogAspect是一个使用了 Spring AOP的日志切面类:
@Aspect
:这个注解标识了这个类是一个切面类,用于定义通知和切点的关系。
@Pointcut("@annotation(com.cherriesovo.blog.common.aop.LogAnnotation)")
:这个注解定义了一个切点logPointCut()
,它表示当目标方法上存在com.cherriesovo.blog.common.aop.LogAnnotation
注解时,这个切点会匹配到。
public void logPointCut() { }
:这个方法定义了切点的具体内容,但方法体为空,因为它只是用于标识切点,实际的逻辑在通知方法中实现。
@Around("logPointCut()")
:这个注解表示环绕通知,它表示在目标方法执行前后都会执行通知逻辑。
public Object around(ProceedingJoinPoint point) throws Throwable { }
:这个方法是环绕通知的具体实现。在目标方法执行前记录开始时间,在执行后记录结束时间,并记录日志。
private void recordLog(ProceedingJoinPoint joinPoint, long time) { }
:这个方法用于记录日志,它获取了目标方法的签名、注解信息、方法参数、请求信息等,并使用日志记录器将这些信息输出到日志中。
ProceedingJoinPoint
是 Spring AOP 中的一个接口,它提供了对连接点(Join Point)进行操作的功能。在面向切面编程中,连接点表示程序执行过程中的特定点,比如方法的调用或异常的处理等。
ProceedingJoinPoint
是JoinPoint
的子接口,在 Spring AOP 中,它专门用于表示可以执行的连接点,例如在环绕通知中,通过调用proceed()
方法可以执行目标方法。通常,在环绕通知中,我们会将
ProceedingJoinPoint
对象作为参数传递给通知方法,在通知方法中可以通过调用proceed()
方法来继续执行目标方法,也可以获取连接点的信息,如方法签名、参数等。整个类的作用是,当目标方法被调用时,记录下方法的执行时间、方法的输入参数、请求的 IP 地址等信息,并将这些信息输出到日志中,以便进行日志记录和监控。
package com.cherriesovo.blog.common.aop;
import com.alibaba.fastjson.JSON;
import com.cherriesovo.blog.utils.HttpContextUtils;
import com.cherriesovo.blog.utils.IpUtils;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
/**
* 日志切面
*/
@Aspect //切面 定义了通知和切点的关系
@Component
@Slf4j
public class LogAspect {
//切点
@Pointcut("@annotation(com.cherriesovo.blog.common.aop.LogAnnotation)")
public void logPointCut() {
}
//通知类,环绕通知
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis(); //开始时间
//执行方法
Object result = point.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
//保存日志
recordLog(point, time);
return result;
}
//记录日志
private void recordLog(ProceedingJoinPoint joinPoint, long time) {
//获取了目标方法的签名信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//通过签名获取目标方法
Method method = signature.getMethod();
//获取了目标方法上的 LogAnnotation 注解,以便获取注解中的信息
LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
log.info("=====================log start================================");
log.info("module:{}",logAnnotation.module()); //输出日志中的模块信息
log.info("operation:{}",logAnnotation.operator()); //输出日志中的操作信息
String className = joinPoint.getTarget().getClass().getName(); //获取目标方法所属类的类名
String methodName = signature.getName(); //获取目标方法的方法名
//输出请求的方法名,格式为类名.方法名()
log.info("request method:{}",className + "." + methodName + "()");
//请求的参数
Object[] args = joinPoint.getArgs(); //获取目标方法的参数列表
String params = JSON.toJSONString(args[0]); //参数列表转换为 JSON 格式的字符串,这里只获取了第一个参数
log.info("params:{}",params); //输出请求的参数信息
//获取request 设置IP地址
HttpServletRequest request = HttpContextUtils.getHttpServletRequest(); 获取当前的HttpServletRequest对象
log.info("ip:{}", IpUtils.getIpAddr(request)); //输出请求的 IP 地址
log.info("excute time : {} ms",time); //输出方法的执行时间
log.info("=====================log end================================");
}
}
package com.cherriesovo.blog.utils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
public class HttpContextUtils {
/*用于获取当前线程的 HttpServletRequest 对象,通过 RequestContextHolder.getRequestAttributes() 获取到当前请求的属性对 象,然后将其转换为 ServletRequestAttributes,再调用 getRequest() 方法获取到 HttpServletRequest 对象。*/
public static HttpServletRequest getHttpServletRequest(){
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
}
package com.cherriesovo.blog.utils;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
//IP 地址获取工具类 IpUtils,用于从 HTTP 请求中获取客户端的真实 IP 地址
public class IpUtils {
private static Logger logger = LoggerFactory.getLogger(IpUtils.class);
/**
* 获取IP地址
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = null;
try {
ip = request.getHeader("x-forwarded-for");
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} catch (Exception e) {
logger.error("IPUtils ERROR ", e);
}
// 使用代理,则获取第一个IP地址
if (StringUtils.isEmpty(ip) && ip.length() > 15) {
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
}
return ip;
}
}
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} catch (Exception e) {
logger.error("IPUtils ERROR ", e);
}
// 使用代理,则获取第一个IP地址
if (StringUtils.isEmpty(ip) && ip.length() > 15) {
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
}
return ip;
}
}