springboot3.2使用dashscope-sdk-java调用阿里千问大模型,流式回答

技术选型

spring-ai更新太快,api不统一,截止到目前为止,不适合企业开发

目前企业开发中,还是使用 dashscope-sdk-java 的 2.16.x 版本更多

依赖

排除 slf4j-simple 依赖,否则启动有警告或报错

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>dashscope-sdk-java</artifactId>
    <version>2.16.10</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
        </exclusion>
    </exclusions>
</dependency>

大模型配置

配置 api-key 和 模型名称

# DashScope AI配置
dashscope:
  api-key: your-api-key-here
  model: qwen-turbo

授权过滤

如果使用 流式回答,必须放开授权,否则会被拦截

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        // 禁用 CSRF 保护,因为我们使用 JWT 认证
        .csrf(AbstractHttpConfigurer::disable)
        // 配置请求授权
        .authorizeHttpRequests(authorize -> authorize
            // 允许登录接口匿名访问
                .requestMatchers("/api/users/login").permitAll()
                .requestMatchers("/app/login").permitAll()
                // 放行 ai 的url
                .requestMatchers("/api/ai/stream").permitAll()
            // 其他所有请求都需要认证
            .anyRequest().authenticated()
        )
        // 添加 JWT 过滤器
        .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build();
}

Controller实现

package com.smartagriculture.controller;

import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.common.Role;
import com.smartagriculture.utils.JwtUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.util.ArrayList;
import java.util.Collections;

/**
 * AI助手控制器
 */
@RestController
@RequestMapping("/api/ai")
public class AIController {

    private static final Logger logger = LoggerFactory.getLogger(AIController.class);
    private static final String PROMPT_SUFFIX = "不要用markdown格式";
    @Autowired
    private JwtUtil jwtUtil;
    @Value("${dashscope.api-key}")
    private String configApiKey;

    @Value("${dashscope.model}")
    private String model;

    @GetMapping("/stream")
    public SseEmitter streamAIResponse(@RequestParam String prompt,
                                       @RequestHeader(value = "Authorization", required = false) String tokenHeader) {
        logger.info("收到AI请求,prompt: {}", prompt);
        SseEmitter emitter = new SseEmitter();

        // ======================
        // 手动鉴权(安全核心)
        // ======================
        int check = check(tokenHeader);
        if (check == -1) {
            emitter.completeWithError(new RuntimeException("无权限访问"));
            return emitter;
        }else if (check == -2) {
            emitter.completeWithError(new RuntimeException("token 无效"));
            return emitter;
        }

        // 异步线程处理
        new Thread(() -> {
            try {
                GenerationParam param = GenerationParam.builder()
                        .apiKey(configApiKey)
                        .model(model)
                        .messages(Collections.singletonList(
                                Message.builder()
                                        .role(Role.USER.getValue())
                                        .content(prompt + PROMPT_SUFFIX)
                                        .build()
                        ))
                        .incrementalOutput(true)
                        .build();

                // 🔥 使用独立的回调类(无Lambda)
                Generation generation = new Generation();
                generation.streamCall(param, new AiStreamResultCallback(emitter));

            } catch (Exception e) {
                logger.error("AI接口调用失败", e);
                emitter.completeWithError(e);
            }
        }).start();

        return emitter;
    }

    private int check(String token){
        logger.debug("JWT Filter: Authorization header = " + token);

        // 检查 token 是否存在且格式正确
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7); // 去除 "Bearer " 前缀
            logger.debug("JWT Filter: Token = " + token);

            // 验证 token
            if (jwtUtil.validateToken(token)) {
                // 从 token 中获取用户名
                String username = jwtUtil.getUsernameFromToken(token);
                logger.debug("JWT Filter: Username from token = " + username);

                // 创建用户详情对象
                UserDetails userDetails = new User(username, "", new ArrayList<>());

                // 创建认证对象
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());

                // 设置认证信息到 SecurityContext
                return 0;
            } else {
                logger.info("JWT Filter: Invalid token");
                return -2;
            }
        } else {
            return -1;
        }
    }
    public static class AiStreamResultCallback extends ResultCallback<GenerationResult> {

        private final SseEmitter emitter;

        public AiStreamResultCallback(SseEmitter emitter) {
            this.emitter = emitter;
        }

        /**
         * 流式接收消息 🔥 2.16 标准写法
         */
        @Override
        public void onEvent(GenerationResult result) {
            try {
                if (result == null || result.getOutput() == null) {
                    return;
                }
                // 企业标准写法,永不null
                String content = result.getOutput().getText();
                if (content != null && !content.isEmpty()) {
                    logger.warn(content);
                    emitter.send(content);
                }
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        }

        /**
         * 流式完成
         */
        @Override
        public void onComplete() {
            try {
                emitter.send("[DONE]");
                emitter.complete();
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        }

        /**
         * 异常处理
         */
        @Override
        public void onError(Exception e) {
            logger.error("AI流式回调异常", e);
            emitter.completeWithError(e);
        }
    }
}