部署一个自己的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-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 "";
                });
    }
}

成功请求

配合黑名单

接口限制

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

相关推荐
purrrew16 分钟前
【Java EE初阶】多线程(二)
java·java-ee
左灯右行的爱情18 分钟前
Redis 缓存并发问题深度解析:击穿、雪崩与穿透防治指南
java·数据库·redis·后端·缓存
百锦再1 小时前
Android Studio开发中Application和Activity生命周期详解
android·java·ide·app·gradle·android studio·studio
大G哥1 小时前
Java 中的 Integer 缓存池:背后的性能优化机制解析
java·开发语言·缓存·性能优化
CN.LG1 小时前
IntelliJ IDEA 内存优化
java·ide·intellij-idea
笨蛋不要掉眼泪1 小时前
SpringMVC再复习1
java·spring·mvc
苹果酱05671 小时前
python3语言基础语法整理
java·vue.js·spring boot·mysql·课程设计
牛马baby1 小时前
Java高频面试之并发编程-11
java·开发语言·面试
Yharim1 小时前
两个客户端如何通过websocket通信
spring boot·后端·websocket
radient2 小时前
Java/Go双修 - Go并发Goroutine与Java对比
java·后端·go