当前位置: 首页 > news >正文

多用户跨学科交流系统(4)参数校验+分页搜索全流程的实现

目录

  • 🌸参数校验
    • 1. Comment 参数校验
    • 2. Post模块参数校验
    • 3. User模块参数校验
    • 4. 全局异常处理补充参数校验异常
  • 🌸JWT登录保护体系修改
  • 🌸分页+搜索
    • 1. 分页DTO
    • 2. Mapper接口
    • 3. Mapper XML
    • 4. Service层
    • 5. Controller层(带 @RequestParam)
  • 🌸面试问题模拟(分页+搜索+MyBatis深挖)
    • 一、分页与性能
    • 二、搜索逻辑与 SQL 优化
    • 三、MyBatis 深度题
    • 四、Service / Controller 设计
    • 五、项目实际场景

本篇博客基于之前三篇继续做模块完善,适用于初学者。

🍿摘要:

  1. 本文介绍了Spring Boot项目中参数校验与JWT登录保护的实现方案。
  2. 参数校验通过在实体类和DTO上添加注解(如@NotBlank、@NotNull),配合@Valid注解和全局异常处理实现统一校验。
  3. 特别强调Service层应专注业务逻辑,避免参数校验代码。
  4. 针对JWT登录保护,优化了Security配置和Filter实现,解决原方案中的跨域、Token解析等问题,实现了基于Token的身份认证机制。主要包括:放行登录注册接口、自动过滤OPTIONS请求、解析Bearer Token并验证有效性、将认证信息存入SecurityContext等核心功能。

🌸参数校验

总体原则:

  1. 校验卸载实体类 or DTO上
  2. Controller 参数前加 @Valid
  3. 异常由全局异常处理统一输出
  4. Service层不写 if 去判断空,只专注业务

加依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>

1. Comment 参数校验

  1. Comment 实体类
package com.example.blog.entity;import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDateTime;@Datapublic class Comment {private Long id;@NotNull(message = "post_id 不能为空")private Long post_id;@NotNull(message = "user_id 不能为空")private Long user_id;private Long parent_id;@NotBlank(message = "评论内不能为空")private String content;private LocalDateTime created_at;
}
  1. CommentController加@Valid
@PostMappingpublic Result<String> addComment(@Valid @RequestBody Comment comment){commentService.addComment(comment);return Result.success("评论发布成功");}

2. Post模块参数校验

  1. Post实体类加校验

@Data
public class Post {private Long id;@NotBlank(message = "标题不能为空")private String title;@NotBlank(message = "内容不能为空")private String content;private String status;@NotNull(message = "topicId 不能为空")private Long topicId;@NotNull(message = "userId 不能为空")private Long userId;private LocalDateTime created_at;private LocalDateTime updated_at;
}
  1. PostController中的addPost函数里的参数前面加@Valid

3. User模块参数校验

不要直接用User实体类做校验。 因为User里有太多字段,比如role、加密密码、id,这些不该让前端传。
所以我们写两个DTO。

DTO = Data Transfer Object
(专门用于 Controller 接收参数的类)

比如注册接口:

  • 只需要 username 和 password
  • 你就写一个只包含这两个字段的类

这样更安全、更干净、更好维护。

  1. RegisterDTO
package com.example.blog.dto;import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;@Data
public class RegisterDTO {@NotBlank(message = "用户名不能为空")private String username;@NotBlank(message = "密码不能为空")@Size(min = 4, max = 16)private String password;
}
  1. LoginDTO
package com.example.blog.dto;import jakarta.validation.constraints.NotBlank;
import lombok.Data;@Data
public class LoginDTO {@NotBlank(message = "用户名不能为空")private String username;@NotBlank(message = "密码不能为空")private String password;
}
  1. UserController
    @Valid
@PostMapping("/register")public Result<?> register(@Valid @RequestBody RegisterDTO dto) {userService.register(dto);return Result.success("注册成功");}// 💡 登录接口@PostMapping("/login")public Result<?> login(@Valid @RequestBody LoginDTO dto) {User dbUser = userService.login(dto.getUsername(), dto.getPassword());// 登录成功,签发TokenString token = JwtUtil.generateToken(dbUser.getUsername());return Result.success("token");}
  1. UserService(配合DTO)
  • UserService.register() 里面的逻辑完全不用管 “密码是否为空”“用户名是否太短”这类低级问题。

  • 所有脏东西在 Controller 直接被拦掉了 ✨

仅修改以下部分:

