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

06-01 1072阅读

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 "";
                });
    }
}

成功请求

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

配合黑名单

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

接口限制

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

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

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码