寫文章
寫文章需要 三個介面:
-
獲取所有文章類別
-
獲取所有標籤
-
釋出文章
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 | 文章類別 | |
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;
}
}