目录
[2.1 用户模型(User.java)](#2.1 用户模型(User.java))
[2.2 黑名单模型(BlackList.java)](#2.2 黑名单模型(BlackList.java))
[2.3 接口耗时模型(FunTime.java)](#2.3 接口耗时模型(FunTime.java))
[3.1 用户数据操作(UserDao.java)](#3.1 用户数据操作(UserDao.java))
[3.2 拦截器相关数据操作(AopDao.java)](#3.2 拦截器相关数据操作(AopDao.java))
[4.1 响应信息工具类(SendMsg.java)](#4.1 响应信息工具类(SendMsg.java))
[4.2 用户代理识别工具类(UserAgentUtils.java)](#4.2 用户代理识别工具类(UserAgentUtils.java))
6.1拦截器配置(InterceptorConfig.java)
[6.2 核心拦截器(CoreHandlerInterceptor.java)](#6.2 核心拦截器(CoreHandlerInterceptor.java))
[七、Nginx 负载均衡配置](#七、Nginx 负载均衡配置)
[10.1 拦截器核心功能测试](#10.1 拦截器核心功能测试)
[10.2 负载均衡测试](#10.2 负载均衡测试)
[10.3 多终端适配测试](#10.3 多终端适配测试)
在微服务架构中,拦截器和负载均衡是两个核心组件。拦截器负责请求的预处理与后处理,实现权限控制、日志记录等横切关注点;负载均衡则实现请求的合理分发,提升系统可用性与吞吐量。本篇博客将结合实际项目,讲解如何在 SpringBoot 中实现自定义拦截器,并通过 Nginx 配置负载均衡。
一、项目整体架构
本项目基于 SpringBoot 2.7.1,主要实现以下功能:
- 自定义拦截器:实现访问时间控制、爬虫识别、黑名单校验
- 负载均衡:通过 Nginx 实现多节点请求分发
- 数据持久化:使用 MyBatis 操作 MySQL 数据库
- 全局异常处理:统一异常响应机制
项目结构如下:

核心依赖配置(pom.xml):
<dependencies> <!-- Spring Web核心依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.24</version> </dependency> <!-- Thymeleaf模板引擎 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies>
二、数据模型设计
2.1 用户模型(User.java)
对应数据库t_user表,存储用户基本信息:
java
package com.pp.interceptor.model;
public class User {
private int uid;
private String uname;
private String upwd;
// getter/setter
public int getUid() { return uid; }
public void setUid(int uid) { this.uid = uid; }
public String getUname() { return uname; }
public void setUname(String uname) { this.uname = uname; }
public String getUpwd() { return upwd; }
public void setUpwd(String upwd) { this.upwd = upwd; }
}
2.2 黑名单模型(BlackList.java)
对应数据库t_black表,存储被禁止访问的用户信息:
java
package com.pp.interceptor.model;
public class BlackList {
private int tid;
private String bname;
// getter/setter
public int getTid() { return tid; }
public void setTid(int tid) { this.tid = tid; }
public String getBname() { return bname; }
public void setBname(String bname) { this.bname = bname; }
}
2.3 接口耗时模型(FunTime.java)
对应数据库t_funtime表,记录接口调用耗时:
java
package com.pp.interceptor.model;
public class FunTime {
private int fid;
private String fname; // 接口标识(包含节点ID)
private long subtime; // 耗时(毫秒)
// getter/setter
public int getFid() { return fid; }
public void setFid(int fid) { this.fid = fid; }
public String getFname() { return fname; }
public void setFname(String fname) { this.fname = fname; }
public long getSubtime() { return subtime; }
public void setSubtime(long subtime) { this.subtime = subtime; }
}
三、数据访问层(DAO)
3.1 用户数据操作(UserDao.java)
提供用户查询功能:
java
package com.pp.interceptor.dao;
import com.pp.interceptor.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper // MyBatis映射接口标识
public interface UserDao {
@Select("select * from t_user")
public List<User> queryUsers();
}
3.2 拦截器相关数据操作(AopDao.java)
提供黑名单查询和接口耗时记录功能:
java
package com.pp.interceptor.dao;
import com.pp.interceptor.model.FunTime;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface AopDao {
// 查询用户是否在黑名单中
@Select("select count(*) from t_black where bname=#{name}")
public int queryBlackName(String name);
// 记录接口耗时
@Insert("insert into t_funtime(fname,subtime) values(#{fname},#{subtime})")
public void insertTime(FunTime ft);
}
四、工具类实现
4.1 响应信息工具类(SendMsg.java)
统一处理响应信息,确保编码一致:
java
package com.pp.interceptor.utils;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
@Component
public class SendMsg {
public void send(HttpServletResponse response, String content) {
try {
response.setCharacterEncoding("UTF-8"); // 设置响应编码为UTF-8
response.setContentType("text/plain;charset=UTF-8"); // 设置响应内容类型为纯文本
// 使用OutputStream避免与其他输出流冲突
OutputStream os = response.getOutputStream();
os.write(content.getBytes(StandardCharsets.UTF_8));
os.flush();
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.2 用户代理识别工具类(UserAgentUtils.java)
识别爬虫请求和鸿蒙终端:
java
package com.pp.interceptor.utils;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class UserAgentUtils {
// 爬虫关键词库(强化规则)
private static final String[] CRAWLER_KEYWORDS = {"python", "scrapy", "curl", "wget", "spider", "bot", "crawler", "scraper"};
// 鸿蒙终端标识
private static final String HARMONY_OS_FLAG = "HarmonyOS";
/**
* 判断是否为爬虫请求
*/
public boolean isCrawler(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
System.out.println("User-Agent: " + userAgent);
if (userAgent == null || userAgent.isEmpty()) {
return true; // 空UA判定为异常爬虫
}
String lowerUA = userAgent.toLowerCase();
for (String keyword : CRAWLER_KEYWORDS) {
if (lowerUA.contains(keyword)) {
return true;
}
}
return false;
}
/**
* 判断是否为鸿蒙终端请求
*/
public boolean isHarmonyOs(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
if (userAgent == null) {
return false;
}
return userAgent.contains(HARMONY_OS_FLAG);
}
}
五、控制器实现
UserController.java 处理用户相关请求,包括用户列表查询和登录:
java
package com.pp.interceptor.controller;
import com.pp.interceptor.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@Controller
@RequestMapping("/users")
public class UserController {
@Autowired
private UserDao userDao;
@Value("${node.id}")
private String nodeId; // 从配置文件获取节点标识
/**
* 查询用户列表
*/
@RequestMapping("/queryList")
public ModelAndView queryList(HttpServletRequest request) {
System.out.printf("[%s] 处理用户列表查询请求%n", nodeId);
ModelAndView mav = new ModelAndView();
List userList = userDao.queryUsers();
mav.setViewName("lists"); // 指定模板页面
mav.addObject("datas", userList); // 传递数据到视图
return mav;
}
/**
* 登录接口(不被拦截)
*/
@RequestMapping("/login")
public String login(HttpServletRequest request) {
System.out.printf("[%s] 处理登录请求%n", nodeId);
return "【" + nodeId + "】登录业务处理成功(负载均衡节点响应)";
}
}
关键说明:
- 基于
@Controller标识为 MVC 控制器,@RequestMapping("/users")定义基础路径 queryList方法查询用户列表并通过ModelAndView传递数据到lists.html视图,login方法直接返回字符串响应- 注入
nodeId用于标识当前服务节点(配合多端口部署实现负载均衡演示)
六、拦截器核心实现
6.1拦截器配置(InterceptorConfig.java)
注册拦截器并配置拦截规则:
java
package com.pp.interceptor.config;
import com.pp.interceptor.interceptor.CoreHandlerInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private CoreHandlerInterceptor coreHandlerInterceptor; // 这里用接口的好处:可以更换绑定的拦截器类,具体看@Component修饰哪个类(IOC)
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册核心拦截器,拦截所有请求,排除登录接口
registry.addInterceptor(coreHandlerInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/users/login",
"/static/**",
"/favicon.ico" // 新增:排除图标请求,因为浏览器会自动发送/favicon.ico请求(获取网站图标)
);
}
}
6.2 核心拦截器(CoreHandlerInterceptor.java)
实现请求拦截逻辑,包括前置检查、后置处理和完成后操作:
java
package com.pp.interceptor.interceptor;
import com.pp.interceptor.dao.AopDao;
import com.pp.interceptor.model.FunTime;
import com.pp.interceptor.model.User;
import com.pp.interceptor.utils.SendMsg;
import com.pp.interceptor.utils.UserAgentUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@Component // 通用组件标记,告诉Spring:这个类要被扫描并加入IOC容器
public class CoreHandlerInterceptor implements HandlerInterceptor {
@Autowired
private AopDao aopDao;
@Autowired
private SendMsg sendMsg;
@Autowired
private UserAgentUtils userAgentUtils;
// 从配置文件中读取节点ID
@Value("${node.id}") // 将配置文件中的值注入到Spring管理的Bean中
private String nodeId; // 节点标识
private long startTime;
/**
* 前置拦截:爬虫识别、黑名单、访问时间控制
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
startTime = System.currentTimeMillis();
String requestUri = request.getRequestURI();
System.out.printf("[%s] 前置拦截开始 - 请求地址:%s%n", nodeId, requestUri);
// 1. 访问时间控制(12-14点禁止访问)
int currentHour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
if (currentHour >= 12 && currentHour <= 14) {
sendMsg.send(response, "【" + nodeId + "】现在是休息时间(12-14点),系统暂不服务,请稍后访问");
return false;
}
// 2. 爬虫识别(强化规则)
if (userAgentUtils.isCrawler(request)) {
sendMsg.send(response, "【" + nodeId + "】检测到爬虫请求,禁止访问(接口防护)");
return false;
}
// 3. 黑名单校验
String uname = request.getParameter("name");
if (uname != null && !uname.isEmpty()) {
int count = aopDao.queryBlackName(uname);
if (count > 0) {
sendMsg.send(response, "【" + nodeId + "】您在黑名单中,请申请解除后访问");
return false;
}
}
// 鸿蒙终端适配提示
if (userAgentUtils.isHarmonyOs(request)) {
System.out.printf("[%s] 检测到鸿蒙终端请求,已适配响应规则%n", nodeId);
}
return true;
}
/**
* 后置拦截:数据处理、耗时日志记录
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
String requestUri = request.getRequestURI();
System.out.printf("[%s] 后置拦截开始 - 请求地址:%s%n", nodeId, requestUri);
// 增加非空判断,避免空指针异常
if (modelAndView != null) {
modelAndView.addObject("heads", "【" + nodeId + "】用户信息表(负载均衡节点返回)");
//建立在@Controller注解机制
Map<String, Object> maps = modelAndView.getModel();
List<User> lists = (List<User>) maps.get("datas");
//lists.remove(0)报错;
System.out.println(lists.size());
Iterator its = lists.iterator();
while (its.hasNext()) {
User u = (User) its.next();
System.out.println(u.getUname());
if (u.getUname().contains("海")) {
its.remove();
}
}
modelAndView.addObject("datas", lists);
}
}
/**
* 完成拦截:接口耗时日志记录(存入数据库)
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
String requestUri = request.getRequestURI();
// 记录接口耗时(关联节点标识)
FunTime funTime = new FunTime();
funTime.setFname(nodeId + "-" + requestUri);
funTime.setSubtime(costTime);
aopDao.insertTime(funTime);
// 异常日志打印
if (ex != null) {
System.err.printf("[%s] 请求异常 - 地址:%s,异常信息:%s%n", nodeId, requestUri, ex.getMessage());
} else {
System.out.printf("[%s] 请求完成 - 地址:%s,耗时:%dms%n", nodeId, requestUri, costTime);
}
}
}
七、Nginx 负载均衡配置
通过 Nginx 实现请求分发,配置如下:
java
http {
# 后端服务集群配置
upstream backend-servers {
server 192.168.247.1:8990 weight=2; # 权重2,接收更多请求
server 192.168.247.1:8900 weight=1; # 权重1
}
server {
listen 89; # Nginx监听端口
server_name localhost;
location / {
proxy_pass http://backend-servers; # 转发到后端集群
# 解决400错误的核心配置
proxy_set_header Host $proxy_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
负载均衡解析:
upstream:定义后端服务集群,weight表示权重(数值越大,分配到的请求越多)proxy_pass:将请求转发到backend-servers集群- 额外请求头配置:解决反向代理导致的请求信息丢失问题
7.1多节点配置
为实现负载均衡,需配置多个服务节点,通过不同端口区分:
application-8990.properties(节点 1):
# 节点1端口 server.port=8990 # 节点标识(用于日志区分) node.id=server-8990
application-8900.properties(节点 2):
# 节点2端口 server.port=8900 # 节点标识(用于日志区分) node.id=server-8900
启动前要选择允许多个实例,并分别启动两个节点:



八、全局异常处理
统一处理项目中抛出的异常,返回标准化的错误信息:
java
package com.pp.interceptor.config;
import com.pp.interceptor.utils.SendMsg;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
// 全局控制器通知注解 @ControllerAdvice + @ResponseBody
@RestControllerAdvice
public class GlobalExceptionHandler {
@Autowired
private SendMsg sendMsg;
@Value("${node.id}")
private String nodeId; // 从配置文件中读取节点标识
// 异常处理方法注解,声明此方法处理指定类型的异常
@ExceptionHandler(Exception.class) // Exception.class表示处理所有异常
public void handleGlobalException(HttpServletRequest request, HttpServletResponse response, Exception e) {
String errorMsg = String.format("[%s] 请求发生异常:%s,已触发容错机制,请稍后重试", nodeId, e.getMessage());
sendMsg.send(response, errorMsg);
// 打印异常栈(便于排查)
e.printStackTrace();
}
}
关键说明:
- 使用
@RestControllerAdvice实现全局异常拦截,无需在每个控制器中重复处理异常 - 通过
@ExceptionHandler(Exception.class)捕获所有异常,结合SendMsg工具类向客户端返回包含节点标识的错误信息,便于问题定位
九、前端视图(lists.html)
基于 Thymeleaf 的用户列表展示页面:
java
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div th:text="${heads}"></div> <!-- 显示节点标识标题 -->
<ul th:each="u : ${datas}"> <!-- 遍历用户列表 -->
<li th:text="${u.uname}"></li> <!-- 显示用户名 -->
</ul>
</body>
</html>
十、测试环节与结果验证
10.1 拦截器核心功能测试
(1)爬虫识别拦截


(2)黑名单拦截


(3)正常请求(非爬虫 / 黑名单,非 12-14 点)


10.2 负载均衡测试
(1)多节点分发
通过 Nginx(代理 8900/8990 节点)多次请求,响应交替显示 "【server-8900】登录业务处理成功" 和 "【server-8990】登录业务处理成功",但8990节点所占权重更大。


10.3 多终端适配测试
模拟鸿蒙终端识别:

