Spring Boot 无缝接入 DeepSeek 和通义千问请求日志记录及其ip黑白名单
SpringBoot版本 3.2.0 JDK 版本为17 redis 3.2.0 mybatis 3.0.3
依赖引入
关键依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
完整依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.cqie</groupId>
<artifactId>spring-ai</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-ai</name>
<description>spring-ai</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>3.2.0</spring-boot.version>
<spring-ai.version>0.8.1</spring-ai.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- JUnit (for testing) -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.40</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.17</version>
</dependency>
<!-- 添加官方Spring AI OpenAI依赖 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<!-- Spring Boot Maven Plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.cqie.SpringAiApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<!-- Spring Milestones Repository -->
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<!-- Spring Snapshots Repository -->
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
</project>
建表(日志+黑名单)
CREATE TABLE `request_log` (
`id` varchar(100) NOT NULL COMMENT '主键',
`date` datetime DEFAULT NULL COMMENT '请求时间',
`request_url` varchar(255) DEFAULT NULL COMMENT '请求路径',
`user_agent` varchar(255) DEFAULT NULL COMMENT 'userAgent',
`status` int(11) DEFAULT NULL COMMENT '状态码',
`ip_address` varchar(255) DEFAULT NULL COMMENT 'ip地址',
`method` varchar(100) DEFAULT NULL COMMENT '方法',
`error_message` varchar(255) DEFAULT NULL COMMENT '错误原因',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `black_ips` (
`id` varchar(100) NOT NULL COMMENT '主键id',
`black_ip` varchar(255) DEFAULT NULL COMMENT 'ip地址',
`status` tinyint(1) DEFAULT NULL COMMENT '转态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
配置文件
# DeepSeek 配置,完全兼容openai配置
# spring:
# ai:
# openai:
# base-url: https://api.deepseek.com # DeepSeek的OpenAI式端点
# api-key: sk-xxxxxxxxx
# chat.options:
# model: deepseek-chat # 指定DeepSeek的模型名称
# 通义千问配置
spring:
ai:
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode # 通义千问
api-key: sk-xxxxxxxxxxx
chat.options:
model: qwen-plus
配置文件示例
server:
port: 8080
spring:
application:
name: spring-ai
ai:
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode
api-key: sk-***
chat.options:
model: qwen-plus
datasource:
url: jdbc:mysql://ip:3306/springai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: 用户名
password: 密码
driver-class-name: com.mysql.cj.jdbc.Driver
# Redis配置
data:
redis:
host: ip
port: 6379
password: 密码
database: 3
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
timeout: 10000ms
# 日志配置
logging:
level:
org.springframework.ai: DEBUG
# mybatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.cqie.entity
configuration:
map-underscore-to-camel-case: true
call-setters-on-nulls: true
jdbc-type-for-null: 'null'
全局异常捕获
统一返回
package com.cqie.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class Result {
private int code;
private String message;
private Object data;
public Result() {
this.code = 0;
this.message = "success";
this.data = null;
}
public static Result success(Object data, String message) {
Result result = new Result();
result.code = 200;
result.message = message;
result.data = data;
return result;
}
public static Result success(Object data) {
Result result = new Result();
result.code = 200;
result.message = "success";
result.data = data;
return result;
}
public static Result error(String errorMsg) {
Result result = new Result();
result.code = 500;
result.message = errorMsg;
result.data = null;
return result;
}
}
定义异常
package com.cqie.common;
/**
* 服务异常
*/
public class ServerException extends RuntimeException {
public ServerException(String message) {
super(message);
}
public ServerException(String message, Throwable cause) {
super(message, cause);
}
}
全局捕获
package com.cqie.config;
import com.cqie.common.CommonException;
import com.cqie.common.Result;
import com.cqie.common.ServerException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器
*/
@RestControllerAdvice
@Slf4j
public class GlobeExceptionHandler {
// 处理全局异常
@ExceptionHandler(CommonException.class)
public Result CommonException(Exception e) {
return Result.error(e.getMessage());
}
@ExceptionHandler(ServerException.class)
public Result ServerException(Exception e) {
return Result.error(e.getMessage());
}
}
基于interceptor的日志拦截器
package com.cqie.common;
import com.cqie.dao.BlackIpsDao;
import com.cqie.dao.RequestLogDao;
import com.cqie.entity.BlackIps;
import com.cqie.entity.RequestLog;
import com.cqie.utils.RedisUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* 请求检查,请求日志记录,黑名单处理
*/
@Component
@ConditionalOnBean(RequestLogDao.class)
public class LoggingInterceptor implements HandlerInterceptor {
private final RequestLogDao requestLogDao;
private final BlackIpsDao blackIpsDao;
private final RedisUtils redisUtils;
private final String BLACK_IPS_KEY = "black_ips:";
public LoggingInterceptor(RequestLogDao requestLogDao, BlackIpsDao blackIpsDao, RedisUtils redisUtils) {
this.requestLogDao = requestLogDao;
this.blackIpsDao = blackIpsDao;
this.redisUtils = redisUtils;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String ipAddress = getClientIpAddress(request);
int status = response.getStatus();
String errorMessage = null;
// 对黑名单的ip进行处理
List<String> blackIps = (List<String>) redisUtils.get(BLACK_IPS_KEY + "spring-ai");
if (blackIps == null) {
List<String> ipBlackList = blackIpsDao.queryByStatus(0).stream().map(BlackIps::getBlackIp).collect(Collectors.toList());
// 一天的过期时间
redisUtils.set(BLACK_IPS_KEY + "spring-ai", ipBlackList, 24);
}
if (blackIps != null && blackIps.contains(ipAddress)) {
status = 500;
errorMessage = "请求ip已被加入黑名单";
saveRequestLog(request, ipAddress, status, errorMessage);
throw new ServerException(errorMessage);
}
// 判断2s请求最多请求一次,对请求频率做限制
boolean exists = redisUtils.exists(ipAddress);
if (exists) {
status = 500;
errorMessage = "ai服务请求太频繁";
saveRequestLog(request, ipAddress, status, errorMessage);
throw new ServerException(errorMessage);
}
// 记录调用日志
saveRequestLog(request, ipAddress, status, null);
// 对请求记录分析 限制2s请求最多请求一次
redisUtils.set(ipAddress, "1", 5);
return HandlerInterceptor.super.preHandle(request, response, handler);
}
private void saveRequestLog(HttpServletRequest request, String ipAddress, int status, String errorMessage) {
LocalDateTime now = LocalDateTime.now();
String method = request.getMethod();
String url = request.getRequestURI();
String userAgent = request.getHeader("User-Agent");
RequestLog requestLog = new RequestLog();
requestLog.setDate(now);
requestLog.setRequestUrl(url);
requestLog.setStatus(status);
requestLog.setUserAgent(userAgent);
requestLog.setIpAddress(ipAddress);
requestLog.setMethod(method);
requestLog.setErrorMessage(errorMessage);
requestLogDao.insert(requestLog);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
private String getClientIpAddress(HttpServletRequest request) {
String ipAddress = request.getHeader("X-Forwarded-For");
if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
return ipAddress;
}
}
注册拦截器
package com.cqie.config;
import com.cqie.common.LoggingInterceptor;
import com.cqie.dao.BlackIpsDao;
import com.cqie.dao.RequestLogDao;
import com.cqie.utils.RedisUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final RequestLogDao requestLogDao;
private final BlackIpsDao blackIpsDao;
private final RedisUtils redisUtils;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//日志拦截器
registry.addInterceptor(new LoggingInterceptor(requestLogDao, blackIpsDao,redisUtils)).addPathPatterns("/**").order(0);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 静态资源访问路径和存放路径配置
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/", "classpath:/public/");
// 新增Camunda webjar资源映射
registry.addResourceHandler("/webjars/camunda/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/camunda-webapp-ui/");
// swagger访问配置
registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/", "classpath:/META-INF/resources/webjars/");
}
}
实现日志和日志表操作
dao server impl 这里只需要dao层就行
略
redis工具类简单封装
package com.cqie.utils;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtils {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 设置缓存
*
* @param key 键
* @param value 值
*/
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 设置缓存并设置过期时间
*
* @param key 键
* @param value 值
* @param timeout 过期时间(秒)
*/
public void set(String key, Object value, long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.HOURS);
}
/**
* 获取缓存
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 删除缓存
*
* @param key 键
*/
public void delete(String key) {
redisTemplate.delete(key);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 设置过期时间
*
* @param key 键
* @param timeout 过期时间(秒)
*/
public void expire(String key, long timeout) {
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 获取过期时间
*
* @param key 键
* @return 过期时间(秒)
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
public boolean exists(String key) {
return redisTemplate.hasKey(key);
}
}
接口实现
package com.cqie.controller;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.StreamingChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
/**
* 基于DeepSeek/通义千问的聊天控制器
*
* @author qingyuqiao
*/
@RestController
@RequestMapping("/api")
public class ChatController {
/**
* 上下文
*/
private final List<Message> contextHistoryList = new ArrayList<>();
private final ChatClient chatClient;
private final StreamingChatClient streamingChatClient;
/**
* ai 初始化信息
*
* @param chatClient
* @param streamingChatClient
*/
public ChatController(ChatClient chatClient, StreamingChatClient streamingChatClient) {
this.chatClient = chatClient;
this.streamingChatClient = streamingChatClient;
// 对用户输入进行增强
contextHistoryList.add(new SystemMessage("你是一个专业的it技术顾问。"));
}
/**
* 普通对话
*
* @param message 问题
* @return 回答结果
*/
@GetMapping("/chat")
public ChatResponse chat(@RequestParam String message) {
contextHistoryList.add(new UserMessage(message));
Prompt prompt = new Prompt(contextHistoryList);
ChatResponse chatResp = chatClient.call(prompt);
if (chatResp.getResult() != null) {
contextHistoryList.add(chatResp.getResult().getOutput());
}
return chatResp;
}
/**
* 流式返回
*
* @param message 问题
* @return 流式结果
*/
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestParam String message) {
contextHistoryList.add(new UserMessage(message));
Prompt prompt = new Prompt(contextHistoryList);
return streamingChatClient.stream(prompt)
.map(chatResponse -> {
if (chatResponse.getResult() != null) {
return chatResponse.getResult().getOutput().getContent();
}
return "";
});
}
}
成功请求

配合黑名单

接口限制
