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

部署一个自己的Spring Ai 服务(deepseek/通义千问)

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-xxxxxxxxxxxchat.options:model: qwen-plus

配置文件示例

server:port: 8080
spring:application:name: spring-aiai:openai:base-url: https://dashscope.aliyuncs.com/compatible-modeapi-key: sk-***chat.options:model: qwen-plusdatasource:url: jdbc:mysql://ip:3306/springai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: 用户名password: 密码driver-class-name: com.mysql.cj.jdbc.Driver# Redis配置data:redis:host: ipport: 6379password: 密码database: 3lettuce:pool:max-active: 8max-wait: -1msmax-idle: 8min-idle: 0timeout: 10000ms
# 日志配置
logging:level:org.springframework.ai: DEBUG# mybatis配置
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.cqie.entityconfiguration:map-underscore-to-camel-case: truecall-setters-on-nulls: truejdbc-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;}@Overridepublic 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);}@Overridepublic 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;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//日志拦截器registry.addInterceptor(new LoggingInterceptor(requestLogDao, blackIpsDao,redisUtils)).addPathPatterns("/**").order(0);}@Overridepublic 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 {@Resourceprivate 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 "";});}
}

成功请求

配合黑名单

接口限制

后续可无缝接入deepseek,只需要修改配置文件的模型和密匙!!!

相关文章:

  • kotlin flatMap 变换函数的特点和使用场景
  • 亚远景-ASPICE认证:如何优化软件开发流程?
  • 极客天成受邀参加2050大会,共赴人工智能科技盛宴
  • IDEA新版本Local Changes
  • Java后端开发day39--方法引用
  • 【学习资源】知识图谱与大语言模型融合
  • 机器学习之五:基于解释的学习
  • Java语言使用GLM-4-Voice的交互示例
  • CSS Transition入门指南
  • 网络通讯【QTcpServer、QTcpSocket、QAbstractSocket】
  • 在 Windows 的终端安装并使用 azd 命令
  • CVE-2025-21756:Linux内核微小漏洞如何引发完整Root提权攻击(含PoC发布)
  • tornado_登录页面(案例)
  • 多地部署Gerrit Replication插件同步异常解决思路及方案(附脚本与CronJob部署)
  • 【大语言模型DeepSeek+ChatGPT+GIS+Python】AI大语言模型驱动的地质灾害全流程智能防治:风险评估、易发性分析与灾后重建多技术融合应用
  • Uniapp:设置TabBar
  • 从 Synchron 会议观察 Lustre/Scade 同步语言的演化 (1994 - 2024)
  • Ubuntu实现远程文件传输
  • Qt/C++开发监控GB28181系统/获取设备信息/设备配置参数/通道信息/设备状态
  • IOS 国际化词条 Python3 脚本
  • 韩国下届大选执政党初选4进2结果揭晓,金文洙、韩东勋胜出
  • 打造全域消费场景,上海大世界百个演艺娱乐新物种待孵化
  • 走访中广核风电基地:701台风机如何乘风化电,点亮3000万人绿色生活
  • 李公明|一周画记:哈佛打响第一枪
  • 一季度规模以上工业企业利润由降转增,国家统计局解读
  • 三大交易所修订股票上市规则:明确关键少数责任,强化中小股东保障