目录

1.jwt令牌

1.1 定义

1.2 组成

1.3 苍穹外卖jwt代码实现

jwt令牌校验类

controller里的登录接口

2.@PathVariable,@RequestParam,@RequestBody

2.1 三者概述

2.2 @PathVariable

2.3 @RequestParam

2.4 @RequestBody

3.对象的属性拷贝(BeanUtils)

4.pagehelper插件实现分页查询

5.ThreadLocal

6.格式化日期

@DateTimeFormat

 @JsonFormat

7.全局异常处理器

7.1 概念

7.2 注释解析(实现流程)

1. @RestControllerAdvice

2. @ExceptionHandler

7.3 苍穹外卖实现案例

1.jwt令牌

1.1 定义

        JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全地传输信息 。这些信息可以通过数字签名进行验证和信任。

1.2 组成

        JWT 通常由三部分组成,分别是 Header(头部)、Payload(负载)和 Signature(签名),各部分之间用点号(.)分隔。

   ① Header(头部)

        Header 主要包含两部分信息:令牌的类型(通常为 JWT)以及使用的签名算法,如 HMAC SHA256 或 RSA。例如,一个典型的 Header 可能是这样的 JSON 对象:

{
    "alg": "HS256",
    "typ": "JWT"
}

 这个 JSON 对象会被 Base64Url 编码,形成 JWT 的第一部分。

  ② Payload(负载)

        Payload 部分用于存放实际的声明(claims),这些声明是关于实体(通常是用户)和其他数据的陈述。在我们的示例代码中,使用 claims.put(JwtClaimsConstant.EMP_ID, employee.getId()); 将员工 ID 作为一个声明放入了 Payload。Payload 中的声明也会被 Base64Url 编码,成为 JWT 的第二部分。

  ③ Signature(签名)

        Signature 用于验证消息在传输过程中没有被更改,并且在使用私钥签名的情况下,还可以验证 JWT 的发送者身份。为了创建签名部分,需要使用编码后的 Header、编码后的 Payload、一个密钥(secret)以及 Header 中指定的签名算法。

1.3 苍穹外卖jwt代码实现

jwt令牌校验类

        以下是课程代码中的JwtTokenAdminInterceptor 类,也就是jwt令牌的校验类

package com.sky.interceptor;

import com.sky.constant.JwtClaimsConstant;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:", empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

        这个类实现了 Spring 的 HandlerInterceptor 接口,用于在请求到达 Controller 方法之前拦截请求并校验 JWT 令牌。

  • 依赖注入:通过 @Autowired 注入 JwtProperties,该属性包含了 JWT 相关的配置信息,如令牌在请求头中的名称和密钥。

preHandle 方法:

        资源类型判断:首先检查被拦截的 handler 是否是 HandlerMethod 类型,如果不是,说明可能是静态资源,直接放行。
        获取令牌:从请求头中获取 JWT 令牌,使用 jwtProperties.getAdminTokenName() 确定令牌在请求头中的名称。
        校验令牌:尝试使用 JwtUtil.parseJWT 方法解析令牌,如果解析成功,从令牌的声明中获取员工 ID 并记录日志,然后放行请求。如果解析失败,捕获异常,设置响应状态码为 401(未授权),并阻止请求继续处理。

controller里的登录接口

@PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
    log.info("员工登录:{}", employeeLoginDTO);

    Employee employee = employeeService.login(employeeLoginDTO);

    //登录成功后,生成jwt令牌
    Map<String, Object> claims = new HashMap<>();
    claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
    String token = JwtUtil.createJWT(
            jwtProperties.getAdminSecretKey(),
            jwtProperties.getAdminTtl(),
            claims);

    EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
          .id(employee.getId())
          .userName(employee.getUsername())
          .name(employee.getName())
          .token(token)
          .build();

    return Result.success(employeeLoginVO);
}

这是一个处理员工登录请求的控制器方法:

  • 接收请求:使用 @PostMapping("/login") 注解表示该方法处理 /login 的 POST 请求,并通过 @RequestBody 接收包含员工登录信息的 EmployeeLoginDTO
  • 登录验证:调用 employeeService.login 方法进行登录验证,获取 Employee 对象。
  • 生成 JWT 令牌:登录成功后,创建一个包含员工 ID 的 claims 映射,然后使用 JwtUtil.createJWT 方法生成 JWT 令牌。这里使用了从 jwtProperties 中获取的密钥和过期时间。

2.@PathVariable,@RequestParam,@RequestBody

2.1 三者概述

               

前端请求的参数传递主要有三种位置,而这三个注解恰好对应了不同位置的参数获取:

  • URL 路径中(@PathVariable):比如/user/100里的100,是资源的标识信息;
  • URL 查询串中(@RequestParam):比如/search?keyword=java&page=1里的键值对,多用作筛选、分页;
  • 请求体中(@RequestBody):比如 POST 请求提交的 JSON 数据,多用来传递复杂的业务数据(如创建用户的完整信息)。