public void register(RegisterDTO dto) {//用户名重复if(userMapper.findByUsername(dto.getUsername())!=null) {throw new BusinessException(1001,"用户名已存在");}User user = new User();user.setUsername(dto.getUsername());user.setPassword(encoder.encode(dto.getPassword()));user.setRole("user");int rows = userMapper.insert(user);if(rows!=1) {throw new BusinessException(1002,"注册失败,请稍后再试");}}

4. 全局异常处理补充参数校验异常

校验失败 → 自动抛出 MethodArgumentNotValidException

GlobalExceptionHandler 加:

@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidation(MethodArgumentNotValidException e) {String msg = e.getBindingResult().getFieldError().getDefaultMessage();return Result.error(400, msg);
}

现在所有校验错误都会自动返回统一格式。

🌸JWT登录保护体系修改

修改目标:

  1. 让用户在登录后拿着 Token 才能访问文章、评论等接口,实现基础的身份认证机制。
  2. 登录、注册接口放行,其他所有接口全部需要 Token。SecurityFilterChain 调整为符合 Spring Security 最新写法。
  3. 修复JwtFilter的核心问题

原来的JwtFilter 存在以下问题:

  1. 没有交给 Spring 管理
  2. 不能处理 Bearer token 格式
  3. 不支持跨域 OPTIONS 请求(会导致前端 403)
  4. 多次解析 token、性能低
  5. 直接输出字符串响应,不规范
  6. 没有异常捕获(token 过期会报 500)
  1. 实现了一个真正可用的 JWT 校验流程

功能包括:

  1. 放行 /login、/register
  2. 自动过滤 OPTIONS
  3. 从 Authorization 中读取 Bearer token
  4. 解析 token,验证合法性和有效期
  5. 将认证信息写入 Spring SecurityContext
  6. 从而实现 Spring Security 能识别当前用户身份。
  1. 代码修改:
  • 删除config包下的FilterConfig
  • 进行以下修改:
// config/SecurityConfigpackage com.example.blog.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.example.blog.filter.JwtFilter;@Configuration
public class SecurityConfig {@Autowiredprivate JwtFilter jwtFilter;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.csrf(csrf -> csrf.disable()).authorizeHttpRequests(auth -> auth.requestMatchers("/user/register", "/user/login").permitAll().anyRequest().authenticated()).addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class).formLogin(form -> form.disable()).httpBasic(basic -> basic.disable());return http.build();}
}
// filter/JwtFilterpackage com.example.blog.filter;import com.example.blog.utils.JwtUtil;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;import java.io.IOException;
import java.util.List;@Component
public class JwtFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {HttpServletRequest req = (HttpServletRequest) request;HttpServletResponse res = (HttpServletResponse) response;// 放行 OPTIONS(跨域预检)if ("OPTIONS".equalsIgnoreCase(req.getMethod())) {chain.doFilter(request, response);return;}String path = req.getRequestURI();// 登录 & 注册放行if (path.contains("/login") || path.contains("/register")) {chain.doFilter(request, response);return;}// 从 header 取 tokenString authHeader = req.getHeader("Authorization");if (authHeader == null || !authHeader.startsWith("Bearer ")) {res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);res.getWriter().write("Missing or invalid Authorization header");return;}String token = authHeader.substring(7); // 去掉 "Bearer "String username;try {username = JwtUtil.parseToken(token);} catch (Exception e) {res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);res.getWriter().write("Token invalid or expired");return;}// 设置用户身份(告诉 Spring Security 当前是谁)UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(username, null,List.of(new SimpleGrantedAuthority("ROLE_USER")));SecurityContextHolder.getContext().setAuthentication(authentication);chain.doFilter(request, response);}
}
  • 现在除了注册登录接口,其他接口测试带token的格式为:
Bearer(空格)"token"

以删除评论接口为例
在这里插入图片描述


🌸分页+搜索

流程:

Controller ——接收分页参数、关键字

Service ——计算 offset、调用 Mapper

Mapper ——执行 SQL(search + count)

数据库 ——返回数据列表 + 总条数

Service ——组装 PageResult

Controller ——返回给前端 / Apifox

为啥分页?
如果你一次性查出来500条数据返回给前端:
数据库压力大、网络传输慢、前端渲染卡顿!
如果分页:
第一页:10条
第2页:10条
……

1. 分页DTO

  1. 将之前entity下的Result类也移到dto下。
  2. 新建类
// dto/PageResult
package com.example.blog.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;@Data
@AllArgsConstructor
public class PageResult<T> {private List<T> content;       // 当前页数据private long totalElements;    // 总条数private int totalPages;        // 总页数
}

如果你只写了 @Data 而没有 @AllArgsConstructor,Lombok 默认只生成无参构造器和 getter/setter。

2. Mapper接口

PostMapper中新增:

 // 分页+搜索List<Post> searchPosts(@Param("keyword") String keyword,@Param("offset") int offset,@Param("size") int size);int countSearchPosts(@Param("keyword") String keyword);

