创建一个案例:使用Spring AI配合MCP(Model Context Protocol)构建一个"智能代码审查助手",它能够自动分析代码质量、生成改进建议,并与多个外部工具集成。
项目概述
这个案例将创建一个智能代码审查系统,具有以下特色功能:
- 自动代码质量分析
- 集成Git仓库管理
- 连接代码质量工具(如SonarQube)
- 生成可视化报告
- 提供重构建议
1. 项目结构
css
code-review-ai/
├── src/main/java/
│ ├── config/
│ │ ├── SpringAIConfig.java
│ │ └── MCPConfig.java
│ ├── controller/
│ │ └── CodeReviewController.java
│ ├── service/
│ │ ├── CodeAnalysisService.java
│ │ └── MCPIntegrationService.java
│ ├── mcp/
│ │ ├── tools/
│ │ │ ├── GitTool.java
│ │ │ ├── SonarQubeTool.java
│ │ │ └── FileAnalysisTool.java
│ │ └── server/
│ │ └── MCPServer.java
│ └── model/
│ ├── CodeReviewRequest.java
│ └── CodeReviewResult.java
└── pom.xml
2. Maven依赖配置
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>code-review-ai</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<properties>
<java.version>17</java.version>
<spring-ai.version>0.8.1</spring-ai.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Spring AI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- MCP相关依赖 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Git集成 -->
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>6.7.0.202309050840-r</version>
</dependency>
<!-- HTTP客户端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- 代码分析工具 -->
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-core</artifactId>
<version>3.24.8</version>
</dependency>
</dependencies>
</project>
3. 核心配置类
Spring AI配置
java
package com.example.config;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringAIConfig {
@Bean
public ChatClient chatClient() {
return OpenAiChatClient.builder()
.withApiKey(System.getenv("OPENAI_API_KEY"))
.build();
}
@Bean
public OpenAiApi openAiApi() {
return new OpenAiApi(System.getenv("OPENAI_API_KEY"));
}
}
MCP配置
java
package com.example.config;
import com.example.mcp.server.MCPServer;
import com.example.mcp.tools.GitTool;
import com.example.mcp.tools.SonarQubeTool;
import com.example.mcp.tools.FileAnalysisTool;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MCPConfig {
@Bean
public MCPServer mcpServer(GitTool gitTool,
SonarQubeTool sonarQubeTool,
FileAnalysisTool fileAnalysisTool) {
MCPServer server = new MCPServer();
server.registerTool(gitTool);
server.registerTool(sonarQubeTool);
server.registerTool(fileAnalysisTool);
return server;
}
@Bean
public GitTool gitTool() {
return new GitTool();
}
@Bean
public SonarQubeTool sonarQubeTool() {
return new SonarQubeTool();
}
@Bean
public FileAnalysisTool fileAnalysisTool() {
return new FileAnalysisTool();
}
}
4. MCP工具实现
Git工具
java
package com.example.mcp.tools;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.List;
import java.util.Map;
@Component
public class GitTool implements MCPTool {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public String getName() {
return "git";
}
@Override
public String getDescription() {
return "Git repository operations for code analysis";
}
@Override
public JsonNode getSchema() {
ObjectNode schema = objectMapper.createObjectNode();
schema.put("type", "object");
ObjectNode properties = objectMapper.createObjectNode();
ObjectNode actionProp = objectMapper.createObjectNode();
actionProp.put("type", "string");
actionProp.put("enum", List.of("clone", "diff", "log", "blame"));
ObjectNode repoProp = objectMapper.createObjectNode();
repoProp.put("type", "string");
repoProp.put("description", "Repository URL or path");
ObjectNode pathProp = objectMapper.createObjectNode();
pathProp.put("type", "string");
pathProp.put("description", "File or directory path");
properties.set("action", actionProp);
properties.set("repository", repoProp);
properties.set("path", pathProp);
schema.set("properties", properties);
schema.put("required", List.of("action", "repository"));
return schema;
}
@Override
public JsonNode execute(JsonNode parameters) {
try {
String action = parameters.get("action").asText();
String repository = parameters.get("repository").asText();
String path = parameters.has("path") ? parameters.get("path").asText() : "";
return switch (action) {
case "clone" -> cloneRepository(repository);
case "diff" -> getRepositoryDiff(repository, path);
case "log" -> getCommitLog(repository, path);
case "blame" -> getBlameInfo(repository, path);
default -> createErrorResponse("Unknown action: " + action);
};
} catch (Exception e) {
return createErrorResponse("Git operation failed: " + e.getMessage());
}
}
private JsonNode cloneRepository(String repoUrl) throws Exception {
File localPath = File.createTempFile("repo", "");
localPath.delete();
localPath.mkdirs();
Git git = Git.cloneRepository()
.setURI(repoUrl)
.setDirectory(localPath)
.call();
ObjectNode result = objectMapper.createObjectNode();
result.put("success", true);
result.put("localPath", localPath.getAbsolutePath());
result.put("message", "Repository cloned successfully");
return result;
}
private JsonNode getRepositoryDiff(String repoPath, String filePath) throws Exception {
Repository repository = new FileRepositoryBuilder()
.setGitDir(new File(repoPath, ".git"))
.build();
Git git = new Git(repository);
// 获取最近的diff
String diff = git.diff()
.call()
.stream()
.findFirst()
.map(Object::toString)
.orElse("No changes detected");
ObjectNode result = objectMapper.createObjectNode();
result.put("success", true);
result.put("diff", diff);
return result;
}
private JsonNode getCommitLog(String repoPath, String filePath) throws Exception {
Repository repository = new FileRepositoryBuilder()
.setGitDir(new File(repoPath, ".git"))
.build();
Git git = new Git(repository);
// 获取提交日志(简化版)
ObjectNode result = objectMapper.createObjectNode();
result.put("success", true);
result.put("log", "Commit log implementation here");
return result;
}
private JsonNode getBlameInfo(String repoPath, String filePath) throws Exception {
ObjectNode result = objectMapper.createObjectNode();
result.put("success", true);
result.put("blame", "Blame information for " + filePath);
return result;
}
private JsonNode createErrorResponse(String message) {
ObjectNode error = objectMapper.createObjectNode();
error.put("success", false);
error.put("error", message);
return error;
}
}
文件分析工具
java
package com.example.mcp.tools;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import org.springframework.stereotype.Component;
import java.io.File;
import java.nio.file.Files;
import java.util.List;
@Component
public class FileAnalysisTool implements MCPTool {
private final ObjectMapper objectMapper = new ObjectMapper();
private final JavaParser javaParser = new JavaParser();
@Override
public String getName() {
return "file_analysis";
}
@Override
public String getDescription() {
return "Analyze source code files for complexity, patterns, and potential issues";
}
@Override
public JsonNode getSchema() {
ObjectNode schema = objectMapper.createObjectNode();
schema.put("type", "object");
ObjectNode properties = objectMapper.createObjectNode();
ObjectNode fileProp = objectMapper.createObjectNode();
fileProp.put("type", "string");
fileProp.put("description", "Path to the source file");
ObjectNode typeProp = objectMapper.createObjectNode();
typeProp.put("type", "string");
typeProp.put("enum", List.of("complexity", "structure", "metrics", "issues"));
properties.set("filePath", fileProp);
properties.set("analysisType", typeProp);
schema.set("properties", properties);
schema.put("required", List.of("filePath", "analysisType"));
return schema;
}
@Override
public JsonNode execute(JsonNode parameters) {
try {
String filePath = parameters.get("filePath").asText();
String analysisType = parameters.get("analysisType").asText();
return switch (analysisType) {
case "complexity" -> analyzeComplexity(filePath);
case "structure" -> analyzeStructure(filePath);
case "metrics" -> calculateMetrics(filePath);
case "issues" -> detectIssues(filePath);
default -> createErrorResponse("Unknown analysis type: " + analysisType);
};
} catch (Exception e) {
return createErrorResponse("File analysis failed: " + e.getMessage());
}
}
private JsonNode analyzeComplexity(String filePath) throws Exception {
String content = Files.readString(new File(filePath).toPath());
CompilationUnit cu = javaParser.parse(content).getResult().orElse(null);
ObjectNode result = objectMapper.createObjectNode();
result.put("success", true);
if (cu != null) {
ArrayNode methods = objectMapper.createArrayNode();
cu.findAll(MethodDeclaration.class).forEach(method -> {
ObjectNode methodInfo = objectMapper.createObjectNode();
methodInfo.put("name", method.getNameAsString());
methodInfo.put("lineCount", method.getEnd().get().line - method.getBegin().get().line);
methodInfo.put("complexity", calculateCyclomaticComplexity(method));
methods.add(methodInfo);
});
result.set("methods", methods);
}
return result;
}
private JsonNode analyzeStructure(String filePath) throws Exception {
String content = Files.readString(new File(filePath).toPath());
CompilationUnit cu = javaParser.parse(content).getResult().orElse(null);
ObjectNode result = objectMapper.createObjectNode();
result.put("success", true);
if (cu != null) {
ArrayNode classes = objectMapper.createArrayNode();
cu.findAll(ClassOrInterfaceDeclaration.class).forEach(cls -> {
ObjectNode classInfo = objectMapper.createObjectNode();
classInfo.put("name", cls.getNameAsString());
classInfo.put("isInterface", cls.isInterface());
classInfo.put("methodCount", cls.getMethods().size());
classInfo.put("fieldCount", cls.getFields().size());
classes.add(classInfo);
});
result.set("classes", classes);
}
return result;
}
private JsonNode calculateMetrics(String filePath) throws Exception {
String content = Files.readString(new File(filePath).toPath());
ObjectNode metrics = objectMapper.createObjectNode();
metrics.put("success", true);
metrics.put("lineCount", content.split("\n").length);
metrics.put("characterCount", content.length());
metrics.put("wordCount", content.split("\\s+").length);
// 简单的代码质量指标
metrics.put("commentRatio", calculateCommentRatio(content));
metrics.put("complexity", "medium"); // 简化实现
return metrics;
}
private JsonNode detectIssues(String filePath) throws Exception {
String content = Files.readString(new File(filePath).toPath());
ObjectNode result = objectMapper.createObjectNode();
result.put("success", true);
ArrayNode issues = objectMapper.createArrayNode();
// 简单的问题检测
String[] lines = content.split("\n");
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
if (line.length() > 120) {
ObjectNode issue = objectMapper.createObjectNode();
issue.put("type", "line_too_long");
issue.put("line", i + 1);
issue.put("message", "Line exceeds 120 characters");
issues.add(issue);
}
if (line.contains("System.out.print")) {
ObjectNode issue = objectMapper.createObjectNode();
issue.put("type", "debug_code");
issue.put("line", i + 1);
issue.put("message", "Debug print statement found");
issues.add(issue);
}
}
result.set("issues", issues);
return result;
}
private int calculateCyclomaticComplexity(MethodDeclaration method) {
// 简化的圈复杂度计算
String methodBody = method.toString();
int complexity = 1; // 基础复杂度
complexity += countOccurrences(methodBody, "if");
complexity += countOccurrences(methodBody, "else");
complexity += countOccurrences(methodBody, "while");
complexity += countOccurrences(methodBody, "for");
complexity += countOccurrences(methodBody, "switch");
complexity += countOccurrences(methodBody, "case");
complexity += countOccurrences(methodBody, "catch");
return complexity;
}
private int countOccurrences(String text, String pattern) {
return text.split(pattern).length - 1;
}
private double calculateCommentRatio(String content) {
String[] lines = content.split("\n");
int commentLines = 0;
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
commentLines++;
}
}
return lines.length > 0 ? (double) commentLines / lines.length : 0.0;
}
private JsonNode createErrorResponse(String message) {
ObjectNode error = objectMapper.createObjectNode();
error.put("success", false);
error.put("error", message);
return error;
}
}
5. MCP服务器实现
java
package com.example.mcp.server;
import com.example.mcp.tools.MCPTool;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class MCPServer {
private final Map<String, MCPTool> tools = new HashMap<>();
private final ObjectMapper objectMapper = new ObjectMapper();
public void registerTool(MCPTool tool) {
tools.put(tool.getName(), tool);
}
public JsonNode listTools() {
ObjectNode response = objectMapper.createObjectNode();
ArrayNode toolsArray = objectMapper.createArrayNode();
tools.values().forEach(tool -> {
ObjectNode toolInfo = objectMapper.createObjectNode();
toolInfo.put("name", tool.getName());
toolInfo.put("description", tool.getDescription());
toolInfo.set("inputSchema", tool.getSchema());
toolsArray.add(toolInfo);
});
response.set("tools", toolsArray);
return response;
}
public JsonNode callTool(String toolName, JsonNode parameters) {
MCPTool tool = tools.get(toolName);
if (tool == null) {
ObjectNode error = objectMapper.createObjectNode();
error.put("error", "Tool not found: " + toolName);
return error;
}
try {
return tool.execute(parameters);
} catch (Exception e) {
ObjectNode error = objectMapper.createObjectNode();
error.put("error", "Tool execution failed: " + e.getMessage());
return error;
}
}
}
// MCP工具接口
interface MCPTool {
String getName();
String getDescription();
JsonNode getSchema();
JsonNode execute(JsonNode parameters);
}
6. 核心服务类
代码分析服务
java
package com.example.service;
import com.example.mcp.server.MCPServer;
import com.example.model.CodeReviewRequest;
import com.example.model.CodeReviewResult;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class CodeAnalysisService {
@Autowired
private ChatClient chatClient;
@Autowired
private MCPServer mcpServer;
private final ObjectMapper objectMapper = new ObjectMapper();
public CodeReviewResult analyzeCode(CodeReviewRequest request) {
try {
// 1. 使用MCP工具分析代码结构
JsonNode structureAnalysis = analyzeCodeStructure(request.getFilePath());
// 2. 使用MCP工具检测问题
JsonNode issuesAnalysis = detectCodeIssues(request.getFilePath());
// 3. 使用Spring AI生成智能建议
String aiSuggestions = generateAISuggestions(request, structureAnalysis, issuesAnalysis);
// 4. 综合分析结果
return buildCodeReviewResult(request, structureAnalysis, issuesAnalysis, aiSuggestions);
} catch (Exception e) {
throw new RuntimeException("Code analysis failed", e);
}
}
private JsonNode analyzeCodeStructure(String filePath) {
ObjectNode parameters = objectMapper.createObjectNode();
parameters.put("filePath", filePath);
parameters.put("analysisType", "structure");
return mcpServer.callTool("file_analysis", parameters);
}
private JsonNode detectCodeIssues(String filePath) {
ObjectNode parameters = objectMapper.createObjectNode();
parameters.put("filePath", filePath);
parameters.put("analysisType", "issues");
return mcpServer.callTool("file_analysis", parameters);
}
private String generateAISuggestions(CodeReviewRequest request,
JsonNode structureAnalysis,
JsonNode issuesAnalysis) {
String prompt = buildAnalysisPrompt(request, structureAnalysis, issuesAnalysis);
ChatResponse response = chatClient.call(
new Prompt(new UserMessage(prompt))
);
return response.getResult().getOutput().getContent();
}
private String buildAnalysisPrompt(CodeReviewRequest request,
JsonNode structureAnalysis,
JsonNode issuesAnalysis) {
StringBuilder prompt = new StringBuilder();
prompt.append("作为一名资深代码审查专家,请分析以下代码信息并提供专业建议:\n\n");
prompt.append("文件路径: ").append(request.getFilePath()).append("\n");
prompt.append("分析类型: ").append(request.getAnalysisType()).append("\n\n");
prompt.append("代码结构分析结果:\n");
prompt.append(structureAnalysis.toPrettyString()).append("\n\n");
prompt.append("发现的问题:\n");
prompt.append(issuesAnalysis.toPrettyString()).append("\n\n");
prompt.append("请基于以上分析提供:\n");
prompt.append("1. 代码质量评估\n");
prompt.append("2. 具体的改进建议\n");
prompt.append("3. 重构建议(如果需要)\n");
prompt.append("4. 最佳实践推荐\n");
prompt.append("5. 潜在风险识别\n\n");
prompt.append("请用中文回答,并提供具体可行的建议。");
return prompt.toString();
}
private CodeReviewResult buildCodeReviewResult(CodeReviewRequest request,
JsonNode structureAnalysis,
JsonNode issuesAnalysis,
String aiSuggestions) {
CodeReviewResult result = new CodeReviewResult();
result.setFilePath(request.getFilePath());
result.setAnalysisType(request.getAnalysisType());
result.setStructureAnalysis(structureAnalysis.toString());
result.setIssuesFound(issuesAnalysis.toString());
result.setAiSuggestions(aiSuggestions);
result.setTimestamp(System.currentTimeMillis());
// 计算总体评分
int score = calculateOverallScore(structureAnalysis, issuesAnalysis);
result.setQualityScore(score);
return result;
}
private int calculateOverallScore(JsonNode structureAnalysis, JsonNode issuesAnalysis) {
int baseScore = 100;
// 根据发现的问题数量扣分
if (issuesAnalysis.has("issues")) {
int issueCount = issuesAnalysis.get("issues").size();
baseScore -= Math.min(issueCount * 5, 50); // 每个问题扣5分,最多扣50分
}
// 根据复杂度调整分数
// 这里可以添加更复杂的评分逻辑
return Math.max(baseScore, 0);
}
}
MCP集成服务
java
package com.example.service;
import com.example.mcp.server.MCPServer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MCPIntegrationService {
@Autowired
private MCPServer mcpServer;
private final ObjectMapper objectMapper = new ObjectMapper();
public JsonNode executeGitOperation(String action, String repository, String path) {
ObjectNode parameters = objectMapper.createObjectNode();
parameters.put("action", action);
parameters.put("repository", repository);
if (path != null && !path.isEmpty()) {
parameters.put("path", path);
}
return mcpServer.callTool("git", parameters);
}
public JsonNode analyzeWithSonarQube(String projectKey, String filePath) {
ObjectNode parameters = objectMapper.createObjectNode();
parameters.put("action", "analyze");
parameters.put("projectKey", projectKey);
parameters.put("filePath", filePath);
return mcpServer.callTool("sonarqube", parameters);
}
public JsonNode getAvailableTools() {
return mcpServer.listTools();
}
public JsonNode executeCustomTool(String toolName, JsonNode parameters) {
return mcpServer.callTool(toolName, parameters);
}
}
7. REST控制器
java
package com.example.controller;
import com.example.model.CodeReviewRequest;
import com.example.model.CodeReviewResult;
import com.example.service.CodeAnalysisService;
import com.example.service.MCPIntegrationService;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/code-review")
@CrossOrigin(origins = "*")
public class CodeReviewController {
@Autowired
private CodeAnalysisService codeAnalysisService;
@Autowired
private MCPIntegrationService mcpIntegrationService;
@PostMapping("/analyze")
public ResponseEntity<CodeReviewResult> analyzeCode(@RequestBody CodeReviewRequest request) {
try {
CodeReviewResult result = codeAnalysisService.analyzeCode(request);
return ResponseEntity.ok(result);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@GetMapping("/tools")
public ResponseEntity<JsonNode> getAvailableTools() {
JsonNode tools = mcpIntegrationService.getAvailableTools();
return ResponseEntity.ok(tools);
}
@PostMapping("/git/{action}")
public ResponseEntity<JsonNode> executeGitOperation(
@PathVariable String action,
@RequestParam String repository,
@RequestParam(required = false) String path) {
JsonNode result = mcpIntegrationService.executeGitOperation(action, repository, path);
return ResponseEntity.ok(result);
}
@PostMapping("/sonarqube/analyze")
public ResponseEntity<JsonNode> analyzWithSonarQube(
@RequestParam String projectKey,
@RequestParam String filePath) {
JsonNode result = mcpIntegrationService.analyzeWithSonarQube(projectKey, filePath);
return ResponseEntity.ok(result);
}
@PostMapping("/tools/{toolName}")
public ResponseEntity<JsonNode> executeCustomTool(
@PathVariable String toolName,
@RequestBody JsonNode parameters) {
JsonNode result = mcpIntegrationService.executeCustomTool(toolName, parameters);
return ResponseEntity.ok(result);
}
}
8. 数据模型类
代码审查请求模型
java
package com.example.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public class CodeReviewRequest {
@JsonProperty("filePath")
private String filePath;
@JsonProperty("analysisType")
private String analysisType;
@JsonProperty("repository")
private String repository;
@JsonProperty("branch")
private String branch;
@JsonProperty("includeMetrics")
private boolean includeMetrics = true;
@JsonProperty("includeSuggestions")
private boolean includeSuggestions = true;
// 构造函数
public CodeReviewRequest() {}
public CodeReviewRequest(String filePath, String analysisType) {
this.filePath = filePath;
this.analysisType = analysisType;
}
// Getter和Setter方法
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getAnalysisType() {
return analysisType;
}
public void setAnalysisType(String analysisType) {
this.analysisType = analysisType;
}
public String getRepository() {
return repository;
}
public void setRepository(String repository) {
this.repository = repository;
}
public String getBranch() {
return branch;
}
public void setBranch(String branch) {
this.branch = branch;
}
public boolean isIncludeMetrics() {
return includeMetrics;
}
public void setIncludeMetrics(boolean includeMetrics) {
this.includeMetrics = includeMetrics;
}
public boolean isIncludeSuggestions() {
return includeSuggestions;
}
public void setIncludeSuggestions(boolean includeSuggestions) {
this.includeSuggestions = includeSuggestions;
}
}
代码审查结果模型
java
package com.example.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
public class CodeReviewResult {
@JsonProperty("filePath")
private String filePath;
@JsonProperty("analysisType")
private String analysisType;
@JsonProperty("qualityScore")
private int qualityScore;
@JsonProperty("structureAnalysis")
private String structureAnalysis;
@JsonProperty("issuesFound")
private String issuesFound;
@JsonProperty("aiSuggestions")
private String aiSuggestions;
@JsonProperty("timestamp")
private long timestamp;
@JsonProperty("metrics")
private Map<String, Object> metrics;
@JsonProperty("recommendations")
private List<String> recommendations;
// 构造函数
public CodeReviewResult() {}
// Getter和Setter方法
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getAnalysisType() {
return analysisType;
}
public void setAnalysisType(String analysisType) {
this.analysisType = analysisType;
}
public int getQualityScore() {
return qualityScore;
}
public void setQualityScore(int qualityScore) {
this.qualityScore = qualityScore;
}
public String getStructureAnalysis() {
return structureAnalysis;
}
public void setStructureAnalysis(String structureAnalysis) {
this.structureAnalysis = structureAnalysis;
}
public String getIssuesFound() {
return issuesFound;
}
public void setIssuesFound(String issuesFound) {
this.issuesFound = issuesFound;
}
public String getAiSuggestions() {
return aiSuggestions;
}
public void setAiSuggestions(String aiSuggestions) {
this.aiSuggestions = aiSuggestions;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public Map<String, Object> getMetrics() {
return metrics;
}
public void setMetrics(Map<String, Object> metrics) {
this.metrics = metrics;
}
public List<String> getRecommendations() {
return recommendations;
}
public void setRecommendations(List<String> recommendations) {
this.recommendations = recommendations;
}
}
9. SonarQube集成工具
java
package com.example.mcp.tools;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
public class SonarQubeTool implements MCPTool {
private final ObjectMapper objectMapper = new ObjectMapper();
private final WebClient webClient;
public SonarQubeTool() {
this.webClient = WebClient.builder()
.baseUrl(System.getenv().getOrDefault("SONARQUBE_URL", "http://localhost:9000"))
.build();
}
@Override
public String getName() {
return "sonarqube";
}
@Override
public String getDescription() {
return "Integration with SonarQube for code quality analysis";
}
@Override
public JsonNode getSchema() {
ObjectNode schema = objectMapper.createObjectNode();
schema.put("type", "object");
ObjectNode properties = objectMapper.createObjectNode();
ObjectNode actionProp = objectMapper.createObjectNode();
actionProp.put("type", "string");
actionProp.put("enum", List.of("analyze", "issues", "metrics", "coverage"));
ObjectNode projectProp = objectMapper.createObjectNode();
projectProp.put("type", "string");
projectProp.put("description", "SonarQube project key");
ObjectNode fileProp = objectMapper.createObjectNode();
fileProp.put("type", "string");
fileProp.put("description", "File path to analyze");
properties.set("action", actionProp);
properties.set("projectKey", projectProp);
properties.set("filePath", fileProp);
schema.set("properties", properties);
schema.put("required", List.of("action", "projectKey"));
return schema;
}
@Override
public JsonNode execute(JsonNode parameters) {
try {
String action = parameters.get("action").asText();
String projectKey = parameters.get("projectKey").asText();
String filePath = parameters.has("filePath") ? parameters.get("filePath").asText() : "";
return switch (action) {
case "analyze" -> triggerAnalysis(projectKey, filePath);
case "issues" -> getIssues(projectKey, filePath);
case "metrics" -> getMetrics(projectKey, filePath);
case "coverage" -> getCoverage(projectKey, filePath);
default -> createErrorResponse("Unknown action: " + action);
};
} catch (Exception e) {
return createErrorResponse("SonarQube operation failed: " + e.getMessage());
}
}
private JsonNode triggerAnalysis(String projectKey, String filePath) {
try {
// 模拟SonarQube分析触发
ObjectNode result = objectMapper.createObjectNode();
result.put("success", true);
result.put("message", "Analysis triggered for project: " + projectKey);
result.put("taskId", "task-" + System.currentTimeMillis());
return result;
} catch (Exception e) {
return createErrorResponse("Failed to trigger analysis: " + e.getMessage());
}
}
private JsonNode getIssues(String projectKey, String filePath) {
try {
// 模拟获取SonarQube问题
ObjectNode result = objectMapper.createObjectNode();
result.put("success", true);
ArrayNode issues = objectMapper.createArrayNode();
// 模拟问题数据
ObjectNode issue1 = objectMapper.createObjectNode();
issue1.put("key", "issue-1");
issue1.put("rule", "java:S1481");
issue1.put("severity", "MAJOR");
issue1.put("message", "Remove this unused local variable");
issue1.put("file", filePath);
issue1.put("line", 42);
issues.add(issue1);
ObjectNode issue2 = objectMapper.createObjectNode();
issue2.put("key", "issue-2");
issue2.put("rule", "java:S1192");
issue2.put("severity", "MINOR");
issue2.put("message", "Define a constant instead of duplicating this literal");
issue2.put("file", filePath);
issue2.put("line", 15);
issues.add(issue2);
result.set("issues", issues);
result.put("totalIssues", issues.size());
return result;
} catch (Exception e) {
return createErrorResponse("Failed to get issues: " + e.getMessage());
}
}
private JsonNode getMetrics(String projectKey, String filePath) {
try {
ObjectNode result = objectMapper.createObjectNode();
result.put("success", true);
ObjectNode metrics = objectMapper.createObjectNode();
metrics.put("complexity", 15);
metrics.put("coverage", 78.5);
metrics.put("duplicatedLines", 12);
metrics.put("linesOfCode", 450);
metrics.put("technicalDebt", "2h 30min");
metrics.put("bugs", 2);
metrics.put("vulnerabilities", 0);
metrics.put("codeSmells", 8);
result.set("metrics", metrics);
return result;
} catch (Exception e) {
return createErrorResponse("Failed to get metrics: " + e.getMessage());
}
}
private JsonNode getCoverage(String projectKey, String filePath) {
try {
ObjectNode result = objectMapper.createObjectNode();
result.put("success", true);
ObjectNode coverage = objectMapper.createObjectNode();
coverage.put("lineCoverage", 85.2);
coverage.put("branchCoverage", 72.8);
coverage.put("coveredLines", 382);
coverage.put("uncoveredLines", 68);
coverage.put("totalLines", 450);
result.set("coverage", coverage);
return result;
} catch (Exception e) {
return createErrorResponse("Failed to get coverage: " + e.getMessage());
}
}
private JsonNode createErrorResponse(String message) {
ObjectNode error = objectMapper.createObjectNode();
error.put("success", false);
error.put("error", message);
return error;
}
}
10. 应用程序主类
java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CodeReviewAIApplication {
public static void main(String[] args) {
SpringApplication.run(CodeReviewAIApplication.class, args);
}
}
11. 配置文件
application.yml
yaml
server:
port: 8080
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4
temperature: 0.1
max-tokens: 2000
logging:
level:
com.example: DEBUG
org.springframework.ai: DEBUG
management:
endpoints:
web:
exposure:
include: health,info,metrics
# 自定义配置
code-review:
sonarqube:
url: ${SONARQUBE_URL:http://localhost:9000}
token: ${SONARQUBE_TOKEN:}
git:
temp-directory: ${java.io.tmpdir}/code-review-repos
analysis:
max-file-size: 1048576 # 1MB
supported-extensions: java,js,ts,py,go,cpp,c,cs
12. 使用示例
前端调用示例(JavaScript)
javascript
// 分析代码示例
async function analyzeCode() {
const request = {
filePath: "/path/to/your/source/file.java",
analysisType: "comprehensive",
repository: "https://github.com/user/repo.git",
includeMetrics: true,
includeSuggestions: true
};
try {
const response = await fetch('/api/code-review/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
});
const result = await response.json();
console.log('分析结果:', result);
// 显示质量评分
console.log(`代码质量评分: ${result.qualityScore}/100`);
// 显示AI建议
console.log('AI建议:', result.aiSuggestions);
} catch (error) {
console.error('分析失败:', error);
}
}
// 获取可用工具
async function getAvailableTools() {
try {
const response = await fetch('/api/code-review/tools');
const tools = await response.json();
console.log('可用工具:', tools);
} catch (error) {
console.error('获取工具失败:', error);
}
}
// Git操作示例
async function cloneRepository() {
const params = new URLSearchParams({
repository: 'https://github.com/user/repo.git'
});
try {
const response = await fetch(`/api/code-review/git/clone?${params}`, {
method: 'POST'
});
const result = await response.json();
console.log('克隆结果:', result);
} catch (error) {
console.error('克隆失败:', error);
}
}
命令行使用示例(curl)
bash
# 分析代码
curl -X POST http://localhost:8080/api/code-review/analyze \
-H "Content-Type: application/json" \
-d '{
"filePath": "/path/to/source/file.java",
"analysisType": "comprehensive",
"includeMetrics": true,
"includeSuggestions": true
}'
# 获取可用工具
curl http://localhost:8080/api/code-review/tools
# 执行Git操作
curl -X POST "http://localhost:8080/api/code-review/git/clone?repository=https://github.com/user/repo.git"
# SonarQube分析
curl -X POST "http://localhost:8080/api/code-review/sonarqube/analyze?projectKey=my-project&filePath=/src/main/java/Main.java"
13. 项目特色功能
智能代码评审流程
- 多维度分析:结合静态代码分析、复杂度计算、问题检测
- AI增强建议:使用Spring AI生成智能化的改进建议
- 工具集成:通过MCP协议集成多种代码质量工具
- 实时反馈:提供即时的代码质量评估和建议
扩展性设计
- 插件化架构:通过MCP协议轻松添加新的分析工具
- 多语言支持:可扩展支持更多编程语言
- 自定义规则:允许定义项目特定的代码质量规则
实际应用价值
- 提高代码质量:自动化识别常见问题和反模式
- 知识传播:AI建议帮助开发者学习最佳实践
- 效率提升:减少人工代码审查的工作量
- 标准化:统一团队的代码质量标准
这个案例展示了Spring AI与MCP的强大结合,创建了一个智能、可扩展的代码审查系统。