# Re:0 - Starting Life in Backend - Week2
2025-08-04
13 min read
🤑seal
There you will
Be the one, Be the one!
We will…
必ず夜明けは 巡ってくるから
Be the light, Be the light!
We will…
未来へ繋ごう
过去を労ろう
SpringDoc
SpringBoot 2.1.6 RELEASE <--> SpringDoc 1.6.14
Change Swagger2 annotations to Openapi annotations
import re
import os
import shutil # For cleaning up test directory
def migrate_swagger_to_openapi3_in_directory(root_directory):
"""
遍历指定路径下的所有 Java 代码文件,并将其中的 Swagger 2 注解和依赖项迁移到 OpenAPI 3。
Args:
root_directory (str): 包含 Java 代码文件的根目录路径。
"""
if not os.path.isdir(root_directory):
print(f"错误:指定的路径 '{root_directory}' 不是一个有效的目录。")
return
print(f"开始遍历目录 '{root_directory}' 下的 Java 文件并进行迁移...")
migrated_files_count = 0
skipped_files_count = 0
error_files_count = 0
for dirpath, _, filenames in os.walk(root_directory):
for filename in filenames:
if filename.endswith(".java"):
filepath = os.path.join(dirpath, filename)
print(f"\n--- 正在处理文件: {filepath} ---")
try:
with open(filepath, 'r', encoding='utf-8') as f:
original_content = f.read()
except Exception as e:
print(f"错误:读取文件 '{filepath}' 时发生错误:{e}")
error_files_count += 1
continue
# Store original content to check if any changes were made
processed_content = original_content
# --- 1. 初始内容解析和旧导入移除 ---
lines = processed_content.splitlines()
package_declaration = ""
existing_imports = set()
code_body_lines = []
for line in lines:
stripped_line = line.strip()
if stripped_line.startswith("package "):
package_declaration = line + "\n\n" # Preserve original package declaration and add blank lines
elif stripped_line.startswith("import "):
existing_imports.add(stripped_line)
else:
code_body_lines.append(line)
# Patterns for old imports to be removed
old_import_patterns = [
re.compile(r"import\s+io\.swagger\.annotations\..*;"),
re.compile(r"import\s+io\.springfox\..*;"),
re.compile(r"import\s+springfox\.documentation\..*;"),
]
# Remove old imports
current_imports_after_removal = set()
for imp in existing_imports:
is_old_import = False
for pattern in old_import_patterns:
if pattern.match(imp):
is_old_import = True
break
if not is_old_import:
current_imports_after_removal.add(imp)
# Temporarily combine the code body into a string for annotation processing
temp_content_for_annotation_processing = "\n".join(code_body_lines)
# --- 2. 注解转换 ---
# Using re.DOTALL flag to make '.' match any character including newlines
# Using re.MULTILINE flag to make '^' and '$' match the start and end of each line
# 2.1. @Api -> @Tag (处理 tags 属性转换为 name)
def replace_api_to_tag(match):
attrs_str = match.group('attrs')
# 尝试匹配 tags = { "..." } 或 tags = "..."
tags_match = re.search(r'tags\s*=\s*(?:\{(?P<tags_arr>[^}]*?)\}|["\'](?P<tags_str>[^"\']*)["\'])', attrs_str)
if tags_match:
# 如果是数组形式 tags = { "tag1", "tag2" },取第一个作为 name
if tags_match.group('tags_arr'):
# 提取数组中的第一个标签
first_tag_match = re.search(r'["\'](?P<first_tag>[^"\']*)["\']', tags_match.group('tags_arr'))
if first_tag_match:
tag_name = first_tag_match.group('first_tag')
return f'@Tag(name = "{tag_name}")'
# 如果是字符串形式 tags = "tag_name"
elif tags_match.group('tags_str'):
tag_name = tags_match.group('tags_str')
return f'@Tag(name = "{tag_name}")'
# 如果没有找到 tags 属性,尝试匹配 value 属性
value_match = re.search(r'value\s*=\s*["\'](?P<value>[^"\']*)["\']', attrs_str)
if value_match:
return f'@Tag(name = "{value_match.group("value")}")'
# 如果@Api没有参数,或者参数无法转换成name,则使用默认的@Tag()或@Tag
# 匹配没有括号的 @Api,或者只有 @Api()
if not attrs_str.strip(): # @Api 或 @Api()
return '@Tag'
# 匹配只有字符串字面量的 @Api("description")
single_string_match = re.search(r'^["\'](?P<desc>[^"\']*)["\']$', attrs_str.strip())
if single_string_match:
return f'@Tag(name = "{single_string_match.group("desc")}")'
# Fallback: remove all unrecognized attributes for @Api and just make it @Tag()
return '@Tag()'
# 先处理带括号的 @Api,包括 tags 和 value 属性
temp_content_for_annotation_processing = re.sub(
r'@Api\s*\((?P<attrs>[^)]*)\)',
replace_api_to_tag,
temp_content_for_annotation_processing,
flags=re.DOTALL
)
# 最后处理不带括号的 @Api
temp_content_for_annotation_processing = re.sub(r'@Api\s*(?=\n|\r|\s|\Z)', '@Tag', temp_content_for_annotation_processing)
# 2.2. @ApiOperation -> @Operation (value -> summary, notes -> description)
temp_content_for_annotation_processing = re.sub(
r'@ApiOperation\s*\(\s*value\s*=\s*["\'](?P<summary>[^"\']*)["\']\s*(?:,\s*notes\s*=\s*["\'](?P<description>[^"\']*)["\'])?\s*\)',
lambda m: f'@Operation(summary = "{m.group("summary")}"' + (f', description = "{m.group("description")}"' if m.group("description") else ') + ')',
temp_content_for_annotation_processing,
flags=re.DOTALL
)
temp_content_for_annotation_processing = re.sub(
r'@ApiOperation\s*\(\s*["\'](?P<summary>[^"\']*)["\']\s*\)',
r'@Operation(summary = "\g<summary>")',
temp_content_for_annotation_processing,
flags=re.DOTALL
)
# 2.3. @ApiResponse conversion: code -> responseCode, message -> description, response -> content=@Content(schema=@Schema(implementation=X.class))
def replace_api_response(match):
code = message = response_class = None
attrs_str = match.group('attrs')
code_match = re.search(r'code\s*=\s*(?P<code>\d+)', attrs_str)
if code_match:
code = code_match.group('code')
message_match = re.search(r'message\s*=\s*["\'](?P<message>[^"\']*)["\']', attrs_str)
if message_match:
message = message_match.group('message')
response_match = re.search(r'response\s*=\s*(?P<response_class>[A-Za-z0-9_.]+)\.class', attrs_str)
if response_match:
response_class = response_match.group('response_class')
parts = []
if code:
parts.append(f'responseCode = "{code}"')
if message:
parts.append(f'description = "{message}"')
if response_class:
parts.append(f'content = @Content(schema = @Schema(implementation = {response_class}.class))')
return f'@ApiResponse({", ".join(parts)})'
temp_content_for_annotation_processing = re.sub(
r'@ApiResponse\s*\((?P<attrs>[^)]*)\)',
replace_api_response,
temp_content_for_annotation_processing,
flags=re.DOTALL
)
# 2.4. @ApiParam -> @Parameter (value -> description)
temp_content_for_annotation_processing = re.sub(
r'@ApiParam\s*\(\s*value\s*=\s*["\'](?P<description>[^"\']*)["\']\s*\)',
r'@Parameter(description = "\g<description>")',
temp_content_for_annotation_processing,
flags=re.DOTALL
)
temp_content_for_annotation_processing = re.sub(
r'@ApiParam\s*\(\s*["\'](?P<description>[^"\']*)["\']\s*\)',
r'@Parameter(description = "\g<description>")',
temp_content_for_annotation_processing,
flags=re.DOTALL
)
# 2.5. @ApiImplicitParam -> @Parameter (value -> description, remove dataType, paramType, allowMultiple)
def process_parameter_attributes(match):
params_str = match.group('params')
params_str = re.sub(r'value\s*=\s*', 'description = ', params_str)
params_str = re.sub(r'dataType\s*=\s*[^,)]*(?:,\s*)?', ', params_str)
params_str = re.sub(r'paramType\s*=\s*[^,)]*(?:,\s*)?', ', params_str)
params_str = re.sub(r'allowMultiple\s*=\s*[^,)]*(?:,\s*)?', ', params_str)
while ',,' in params_str:
params_str = params_str.replace(',,', ',')
params_str = params_str.strip(', ').strip()
if not params_str:
return '@Parameter()'
return f'@Parameter({params_str})'
temp_content_for_annotation_processing = re.sub(
r'@ApiImplicitParam\s*\((?P<params>[^)]*)\)',
process_parameter_attributes,
temp_content_for_annotation_processing,
flags=re.DOTALL
)
# 2.6. @ApiModel -> @Schema (value -> name, description direct map)
# Handle @ApiModel without parentheses
temp_content_for_annotation_processing = re.sub(r'@ApiModel\s*(?=\n|\r|\s|\Z)', '@Schema', temp_content_for_annotation_processing)
# Handle @ApiModel with value and/or description
temp_content_for_annotation_processing = re.sub(
r'@ApiModel\s*\(\s*value\s*=\s*["\'](?P<name>[^"\']*)["\']\s*(?:,\s*description\s*=\s*["\'](?P<description>[^"\']*)["\'])?\s*\)',
lambda m: f'@Schema(name = "{m.group("name")}"' + (f', description = "{m.group("description")}"' if m.group("description") else ') + ')',
temp_content_for_annotation_processing,
flags=re.DOTALL
)
# Handle @ApiModel with only string value
temp_content_for_annotation_processing = re.sub(
r'@ApiModel\s*\(\s*["\'](?P<name>[^"\']*)["\']\s*\)',
r'@Schema(name = "\g<name>")',
temp_content_for_annotation_processing,
flags=re.DOTALL
)
# 2.7. @ApiModelProperty -> @Schema
# User specific requirement: @ApiModelProperty("总数") -> @Schema(defaultValue = "总数")
temp_content_for_annotation_processing = re.sub(
r'@ApiModelProperty\s*\(\s*["\'](?P<value>[^"\']*)["\']\s*\)',
r'@Schema(defaultValue = "\g<value>")',
temp_content_for_annotation_processing,
flags=re.DOTALL
)
def replace_api_model_property(match):
attrs_str = match.group('attrs')
new_attrs = []
value_match = re.search(r'value\s*=\s*["\'](?P<val>[^"\']*)["\']', attrs_str)
if value_match:
new_attrs.append(f'description = "{value_match.group("val")}"')
attrs_str = re.sub(r'value\s*=\s*["\'][^"\']*["\']', ', attrs_str)
attrs_str = re.sub(r'notes\s*=\s*["\'][^"\']*["\']', ', attrs_str)
datatype_match = re.search(r'dataType\s*=\s*["\'](?P<val>[^"\']*)["\']', attrs_str)
if datatype_match:
new_attrs.append(f'type = "{datatype_match.group("val")}"')
attrs_str = re.sub(r'dataType\s*=\s*["\'][^"\']*["\']', ', attrs_str)
allow_empty_match = re.search(r'allowEmptyValue\s*=\s*(?P<val>true|false)', attrs_str)
if allow_empty_match:
new_attrs.append(f'nullable = {allow_empty_match.group("val")}')
attrs_str = re.sub(r'allowEmptyValue\s*=\s*(?:true|false)', ', attrs_str)
required_match = re.search(r'required\s*=\s*(?P<val>true|false)', attrs_str)
if required_match:
new_attrs.append(f'required = {required_match.group("val")}')
attrs_str = re.sub(r'required\s*=\s*(?:true|false)', ', attrs_str)
example_match = re.search(r'example\s*=\s*["\'](?P<val>[^"\']*)["\']', attrs_str)
if example_match:
new_attrs.append(f'example = "{example_match.group("val")}"')
attrs_str = re.sub(r'example\s*=\s*["\'][^"\']*["\']', ', attrs_str)
attrs_str = re.sub(r'position\s*=\s*\d+', ', attrs_str)
remaining_attrs = [a.strip() for a in attrs_str.split(',') if a.strip()]
new_attrs.extend(remaining_attrs)
final_attrs_str = ", ".join(filter(None, new_attrs))
return f'@Schema({final_attrs_str})'
# Standard @ApiModelProperty conversion: value -> description, notes removed, dataType -> type, allowEmptyValue -> nullable, required, example direct map
# position removed
temp_content_for_annotation_processing = re.sub(
r'@ApiModelProperty\s*\((?!["\'][^"\']*["\']\s*\))(?P<attrs>[^)]*)\)',
replace_api_model_property,
temp_content_for_annotation_processing,
flags=re.DOTALL
)
# Added: Handle @ApiModelProperty without parentheses
temp_content_for_annotation_processing = re.sub(r'@ApiModelProperty\s*(?=\n|\r|\s|\Z)', '@Schema', temp_content_for_annotation_processing)
# 2.8. @ApiIgnore -> @Hidden
temp_content_for_annotation_processing = re.sub(r'@ApiIgnore', '@Hidden', temp_content_for_annotation_processing)
# 2.9. @SwaggerDefinition -> @OpenAPIDefinition
temp_content_for_annotation_processing = re.sub(r'@SwaggerDefinition', '@OpenAPIDefinition', temp_content_for_annotation_processing)
# 2.10. Remove @EnableSwagger2
temp_content_for_annotation_processing = re.sub(r'@EnableSwagger2\s*', ', temp_content_for_annotation_processing)
# --- 3. 根据替换后的内容添加必要的 OpenAPI 3 导入 ---
# Mapping of OpenAPI 3 annotations to their full import paths
openapi3_annotations_to_imports = {
'Operation': "import io.swagger.v3.oas.annotations.Operation;",
'Content': "import io.swagger.v3.oas.annotations.media.Content;",
'Schema': "import io.swagger.v3.oas.annotations.media.Schema;",
'ApiResponse': "import io.swagger.v3.oas.annotations.responses.ApiResponse;",
'ApiResponses': "import io.swagger.v3.oas.annotations.responses.ApiResponses;",
'Parameter': "import io.swagger.v3.oas.annotations.Parameter;",
'Tag': "import io.swagger.v3.oas.annotations.tags.Tag;",
'Hidden': "import io.swagger.v3.oas.annotations.Hidden;",
'OpenAPIDefinition': "import io.swagger.v3.oas.annotations.OpenAPIDefinition;",
}
# Check which OpenAPI 3 annotations are actually used in the code
newly_required_imports = set()
for annotation, import_path in openapi3_annotations_to_imports.items():
# Use regex to match annotations, ensuring no partial matches (e.g., @Schema within @ApiResponses(..., schema=...))
# Match @AnnotationName or AnnotationName(
if re.search(rf'@{re.escape(annotation)}(?![a-zA-Z0-9_])', temp_content_for_annotation_processing) or \
re.search(rf'@{re.escape(annotation)}\(', temp_content_for_annotation_processing):
newly_required_imports.add(import_path)
# Merge dynamically determined new imports with existing ones
final_imports_set = current_imports_after_removal.union(newly_required_imports)
sorted_final_imports = sorted(list(final_imports_set))
# --- 4. Reconstruct the final content ---
final_content_parts = []
if package_declaration:
final_content_parts.append(package_declaration.strip())
if sorted_final_imports:
final_content_parts.extend(sorted_final_imports)
final_content_parts.append("") # Add a blank line between imports and code
final_content_parts.extend(temp_content_for_annotation_processing.splitlines()) # Re-split processed code body into lines
new_content = "\n".join(final_content_parts)
# --- 5. Write back to file if content changed ---
if new_content != original_content:
try:
with open(filepath, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f"成功将修改后的代码写入到文件:'{filepath}'。")
migrated_files_count += 1
except Exception as e:
print(f"错误:写入文件 '{filepath}' 时发生错误:{e}")
error_files_count += 1
else:
print(f"文件 '{filepath}' 未发生变化,跳过写入。")
skipped_files_count += 1
print("\n--- 迁移完成 ---")
print(f"总计处理文件:{migrated_files_count + skipped_files_count + error_files_count}")
print(f"成功迁移文件:{migrated_files_count}")
print(f"未发生变化文件:{skipped_files_count}")
print(f"处理出错文件:{error_files_count}")
if __name__ == "__main__":
test_dir = "temp_java_project"
migrate_swagger_to_openapi3_in_directory(test_dir)
- Config nacos configuration
springdoc:
swagger-ui:
enabled: true
path: /swagger-ui.html
url: /v3/api-docs
api-docs:
enabled: true
path: /v3/api-docs
info:
title: FastOp
version: 1.0.0
description: 微服务
contact:
name: FastOp
email:
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
- Config netGate, give access to swagger ui.