2.2 @PathVariable

        @PathVariable用于提取 URL 路径中用{}标记的 “路径变量”。其通常有以下特点:

  • 参数与 URL 路径深度绑定,格式固定为/{变量名}
  • 支持多个路径变量(如/order/{orderNo}/item/{itemId});
  • 变量名与方法参数名一致时,可省略注解的value属性。

     实战示例:

1.单个路径变量:假设要通过用户 ID 查询用户信息,请求 URL 为/user/100

// 控制器方法
@RequestMapping("/user/{id}")
@ResponseBody
public String getUserById(@PathVariable("id") Long userId) {
    // 这里的"id"对应URL中的{id},参数名userId可自定义
    return "查询到用户ID:" + userId; // 输出:查询到用户ID:100
}

2.变量名一致时省略 value

@RequestMapping("/user/{id}")
@ResponseBody
public String getUserById(@PathVariable Long id) {
    return "查询到用户ID:" + id;
}

3.多个路径变量:查询某个订单下的某个商品,请求 URL 为/order/NO20240501/item/5

@RequestMapping("/order/{orderNo}/item/{itemId}") //有orderNo和itemId两个路径变量
@ResponseBody
public String getOrderItem(
    @PathVariable String orderNo,
    @PathVariable Integer itemId
) {
    return "订单号:" + orderNo + ",商品ID:" + itemId;
    // 输出:订单号:NO20240501,商品ID:5
}

