個人部落格專案筆記_07

CherriesOvO發表於2024-04-11

寫文章

寫文章需要 三個介面:

  1. 獲取所有文章類別

  2. 獲取所有標籤

  3. 釋出文章

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共需要經歷如下步驟:

  1. 建立一個 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);
    
  2. 將文章id與標籤id進行關聯——獲取文章的標籤列表,遍歷標籤列表,對每個標籤執行以下操作:

    1. 建立一個 ArticleTag 物件,並設定其文章ID和標籤ID。
    2. 將 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);
                }
            }
    
  3. 文章內容儲存(article_body表)

    		ArticleBody articleBody = new ArticleBody();
            articleBody.setContent(articleParam.getBody().getContent());
            articleBody.setContentHtml(articleParam.getBody().getContentHtml());
            articleBody.setArticleId(article.getId());
            articleBodyMapper.insert(articleBody);
    
  4. 更新article表中的body屬性

    		article.setBodyId(articleBody.getId());
            articleMapper.updateById(article);
    
  5. 設定返回值

    		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 屬性,用於指定執行操作的操作者,預設值為空字串。

這個自定義註解可以用於方法上,用於標記需要記錄日誌的方法,並且可以透過 moduleoperator 屬性指定日誌的模組和操作者。

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)進行操作的功能。在面向切面程式設計中,連線點表示程式執行過程中的特定點,比如方法的呼叫或異常的處理等。

      ProceedingJoinPointJoinPoint 的子介面,在 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;
    }
}

相關文章