1 登錄認證
技術點:JWT令牌技術(JSON Web Token)
JWT(JSON Web Token)是一種令牌技術,主要由三部分組成:Header頭部、Payload載荷和Signature簽名。Header頭部存儲令牌的類型(如JWT)和使用的加密算法(如HS256)。Payload載荷包含具體信息,如用戶身份、權限、過期時間等聲明(Claims)。Signature簽名通過加密算法對Header和Payload進行簽名,用于驗證數據完整性和發行者身份。
在實際業務中,用戶登錄時,后端服務器接收客戶端請求并解析傳遞的登錄信息,驗證用戶名和密碼是否正確。若驗證成功,服務器生成JWT令牌返回給前端。后端無需存儲Token,只需保存密鑰(Secret Key)。后續請求時,服務器通過攔截器在請求前攔截,提取JWT并進行解析與驗證:首先檢查簽名是否有效(防止篡改),再校驗Payload中的聲明(如是否過期、權限是否有效)。驗證通過后,放行請求并執行業務邏輯。
Session與Token的對比
2 分頁查詢
PageHelper是一個基于MyBatis的分頁插件,通過攔截MyBatis的執行器實現分頁功能。
當調用 PageHelper.startPage()
設置分頁參數后,MyBatis 會通過其攔截器機制自動觸發分頁邏輯,動態修改后續的 SQL 語句以實現分頁。
分頁參數通過 ThreadLocal
存儲到當前線程的上下文中(PageContext
),確保同一線程內的后續操作可獲取這些參數。PageHelper 在處理完當前 SQL 后,自動清除 ThreadLocal
中的分頁參數,因此同一線程后續的查詢不會被分頁,除非再次調用 startPage()
。
/*** 分頁查詢套餐** @param setmealPageQueryDTO* @return*/@Overridepublic PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {int pageNum = setmealPageQueryDTO.getPage();int pageSize = setmealPageQueryDTO.getPageSize();PageHelper.startPage(pageNum, pageSize);Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);return new PageResult(page.getTotal(), page.getResult());}
-- 原始SQL
SELECT * FROM table;
-- 重寫后(MySQL示例)
SELECT * FROM table LIMIT offset, pageSize;
3 MVC當中的參數注解
1 @RequestBoby:綁定HTTP請求體,反序列化為java對象。-(JSON,XML)
2 @RequestParam:綁定查詢參數。(URL后-問號傳參,參數用??
?分隔,參數間用?&
?連接。)
3 @PathVariable:綁定URL路徑變量。(URL中-路徑傳參,參數用用?{}
?包裹,/連接。)
4 @RequestHeader :綁定HTTP請求頭。
5 @CookieValue:綁定Cookie
4 ThreadLocal
ThreadLocal是Java中的一個線程變量,它可以為每個線程提供一個獨立的變量副本。ThreadLocal實例是共享的,但每個線程通過它訪問的是自己的ThreadLocalMap中的值。
ThreadLocal的主要作用是在多線程的環境下提供線程安全的變量訪問。它常用于解決線程間數據共享的問題,特別是在并發編程中,當多個線程需要使用同一個變量時,可以使用ThreadLocal確保每個線程訪問的都是自己的變量副本,從而避免了線程安全問題。
ThreadLocal底層是通過ThreadLocalMap來實現的,每一個Thread(線程)對象中都存在一個ThreadLocalMap,Map的key為ThreadLocal對象,Map的value為需要緩存的值。
static修飾的ThreadLocal對象屬于類級別,在JVM的整個生命周期中僅初始化一次,后續所有的線程通過BaseContext.threadLocal訪問同一個ThreadLocal實例,但每個線程的變量副本獨立存儲,避免重復創建對象。
內存泄漏問題:當ThreadLocal對象使用完之后,應該將Entry對象(即key和value)回收。而線程對象是通過強引用指向ThreadLocalMap,ThreadLocalMap也是通過強引用指向Entry對象。在Entry中,key是弱引用,會觸發自動回收機制,但value是強引用不會自動回收,最終導致Entry整體無法被回收機制回收。最終導致線程池中的線程因ThreadLocalMap未清理而出現內存泄漏。解決方法是手動調用ThreadLocal的remove()方法,清除Entry對象。
package com.sky.context;public class BaseContext {public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();public static void setCurrentId(Long id) {threadLocal.set(id);}public static Long getCurrentId() {return threadLocal.get();}public static void removeCurrentId() {threadLocal.remove();}}
示例:
public class ThreadLocalExample {// 定義一個ThreadLocal變量private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();public static void main(String[] args) {// 線程A設置值Thread threadA = new Thread(() -> {threadLocal.set(100); // 線程A的值為100System.out.println("線程A的值:" + threadLocal.get()); // 輸出100});// 線程B嘗試獲取值Thread threadB = new Thread(() -> {System.out.println("線程B的值:" + threadLocal.get()); // 輸出null(未設置時默認值)threadLocal.set(200); // 線程B的值為200System.out.println("線程B的值:" + threadLocal.get()); // 輸出200});threadA.start();threadB.start();}
}
項目當中便可使用這個來存儲用戶的id值其可全局獲取,并且不需要多次實例化對象,在jwt校驗結束便可設置。
5 @JSONFormat
在業務需求當中可能會出現前端給我們傳遞過來的時間參數,其格式不一定符合我們變量的格式。
因此我們需要對前端的時間參數進行格式化,將前端傳遞的參數指定為pattern當中的參數格式。
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime createTime;
6 基于注解和AOP的公共字段填充
在業務開發中,由于存在大量數據表且字段重疊較多,我們可以使用AOP技術結合注解技術對公共字段進行填充,從而減少代碼的冗雜性。
首先,我們需要創建一個自定義注解,用于標記需要自動填充公共字段的方法。
注解:AutoFill
注解的目的作用在Mapper業務層,對那些需要對數據庫操作的進行字段填充。
package com.sky.annotation;import com.sky.enumeration.OperationType;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 自定義注解,用于標識需要自動填充的字段*/// 標識在方法上
@Target(ElementType.METHOD)
// 標識在運行時
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {//數據庫操作類型 UPDATE INSERTOperationType value();}
然后,AOP切面攔截這些注解的方法
切面:AutoFillAspect
使用的是前置通知,目標方法執行前自動調用,攔截帶有@AutoFill的方法,業務當中通過反射的思想進行字段賦值。
package com.sky.aspect;import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;
import java.time.LocalDateTime;/*** 自定義切面,用于自動填充公共字段處理邏輯*/@Aspect
@Component
@Slf4j
public class AutoFillAspect {/*** 切入點表達式 com.sky.mapper 包下的所有類中的所有方法并且有 @AutoFill 注解的方法*/@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")public void autoFillPointCut() {}/*** 前置通知,在目標方法執行前執行*/@Before("autoFillPointCut()")public void autoFill(JoinPoint joinPoint) {log.info("開始進行公共字段自動填充...");// 通過方法簽名獲取方法上的注解MethodSignature signature = (MethodSignature) joinPoint.getSignature();AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); // 關鍵修改if (autoFill == null) {log.info("當前方法沒有 @AutoFill 注解,不需要自動填充");return;}OperationType operationType = autoFill.value();//數據庫操作類型//獲取當前杯攔截的方法的參數--實體對象Object[] args = joinPoint.getArgs();if (args == null || args.length == 0) {log.info("當前方法沒有參數,不需要自動填充");return;}Object entity = args[0];log.info("當前自動填充的實體對象:{}", entity.toString());//準備賦值的數據LocalDateTime now = LocalDateTime.now();Long currentId = BaseContext.getCurrentId();log.info("當前操作的用戶id:{}", currentId);//根據當前不同的操作類型,為對應的實體對象通過反射來賦值if (operationType == OperationType.INSERT) {//四個公共字段:createTime、createUser、updateTime、updateUser賦值try {//獲取當前實體類中的對應方法(使用本地的常量方法名)Method createTimeMethod = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);Method createUserMethod = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);Method updateTimeMethod = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method updateUserMethod = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);//通過反射為實體對象賦值createTimeMethod.invoke(entity, now);createUserMethod.invoke(entity, currentId);updateTimeMethod.invoke(entity, now);updateUserMethod.invoke(entity, currentId);log.info("為實體類 {} 賦值成功", entity);} catch (Exception e) {e.printStackTrace();}} else if (operationType == OperationType.UPDATE) {try {//兩個公共字段:updateTime、updateUser賦值Method updateTimeMethod = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method updateUserMethod = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);updateTimeMethod.invoke(entity, now);updateUserMethod.invoke(entity, currentId);log.info("為實體類 {} 賦值成功", entity);} catch (Exception e) {e.printStackTrace();}}}
}
示例:(這段代碼就實現了對公共字段的填充)
/*** 新增套餐** @param setmeal*/@AutoFill(OperationType.INSERT)void insert(Setmeal setmeal);
7 個人感悟
在初次接觸項目時第一感覺就是覺得太復雜了,當時看見那么多的類文件,覺得自己肯定學不好也學不會,但是不斷的接觸才發現,是有其自己的一套方法,也是可以接受的,也能跟著照葫蘆畫瓢,其三層架構十分具有條理性的將代碼進行分割將業務代碼進行拆分,在這個項目當中解除了后端方面對基礎業務CRUD的實現,同時也有一些常見開發規范的學習,以及小程序端的開發,實現前后端的聯調實現業務的完整性,但是學習的過程也是有不足的很多實現的過程都是跟著老師進行開發的很多都沒有由自己開發實現,自己單獨分析接口文檔進行接口編寫的能力還是有所欠缺。在學習過程中也發現自己之前的遺漏點,也是一種學習的方法,遇到不會的再向前學習,再進行運用,