2.3 @RequestParam

        @RequestParam用于获取 URL 中?后面的 “查询参数”(Query String),也就是键值对形式的参数。比如分页查询、关键词搜索等场景,参数通常放在查询串中,方便灵活调整。它有三个属性:

  • value(或name:指定请求参数的名称,若方法参数名与请求参数名一致,可省略该属性。
  • required:指定参数是否必填,默认为true,及请求路径中必须包含参数,否则会报错。
  • defaultValue:指定参数的默认值,若设置了defaultValue,则required属性会被自动视为false,因为默认值会 “兜底”。

实战演练:
1. 必填参数:关键词搜索功能,请求 URL 为/search?keyword=SpringMVC

@RequestMapping("/search") 
@ResponseBody
public String search(@RequestParam("keyword") String key) {
    return "搜索关键词:" + key; // 输出:搜索关键词:SpringMVC
}

2.非必填 + 默认值:分页查询场景,请求 URL 可能不带pagesize(默认查第 1 页,每页 10 条)

@RequestMapping("/article")
@ResponseBody
public String getArticles(  //下面方法形参名与路径中的key相同,所以省略value
    @RequestParam(required = false) String category, // 非必填:文章分类
    @RequestParam(defaultValue = "1") Integer page,   // 默认值:第1页
    @RequestParam(defaultValue = "10") Integer size   // 默认值:每页10条
) {
    return "分类:" + (category == null ? "全部" : category) + 
           ",第" + page + "页,每页" + size + "条";
}
  • 若请求/article:输出 “分类:全部,第 1 页,每页 10 条”;
  • 若请求/article?category=tech&page=2:输出 “分类:tech,第 2 页,每页 10 条”。

2.4 @RequestBody

   @RequestBody用于读取 HTTP 请求体中的数据(如 JSON、XML),并自动将其转换为 Java 对象

实战演示

假设创建用户时,前端发送 POST 请求,请求体为 JSON:

// 请求体
{
  "name": "张三",
  "age": 25,
  "email": "zhangsan@example.com"
}

首先定义对应的 Java 实体类(需有 getter/setter 或用 Lombok 简化):

// User实体类
public class User {
    private String name;
    private Integer age;
    private String email;
    
    // 必须有默认无参构造器(Spring反射需要)
    public User() {}
    
    // getter和setter
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    // 其他字段的getter/setter省略
}

然后在控制器中用@RequestBody接收:

@RequestMapping(value = "/user", method = RequestMethod.POST)
@ResponseBody
public String addUser(@RequestBody User user) {
    return "新增用户成功:" + user.getName() + ",邮箱:" + user.getEmail();
    // 输出:新增用户成功:张三,邮箱:zhangsan@example.com
}

3.对象的属性拷贝(BeanUtils)

       在 Java 开发中,BeanUtils 是用于对象属性拷贝的工具类,能快速将一个对象的属性值复制到另一个对象中,避免手动编写大量 getter/setter 代码。 

  • 注意:只有当源对象和目标对象的属性名完全相同时,才能自动拷贝。

        在苍穹外卖课程中,我们使用了BeanUtils中的copyProperties方法实现了将employeeDTO中的属性浅拷贝到employee中。

//使用工具类进行属性拷贝
BeanUtils.copyProperties(employeeDTO, employee);

4.pagehelper插件实现分页查询

        PageHelper 是一款基于 MyBatis 的分页插件,在 Java 开发中广泛用于实现数据库查询结果的分页功能。它极大地简化了分页操作的代码编写,提高了开发效率。只需在执行查询前调用 PageHelper.startPage(pageNum, pageSize) 方法,即可轻松实现分页,无需手动编写复杂的 SQL 分页语句。例如:

    public PageResult page(EmployeePageQueryDTO employeePageQueryDTO)
    {
        /*
         设置分页查询的参数
         startPage方法
            参数一:pageNum - 页码
            参数二:pageSize - 每页显示数
         */
        //使用pagehelper
        PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
        //查询所有数据,返回值为Page<Employee>,这里的Page就是一个List集合,pagehelper对其进行封装,方便调用下面的两个方法
        Page<Employee> page = employeeMapper.page(employeePageQueryDTO);

        //对page进行处理,成为PageResult
        PageResult pageResult = new PageResult();
        pageResult.setTotal(page.getTotal());
        pageResult.setRecords(page.getResult());
        return pageResult;
    }

那么这时我们在mapper层就可以写:

    <!--员工分页查询
        id:mapper层中的方法名
        resultType:该方法的返回结果。这里的返回结果是Page<Employee>,实际返回的是里面的泛型Employee
    -->
    <select id="page" resultType="com.sky.entity.Employee">
        SELECT * from employee
        <where>
            <if test="name != null and name != ''" >
                and name like concat('%',#{name},'%')
            </if>
        </where>
        order by create_time desc
    </select>

5.ThreadLocal

        我们在添加员工时需要当前登陆人的id,这个我们可以从jwt令牌校验类中获取,但如何才能传递到Service层呢?

        这时我们就需要ThreadLocal这个工具类,它的核心作用是为每个线程提供一个独立的空间,而初始代码中对这个工具类进行了包装,也就是BaseContext:

package com.sky.context;

/**
 * 基础上下文工具类,用于在当前线程中存储和获取Long类型的标识(通常用于存储当前登录用户ID等线程私有信息)
 * 基于ThreadLocal实现,确保多线程环境下数据隔离,每个线程只能访问自己的标识
 */
public class BaseContext {

    /**
     * ThreadLocal实例,用于存储当前线程的Long类型标识(如用户ID)
     * ThreadLocal为每个线程提供独立的变量副本,实现线程间数据隔离
     */
    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    /**
     * 向当前线程的ThreadLocal中设置Long类型标识
     * 通常在请求入口处(如拦截器)调用,存储当前操作的关联标识(如登录用户ID)
     * @param id 要存储的Long类型标识(如用户ID)
     */
    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    /**
     * 从当前线程的ThreadLocal中获取存储的Long类型标识
     * 用于在当前线程的任意业务节点获取之前设置的标识(如在Service层获取当前登录用户ID)
     * @return 当前线程中存储的Long类型标识,若未设置则返回null
     */
    public static Long getCurrentId() {
        return threadLocal.get();
    }

    /**
     * 移除当前线程的ThreadLocal中存储的标识
     * 必须在线程任务结束时(如请求处理完成后)调用,防止ThreadLocal内存泄漏
     * 尤其在线程池环境下,线程复用可能导致数据残留,此方法可确保线程清理干净
     */
    public static void removeCurrentId() {
        threadLocal.remove();
    }

}

我们只需要在jwt令牌校验类中将当前登陆人的id存到线程空间里:

//2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            BaseContext.setCurrentId(empId); //将id存入线程空间
            log.info("当前员工id:", empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }

再在Service中调用即可:

​
        //补全属性
        employee.setStatus(StatusConstant.ENABLE);
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
        employee.setUpdateTime(LocalDateTime.now());
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateUser(BaseContext.getCurrentId()); //从线程空间中去除id
        employee.setCreateUser(BaseContext.getCurrentId()); //从线程空间中去除id

​

6.格式化日期

        在javaweb中我们了解了@DateTimeFormat,在外卖课程中又出现了@JsonFormat,所以对这两个进行一个总结。

  @DateTimeFormat 和 @JsonFormat 都是 Java 中用于处理日期时间格式的注解,但它们的所属框架、作用场景和处理时机完全不同。理解两者的区别是避免日期转换错误的关键,下面详细解析:

维度 @DateTimeFormat @JsonFormat
所属框架

Spring 框架(org.springframework.format.annotation

Jackson 库(com.fasterxml.jackson.annotatio
核心作用 请求参数(如表单、URL 查询参数)的字符串转换为 Java 日期对象 JSON 数据与 Java 日期对象相互转换(序列化 / 反序列化)
作用时机 请求参数绑定阶段(前端 → 后端,非 JSON 格式的参数) JSON 处理阶段(前端 JSON ↔ 后端 Java 对象)
典型使用场景 接收表单提交的日期字符串、URL 查询参数中的日期 接收前端 JSON 中的日期字符串,或响应 JSON 时格式化日期

@DateTimeFormat

   @DateTimeFormat 是 Spring 提供的注解,专门用于将前端传递的字符串类型日期参数(如表单提交、URL 查询参数)转换为 Java 日期对象(如 LocalDateLocalDateTimeDate 等)。

它只作用于非 JSON 格式的参数绑定,常见于:

  • @RequestParam 接收的 URL 查询参数;
  • @ModelAttribute 接收的表单数据;
  • @PathVariable 接收的路径参数(较少见,因路径参数通常不包含复杂日期)。

用法示例:接收 URL 查询参数中的日期

前端请求:GET /user?birthday=1990-03-15&createTime=2024-09-27 15:30:45

后端处理:

import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import java.time.LocalDateTime;

@RestController
public class UserController {

    @GetMapping("/user")
    public String getUser(
        // 绑定 birthday 参数(LocalDate 类型,格式 yyyy-MM-dd)
        @RequestParam 
        @DateTimeFormat(pattern = "yyyy-MM-dd") 
        LocalDate birthday,

        // 绑定 createTime 参数(LocalDateTime 类型,格式 yyyy-MM-dd HH:mm:ss)
        @RequestParam 
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") 
        LocalDateTime createTime
    ) {
        return "生日:" + birthday + ",创建时间:" + createTime;
    }
}
  • 关键pattern 必须与前端传递的字符串格式完全一致(如前端传 "1990/03/15"pattern 需设为 "yyyy/MM/dd"),否则会抛出参数转换异常。

 @JsonFormat

  • @JsonFormat 是 Jackson 库(JSON 处理库)提供的注解,用于控制 Java 日期对象与 JSON 字符串之间的相互转换

  • 序列化:Java 日期对象 → JSON 字符串(后端响应给前端时);
  • 反序列化:JSON 字符串 → Java 日期对象(前端发送 JSON 请求体给后端时,配合 @RequestBody)。

用法示例:处理 JSON 中的日期

前端发送 JSON 请求体(POST /order):

{
  "orderNo": "NO20240927",
  "createDate": "2024-09-27",
  "payTime": "2024-09-27 16:45:30"
}

后端处理:

import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import java.time.LocalDateTime;

@RestController
public class OrderController {

    @PostMapping("/order")
    public String createOrder(@RequestBody Order order) {
        return "订单:" + order.getOrderNo() + 
               ",创建日期:" + order.getCreateDate() + 
               ",支付时间:" + order.getPayTime();
    }

    // 订单实体类
    static class Order {
        private String orderNo;

        // JSON 反序列化/序列化时,按 yyyy-MM-dd 格式处理
        @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
        private LocalDate createDate;

        // JSON 反序列化/序列化时,按 yyyy-MM-dd HH:mm:ss 格式处理,指定东八区
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
        private LocalDateTime payTime;

        // getter/setter 省略
    }
}
  • pattern 控制 JSON 字符串的格式;
  • timezone 必须指定(如 GMT+8 或 Asia/Shanghai),否则会因时区偏移导致日期时间错误(默认 UTC 时区,比东八区晚 8 小时)。

7.全局异常处理器

7.1 概念

       全局异常处理器是基于 Spring AOP(面向切面编程)实现的 “集中式异常处理组件”,它能统一捕获项目中所有控制器抛出的异常,替代分散在各接口中的try-catch。通俗点来讲就是定义了一个异常捕获的类,在这个类里面捕捉代码中出现的异常并指定返回到前端的结果

7.2 注释解析(实现流程)

        实现全局异常处理器依赖 Spring 的两个核心注解:@RestControllerAdvice@ExceptionHandler

1. @RestControllerAdvice

  • 本质@ControllerAdvice + @ResponseBody 的组合注解;
  • 作用 1(@ControllerAdvice)声明这是一个 “控制器增强类”,能够扫描并监听项目中所有标注了@Controller@RestController的组件;
  • 作用 2(@ResponseBody):自动将方法返回值转换为 JSON 格式,这个我们在学web的时候有讲到。

2. @ExceptionHandler

  • 作用:指定当前方法处理哪种类型的异常;
  • 参数:需传入异常类的class对象(如@ExceptionHandler(BaseException.class)),表示该方法仅处理此类异常及其子类;

7.3 苍穹外卖实现案例

      教程中我们主要利用全局异常处理器处理了新增员工模块的重复添加问题

在没有完善全局异常处理器之前,我们重复添加员工会出现以下问题

我们在GlobalExceptionHandler类中可以编写以下代码

这样我们就可以在前端看到重复添加员工的提示信息

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