# Re:0 - Starting Life in Backend - Week2

🤑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)
  1. 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
  1. Config netGate, give access to swagger ui.