苍穹外卖心得体会

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()

java 复制代码
    /**
     * 分页查询套餐
     *
     * @param setmealPageQueryDTO
     * @return
     */
    @Override
    public 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 复制代码
-- 原始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对象。

java 复制代码
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();}

}

示例:

java 复制代码
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的值为100
            System.out.println("线程A的值:" + threadLocal.get()); // 输出100
        });

        // 线程B尝试获取值
        Thread threadB = new Thread(() -> {
            System.out.println("线程B的值:" + threadLocal.get()); // 输出null(未设置时默认值)
            threadLocal.set(200); // 线程B的值为200
            System.out.println("线程B的值:" + threadLocal.get()); // 输出200
        });

        threadA.start();
        threadB.start();
    }
}

项目当中便可使用这个来存储用户的id值其可全局获取,并且不需要多次实例化对象,在jwt校验结束便可设置。

5 @JSONFormat

在业务需求当中可能会出现前端给我们传递过来的时间参数,其格式不一定符合我们变量的格式。

因此我们需要对前端的时间参数进行格式化,将前端传递的参数指定为pattern当中的参数格式。

java 复制代码
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;

6 基于注解和AOP的公共字段填充

在业务开发中,由于存在大量数据表且字段重叠较多,我们可以使用AOP技术结合注解技术对公共字段进行填充,从而减少代码的冗杂性。

首先,我们需要创建一个自定义注解,用于标记需要自动填充公共字段的方法。

注解:AutoFill

注解的目的作用在Mapper业务层,对那些需要对数据库操作的进行字段填充。

java 复制代码
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 INSERT
    OperationType value();

}

然后,AOP切面拦截这些注解的方法

切面:AutoFillAspect

使用的是前置通知,目标方法执行前自动调用,拦截带有@AutoFill的方法,业务当中通过反射的思想进行字段赋值。

java 复制代码
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();
            }

        }


    }
}

示例:(这段代码就实现了对公共字段的填充)

java 复制代码
    /**
     * 新增套餐
     *
     * @param setmeal
     */
    @AutoFill(OperationType.INSERT)
    void insert(Setmeal setmeal);

7 个人感悟

在初次接触项目时第一感觉就是觉得太复杂了,当时看见那么多的类文件,觉得自己肯定学不好也学不会,但是不断的接触才发现,是有其自己的一套方法,也是可以接受的,也能跟着照葫芦画瓢,其三层架构十分具有条理性的将代码进行分割将业务代码进行拆分,在这个项目当中解除了后端方面对基础业务CRUD的实现,同时也有一些常见开发规范的学习,以及小程序端的开发,实现前后端的联调实现业务的完整性,但是学习的过程也是有不足的很多实现的过程都是跟着老师进行开发的很多都没有由自己开发实现,自己单独分析接口文档进行接口编写的能力还是有所欠缺。在学习过程中也发现自己之前的遗漏点,也是一种学习的方法,遇到不会的再向前学习,再进行运用,

相关推荐
SuperherRo13 分钟前
Web开发-JavaEE应用&SpringBoot栈&模版注入&Thymeleaf&Freemarker&Velocity
spring boot·java-ee·thymeleaf·freemarker·模板注入·velocity
刘 大 望13 分钟前
Java写数据结构:队列
java·数据结构·intellij-idea
Humbunklung15 分钟前
Sigmoid函数简介及其Python实现
开发语言·python·深度学习·机器学习
bing_15817 分钟前
Spring MVC @RequestParam 注解怎么用?如何处理可选参数和默认值?
java·spring·mvc·requestparam
__lost24 分钟前
MATLAB退火算法和遗传算法解决旅行商问题
开发语言·算法·matlab·遗传算法·退火算法
恶霸不委屈31 分钟前
MATLAB函数调用全解析:从入门到精通
开发语言·算法·matlab·匿名函数·函数句柄
Clf丶忆笙38 分钟前
Java IO流与NIO终极指南:从基础到高级应用
java·网络·nio
建群新人小猿1 小时前
CRMEB-PRO系统定时任务扩展开发指南
android·java·开发语言·前端
程序员秘密基地1 小时前
基于c#,asp.net webform, sql server数据库,在线档案管理系统
开发语言·sqlserver·asp.net·.net·源代码管理
知识分享小能手1 小时前
JavaScript学习教程,从入门到精通,Ajax数据交换格式与跨域处理(26)
xml·开发语言·前端·javascript·学习·ajax·css3