目录

一、项目整体架构

二、数据模型设计

2.1 用户模型(User.java)

2.2 黑名单模型(BlackList.java)

2.3 接口耗时模型(FunTime.java)

三、数据访问层(DAO)

3.1 用户数据操作(UserDao.java)

3.2 拦截器相关数据操作(AopDao.java)

四、工具类实现

4.1 响应信息工具类(SendMsg.java)

4.2 用户代理识别工具类(UserAgentUtils.java)

五、控制器实现

六、拦截器核心实现

6.1拦截器配置(InterceptorConfig.java)

6.2 核心拦截器(CoreHandlerInterceptor.java)

七、Nginx 负载均衡配置

7.1多节点配置

八、全局异常处理

九、前端视图(lists.html)

十、测试环节与结果验证

10.1 拦截器核心功能测试

10.2 负载均衡测试

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表,存储用户基本信息:

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表,存储被禁止访问的用户信息:

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表,记录接口调用耗时:

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)

提供用户查询功能:

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)

提供黑名单查询和接口耗时记录功能:

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)

统一处理响应信息,确保编码一致:

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)

识别爬虫请求和鸿蒙终端:

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 处理用户相关请求,包括用户列表查询和登录:

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)

注册拦截器并配置拦截规则:

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)

实现请求拦截逻辑,包括前置检查、后置处理和完成后操作:

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 实现请求分发,配置如下:

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

启动前要选择允许多个实例,并分别启动两个节点:

八、全局异常处理

统一处理项目中抛出的异常,返回标准化的错误信息:

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 的用户列表展示页面:

<!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 多终端适配测试

模拟鸿蒙终端识别:

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