多参数必须要用 @Param

3. Mapper XML

src/main/resources/Mapper/PostMapper.xml 中新增

	<select id="searchPosts" resultType="Post">SELECT * FROM postWHERE title LIKE CONCAT('%', #{keyword}, '%')OR content LIKE CONCAT('%', #{keyword}, '%')LIMIT #{size} OFFSET #{offset}</select><select id="countSearchPosts" resultType="int">SELECT COUNT(*) FROM postWHERE title LIKE CONCAT('%', #{keyword}, '%')OR content LIKE CONCAT('%', #{keyword}, '%')</select>

SQL占位符必须与 Param 名称一致。

  • LIMIT + OFFSET 是啥?
    1. 它是 MySQL(和 PostgreSQL)里用来做“取第几页数据”的语法。
      LIMIT = 每页多少条
      OFFSET = 跳过多少条
    2. 另一个写法(等价):LIMIT offset, size
    3. 想要分页,必须加!

4. Service层

  1. PostService 接口中新增:
    PageResult<Post> searchPosts(String keyword, int page, int size);
  1. PostServiceImpl中新增:
 @Overridepublic PageResult<Post> searchPosts(String keyword, int page, int size) {int offset = (page-1* size;List<Post> content = postMapper.searchPosts(keyword, offset, size);int total = postMapper.countSearchPosts(keyword);int totalPages = (total + size - 1) / size; // 向上取整return new PageResult<>(content, total, totalPages);}

注意:计算offset时,前端page从1开始

5. Controller层(带 @RequestParam)

@GetMapping("/posts/search")public PageResult<Post> searchPosts(@RequestParam(defaultValue = "") String keyword,@RequestParam(defaultValue = "0") int page,@RequestParam(defaultValue = "10") int size) {return postService.searchPosts(keyword, page, size);}
  • @Param 和 @RequestParam 完全不同
注解用途
@ParamMyBatis 注解 —— 给 SQL 参数命名
@RequestParamSpring MVC 注解 —— 接收 URL 里的 query 参数

🌸面试问题模拟(分页+搜索+MyBatis深挖)

一、分页与性能

1、如果数据量非常大(百万级),LIMIT + OFFSET 会发生什么性能问题?有没有证明?如何优化?

考察点:数据库内部执行机制

思考方向:

  1. OFFSET 会扫描前 N 行再丢掉 → 本质是跳页,并不是“直接从中间开始查”

  2. explain 可以看到 “Using where; Using filesort” 或 “Using index”

  3. 优化方式:
    基于索引的游标分页(id > last_id)
    覆盖索引 + limit
    反向分页(倒序)
    或者 ES(分词搜索)

2、如果分页时 page = -1 或 size 特别大,你的接口如何防止被恶意调用导致数据库被打爆?

考察点:接口防护思维

思考方向:

  • 参数合法性校验
  • size 上限(例如最大 100)
  • 限流(网关或接口级)

二、搜索逻辑与 SQL 优化

3、LIKE ‘%keyword%’ 为什么非常慢?如何分析是否走索引?

考察点:SQL 优化、索引规则

思考方向:

  • 前置 % 会导致索引失效
  • explain 看 type 是否为 ALL(全表扫描)
  • 优化方案:
    • 使用全文索引(MySQL FULLTEXT)
    • 分词搜索(ES / Sphinx)
    • 业务端分词后多词匹配

4、如果 keyword 为空,你的 SQL 是 WHERE title LIKE ‘%%’,这种写法在大表下有什么隐患?

思考:

  • 会导致全表扫描

  • 若表很大,会成为慢查询

  • 可以使用动态 SQL:keyword 空时不加 WHERE,走普通分页

5、WHERE title LIKE CONCAT(‘%’, #{kw}, ‘%’) OR content LIKE … 这种写法有什么隐患?

考察点:
OR 会严重影响性能
索引失效
可能会触发 filesort、temporary

优化方向:

  • 分开两个 LIKE 查询结果 union

  • 或者使用两列全文索引

三、MyBatis 深度题

6、你写过多参数传递导致 TypeException,能说说 MyBatis 多参数到底是怎么封装的吗?

考察点:源码理解(实习面很加分)

思考方向:

  • 多参数时,MyBatis 使用 ParamNameResolver

  • 最终封成一个 Map

    • param1, param2…

    • 或 @Param 指定名字

  • XML 里用 #{keyword} 时,需要对上 Map 的 key

  • 如果 XML 里写 #{keyword} 但 Map 只有 param1 → 报错

7、为什么单参数时 #{keyword} 可以用,但多个参数时就不行?

考察点:细节理解

思考:

  • 单参数时 MyBatis 会自动把参数名替换为 param1 和原名

  • 多参数时没有真实参数名,只用 param1/param2

8、在 MyBatis 里,#{keyword} 和 ${keyword} 有什么底层差别?分别适合什么场景?

考察点:SQL 注入 + 预编译机制

思路:

  • #{} → 预编译 → 安全

  • ${} → 字符串拼接 → 有风险

  • 我们这里 LIKE 拼接必须用 #{},不能 ${},否则可能注入

9、如果你现在需要动态拼接搜索条件(标题、内容、作者都可选),你如何写 MyBatis XML?写法能否兼容多个条件?

考察点:动态 SQL 的熟练度

思路:

  • <where><if><trim>

  • 重点:自动去掉多余 AND

10、Mapper 层写 LIMIT #{size} OFFSET #{offset} 和 LIMIT #{offset}, #{size} 有啥区别?哪种更标准?

考察点:细节

方向:

  • MySQL 支持两种,标准语法是:LIMIT offset, size

  • PostgreSQL 是 LIMIT size OFFSET offset

  • 你的写法跟数据库兼容性相关

四、Service / Controller 设计

11、分页 offset 在 Controller 算还是 Service 算?为什么?

考察点:分层思想

思路:

  • 业务逻辑不应写在 Controller

  • Controller 就是参数解析 + 调用 Service

  • Service 计算 offset 更合理

  • Mapper 尽量只承接简单 SQL,不做业务转换

12、搜索接口如何设计才能让前端分页时既能展示总条数,又不影响效率?

考察点:实际业务经验

方向:

  • 查询记录

  • 查询 count

  • 有必要拆成两个查询,不要 count + data 一起查

  • count 单独查更快

13、你设计 PageResult 这个类时考虑了哪些字段?为什么这样设计?

考察点:对象封装习惯

方向:

  • content(返回列表)

  • totalElements(总数)

  • totalPages

  • currentPage

  • size

  • 是否有下一页

五、项目实际场景

14、如果搜索关键字很长(比如 200 字),你的 LIKE 会怎么样?如何限制?

考点:安全 + 性能

方向:

  • 长 LIKE 会导致 Range Scan 放大

  • 甚至拖垮索引缓存

  • 限制最大长度(例如 50)

  • 过长直接返回空或报错

15、怎么缓存分页结果?会有什么坑?你会缓存吗?

考点:缓存策略理解

方向:

  • 缓存分页结果困难:数据更新时所有页缓存都失效

  • 多维度(page + keyword)导致 key 巨多

  • 不推荐对动态搜索分页使用缓存

  • 可缓存热门搜索词

16、如果前端频繁调你的搜索接口(用户疯狂输入字符),你如何防止数据库被压垮?

考点:防抖 + 限流

方向:

  • 前端输入框防抖(300ms)

  • 后端限流:Redis + Token Bucket

  • 热词缓存

  • 降级策略(关键字为空直接不查)

http://www.dtcms.com/a/619256.html

相关文章:

  • 温州市网站制作免费下载app软件官网
  • 联系人网站设计江西省住房建设部官方网站
  • 硬件架构的异构化和硬件指令集生态多样化的必然性以及Wintel商业联盟对科技进步的阻碍
  • 做美食软件视频网站有哪些为什么要做营销型的网站建设
  • 网站做自适应广告主广告商对接平台
  • 网站建设摊销年限上海企业自助建站
  • 新乡网站建设加盟电话重庆有哪些建设公司
  • 网站建设外包工作室自己电脑做网站空间
  • 用Coze打造智能文档整理助手:从创建到发布的完整指南
  • Linux命令大全-chmod命令
  • 网站别名制作app的公司有哪些
  • 上海网站建设-网建知识网站专题设计模板
  • 网站设计谈判东莞发布最新通告
  • Avalonia+ReactiveUI+SourceGenerators带返回值的异步命令实现
  • C++中自增自减运算符的重载
  • 用vs2010做网站应用程序脱机模板建站oem代理
  • 做得好的网站手机怎么进入国外网站
  • 网站建设视频vs南通做网站多少钱
  • 学院网站设计说明书网络营销的职能是什么
  • 营口化工网站建设支持wordpress的mysql
  • 网站科技感颜色城市焦点商城网站建设案例
  • Android 开发架构
  • 深入理解Ansible条件语句:从基础到高级应用
  • 怎样做海外淘宝网站地方生活门户网站有哪些
  • 学习周报二十二
  • 软件第三方检测机构选择的五大关键问题
  • 整站网站优化运营加强学院网站建设
  • 网站开发项目报告书商标设计网软件
  • 【电工】网线(T568B线序)的制作
  • 香蕉叶子病害分类数据集898张4类别