部署一个自己的Spring Ai 服务(deepseek/通义千问)
Spring Boot 无缝接入 DeepSeek 和通义千问请求日志记录及其ip黑白名单
SpringBoot版本 3.2.0 JDK 版本为17 redis 3.2.0 mybatis 3.0.3
依赖引入
关键依赖
org.springframework.ai spring-ai-openai-spring-boot-starter
完整依赖
4.0.0 com.cqie spring-ai 0.0.1-SNAPSHOT spring-ai spring-ai 17 UTF-8 UTF-8 3.2.0 0.8.1 org.springframework.boot spring-boot-starter-parent 3.2.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test junit junit test org.apache.commons commons-lang3 io.swagger swagger-annotations 1.5.21 com.alibaba fastjson 2.0.40 org.projectlombok lombok true mysql mysql-connector-java 8.0.28 cn.hutool hutool-all 5.8.17 org.springframework.ai spring-ai-openai-spring-boot-starter org.mybatis.spring.boot mybatis-spring-boot-starter 3.0.3 org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.springframework.ai spring-ai-bom ${spring-ai.version} pom import org.apache.maven.plugins maven-compiler-plugin 3.8.1 ${java.version} ${java.version} ${project.build.sourceEncoding} org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} com.cqie.SpringAiApplication repackage repackage spring-milestones Spring Milestones https://repo.spring.io/milestone false spring-snapshots Spring Snapshots https://repo.spring.io/snapshot false
建表(日志+黑名单)
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 blackIps = (List) redisUtils.get(BLACK_IPS_KEY + "spring-ai"); if (blackIps == null) { List 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 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 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 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,只需要修改配置文件的模型和密匙!!!
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。