diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..3fe4d065 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "maven" # See documentation for possible values + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 10 \ No newline at end of file diff --git a/.github/scripts/generate-wiki-docs.py b/.github/scripts/generate-wiki-docs.py new file mode 100755 index 00000000..754d413c --- /dev/null +++ b/.github/scripts/generate-wiki-docs.py @@ -0,0 +1,831 @@ +#!/usr/bin/env python3 +""" +Microsphere Java Wiki Documentation Generator + +Parses Java source files in the project and generates +Markdown wiki pages for each public Java component (class, interface, enum, annotation). + +Each wiki page includes: +- Detailed explanation of the component +- Example code extracted from Javadoc +- Version compatibility information +- Since version metadata + +Generated pages are written to a specified output directory, one page per component, +ready to be pushed to the GitHub wiki repository. +""" + +import os +import re +import sys +import argparse +from collections import OrderedDict + +# ────────────────────────────────────────────── +# Constants +# ────────────────────────────────────────────── + +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Source directory path suffix +SRC_MAIN_JAVA = os.path.join("src", "main", "java") + + +def _discover_modules(project_root): + """Discover module directories that contain Java sources.""" + modules = [] + for entry in sorted(os.listdir(project_root)): + entry_path = os.path.join(project_root, entry) + if os.path.isdir(entry_path) and os.path.isdir(os.path.join(entry_path, SRC_MAIN_JAVA)): + modules.append(entry) + return modules + + +def _read_java_versions(project_root): + """Read Java versions from the CI workflow matrix configuration.""" + workflow_path = os.path.join(project_root, '.github', 'workflows', 'maven-build.yml') + with open(workflow_path, 'r', encoding='utf-8') as f: + content = f.read() + match = re.search(r'matrix:\s*\n\s*java:\s*\[([^\]]+)\]', content) + if match: + return [v.strip().strip("'\"") for v in match.group(1).split(',')] + print("WARNING: Could not parse Java versions from matrix in maven-build.yml", file=sys.stderr) + return [] + + +def _read_pom_revision(project_root): + """Read the 'revision' property from the root pom.xml.""" + pom_path = os.path.join(project_root, 'pom.xml') + with open(pom_path, 'r', encoding='utf-8') as f: + content = f.read() + match = re.search(r'([^<]+)', content) + if match: + return match.group(1).strip() + print("WARNING: Could not find property in pom.xml", file=sys.stderr) + return "" + + +def _read_pom_artifact_id(project_root): + """Read the project artifactId from the root pom.xml (outside the block).""" + pom_path = os.path.join(project_root, 'pom.xml') + with open(pom_path, 'r', encoding='utf-8') as f: + content = f.read() + no_parent = re.sub(r'.*?', '', content, flags=re.DOTALL) + match = re.search(r'([^<]+)', no_parent) + if match: + return match.group(1).strip() + print("WARNING: Could not find in pom.xml", file=sys.stderr) + return "" + + +def _read_readme_title(project_root): + """Read the top-level heading from README.md.""" + readme_path = os.path.join(project_root, 'README.md') + with open(readme_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line.startswith('# '): + return line[2:].strip() + print("WARNING: Could not find a title heading in README.md", file=sys.stderr) + return "" + + +MODULES = _discover_modules(PROJECT_ROOT) +JAVA_VERSIONS = _read_java_versions(PROJECT_ROOT) +PROJECT_VERSION = _read_pom_revision(PROJECT_ROOT) +ARTIFACT_ID = _read_pom_artifact_id(PROJECT_ROOT) +PROJECT_TITLE = _read_readme_title(PROJECT_ROOT) + +# Regex patterns +CLASS_DECL_RE = re.compile( + r'^(?:public\s+)?(?:abstract\s+)?(?:final\s+)?' + r'(?:(?:class|interface|enum|@interface)\s+)' + r'(\w+)' + r'(?:<[^{]*>)?' + r'(?:\s+extends\s+[\w.<>, ]+)?' + r'(?:\s+implements\s+[\w.<>, ]+)?', + re.MULTILINE, +) + +PACKAGE_RE = re.compile(r'^\s*package\s+([\w.]+)\s*;', re.MULTILINE) + +JAVADOC_BLOCK_RE = re.compile(r'/\*\*(.*?)\*/', re.DOTALL) + +SINCE_TAG_RE = re.compile(r'@since\s+(.+?)(?:\n|\r|$)') +AUTHOR_TAG_RE = re.compile(r'@author\s+(.+?)(?:\n|\r|$)') +SEE_TAG_RE = re.compile(r'@see\s+(.+?)(?:\n|\r|$)') +PARAM_TAG_RE = re.compile(r'@param\s+(\S+)\s+(.*?)(?=@\w|\Z)', re.DOTALL) + +# Matches @Since annotation on a class (not inside Javadoc) +SINCE_ANNOTATION_RE = re.compile(r'@Since\s*\(\s*(?:value\s*=\s*)?["\']([^"\']+)["\']\s*\)') + +# Code example blocks in Javadoc +CODE_EXAMPLE_RE = re.compile(r'
\s*\{@code\s*(.*?)\}
', re.DOTALL) +CODE_EXAMPLE_ALT_RE = re.compile(r'
\s*(.*?)
', re.DOTALL) + +# HTML tags used in Javadoc +LINK_TAG_RE = re.compile(r'\{@link\s+([^}]+)\}') +CODE_TAG_RE = re.compile(r'\{@code\s+([^}]+)\}') +LINKPLAIN_TAG_RE = re.compile(r'\{@linkplain\s+([^}]+)\}') +VALUE_TAG_RE = re.compile(r'\{@value\s+([^}]+)\}') + +# Method/field signatures +PUBLIC_METHOD_RE = re.compile( + r'(?:/\*\*(.*?)\*/\s*)?' + r'(?:@\w+(?:\([^)]*\))?\s*)*' + r'(public\s+(?:static\s+)?(?:final\s+)?(?:synchronized\s+)?' + r'(?:<[^>]+>\s+)?' + r'\S+\s+' # return type + r'(\w+)\s*' # method name + r'\([^)]*\))', # parameters + re.DOTALL, +) + + +# ────────────────────────────────────────────── +# Javadoc Parsing Utilities +# ────────────────────────────────────────────── + +def clean_javadoc_line(line): + """Remove leading whitespace, asterisks, and extra spaces from a Javadoc line.""" + line = line.strip() + if line.startswith('*'): + line = line[1:] + if line.startswith(' '): + line = line[1:] + return line + + +def parse_javadoc(javadoc_text): + """Parse a Javadoc comment block into structured components.""" + if not javadoc_text: + return { + "description": "", + "since": "", + "author": "", + "see": [], + "params": [], + "examples": [], + } + + lines = javadoc_text.split('\n') + cleaned_lines = [clean_javadoc_line(line) for line in lines] + full_text = '\n'.join(cleaned_lines) + + # Extract tags + since_match = SINCE_TAG_RE.search(full_text) + since = since_match.group(1).strip() if since_match else "" + + author_match = AUTHOR_TAG_RE.search(full_text) + author = author_match.group(1).strip() if author_match else "" + # Clean HTML from author + author = re.sub(r'<[^>]+>', '', author).strip() + + see_matches = SEE_TAG_RE.findall(full_text) + see_refs = [s.strip() for s in see_matches] + + # Extract description (text before any @tag) + desc_lines = [] + for line in cleaned_lines: + stripped = line.strip() + if stripped.startswith('@'): + break + desc_lines.append(line) + description = '\n'.join(desc_lines).strip() + + # Extract code examples + examples = [] + for match in CODE_EXAMPLE_RE.finditer(javadoc_text): + code = match.group(1).strip() + # Clean Javadoc asterisks from code lines + code_lines = code.split('\n') + cleaned_code = '\n'.join(clean_javadoc_line(l) for l in code_lines) + examples.append(cleaned_code.strip()) + + if not examples: + for match in CODE_EXAMPLE_ALT_RE.finditer(javadoc_text): + code = match.group(1).strip() + if '{@code' not in code and len(code) > 10: + code_lines = code.split('\n') + cleaned_code = '\n'.join(clean_javadoc_line(l) for l in code_lines) + examples.append(cleaned_code.strip()) + + return { + "description": description, + "since": since, + "author": author, + "see": see_refs, + "params": [], + "examples": examples, + } + + +def convert_javadoc_to_markdown(text): + """Convert Javadoc HTML/tags to Markdown.""" + if not text: + return "" + + # Convert {@link ...} to `...` + text = LINK_TAG_RE.sub(r'`\1`', text) + text = LINKPLAIN_TAG_RE.sub(r'`\1`', text) + text = CODE_TAG_RE.sub(r'`\1`', text) + text = VALUE_TAG_RE.sub(r'`\1`', text) + + # Convert basic HTML + text = re.sub(r'', '\n\n', text) + text = re.sub(r'

', '', text) + text = re.sub(r'', '\n', text) + text = re.sub(r'(.*?)', r'**\1**', text) + text = re.sub(r'(.*?)', r'*\1*', text) + text = re.sub(r'(.*?)', r'*\1*', text) + text = re.sub(r'(.*?)', r'**\1**', text) + text = re.sub(r'(.*?)', r'`\1`', text) + text = re.sub(r'

(.*?)

', r'### \1', text) + text = re.sub(r'

(.*?)

', r'#### \1', text) + text = re.sub(r'
    ', '', text) + text = re.sub(r'
', '', text) + text = re.sub(r'
  • (.*?)
  • ', r'- \1', text, flags=re.DOTALL) + text = re.sub(r'
  • ', '- ', text) + + # Remove remaining HTML tags (except
    )
    +    text = re.sub(r'<(?!pre|/pre)[^>]+>', '', text)
    +
    +    return text.strip()
    +
    +
    +# ──────────────────────────────────────────────
    +# Java Source File Parser
    +# ──────────────────────────────────────────────
    +
    +class JavaComponent:
    +    """Represents a parsed Java component (class, interface, enum, annotation)."""
    +
    +    def __init__(self):
    +        self.name = ""
    +        self.package = ""
    +        self.module = ""
    +        self.component_type = ""  # class, interface, enum, annotation
    +        self.description = ""
    +        self.since_version = ""
    +        self.author = ""
    +        self.see_refs = []
    +        self.examples = []
    +        self.extends_class = ""
    +        self.implements_interfaces = []
    +        self.declaration_line = ""
    +        self.public_methods = []
    +        self.source_path = ""
    +
    +    @property
    +    def fully_qualified_name(self):
    +        if self.package:
    +            return f"{self.package}.{self.name}"
    +        return self.name
    +
    +    @property
    +    def wiki_page_name(self):
    +        """Generate a wiki-friendly page name."""
    +        return self.fully_qualified_name.replace('.', '-')
    +
    +
    +class JavaMethod:
    +    """Represents a parsed public method."""
    +
    +    def __init__(self):
    +        self.name = ""
    +        self.signature = ""
    +        self.description = ""
    +        self.since_version = ""
    +        self.examples = []
    +        self.params = []
    +
    +
    +def parse_java_file(filepath, module_name):
    +    """Parse a Java source file and extract component information."""
    +    try:
    +        with open(filepath, 'r', encoding='utf-8') as f:
    +            content = f.read()
    +    except (IOError, UnicodeDecodeError):
    +        return None
    +
    +    # Extract package
    +    pkg_match = PACKAGE_RE.search(content)
    +    package_name = pkg_match.group(1) if pkg_match else ""
    +
    +    # Check for package-info.java
    +    if os.path.basename(filepath) == 'package-info.java':
    +        return None
    +
    +    # Find the class/interface/enum/annotation declaration
    +    # First, find the Javadoc that precedes the class declaration
    +    class_javadoc = None
    +    class_decl_match = None
    +
    +    # Strategy: find all Javadoc blocks and the class declaration
    +    javadoc_blocks = list(JAVADOC_BLOCK_RE.finditer(content))
    +
    +    # Find the main type declaration
    +    for line in content.split('\n'):
    +        stripped = line.strip()
    +        if re.match(r'(?:public\s+)?(?:abstract\s+)?(?:final\s+)?(?:class|interface|enum|@interface)\s+', stripped):
    +            class_decl_match = stripped
    +            break
    +
    +    if not class_decl_match:
    +        return None
    +
    +    # Determine component type (check @interface before interface)
    +    comp_type = "class"
    +    if re.search(r'@interface\s+', class_decl_match):
    +        comp_type = "annotation"
    +    elif re.search(r'\binterface\s+', class_decl_match):
    +        comp_type = "interface"
    +    elif re.search(r'\benum\s+', class_decl_match):
    +        comp_type = "enum"
    +
    +    # Extract class name
    +    name_match = re.search(
    +        r'(?:class|interface|enum|@interface)\s+(\w+)', class_decl_match
    +    )
    +    if not name_match:
    +        return None
    +
    +    class_name = name_match.group(1)
    +
    +    # Skip non-public classes, inner classes, and module-info
    +    if 'public' not in class_decl_match and comp_type != "annotation":
    +        # Check if the file name matches the class name (top-level class)
    +        file_basename = os.path.splitext(os.path.basename(filepath))[0]
    +        if file_basename != class_name:
    +            return None
    +
    +    if class_name in ('module-info', 'package-info'):
    +        return None
    +
    +    # Find the class-level Javadoc (the last Javadoc before the class declaration)
    +    class_decl_pos = content.find(class_decl_match)
    +    for jd_block in reversed(javadoc_blocks):
    +        if jd_block.end() <= class_decl_pos:
    +            # Verify there's no other declaration between this Javadoc and the class
    +            between = content[jd_block.end():class_decl_pos].strip()
    +            # Remove annotations between Javadoc and class decl
    +            between_cleaned = re.sub(r'@\w+(?:\([^)]*\))?', '', between).strip()
    +            if not between_cleaned or between_cleaned.startswith('@'):
    +                class_javadoc = jd_block.group(1)
    +                break
    +
    +    # Parse the class Javadoc
    +    javadoc_info = parse_javadoc(class_javadoc)
    +
    +    # Check for @Since annotation on the class (limit search to nearby context)
    +    search_start = max(0, class_decl_pos - 2000)
    +    since_annotation = SINCE_ANNOTATION_RE.search(content[search_start:class_decl_pos + len(class_decl_match)])
    +    annotation_since = since_annotation.group(1) if since_annotation else ""
    +
    +    # Extract extends/implements
    +    extends_match = re.search(r'\bextends\s+([\w.<>, ]+?)(?:\s+implements|\s*\{)', class_decl_match)
    +    extends_class = extends_match.group(1).strip() if extends_match else ""
    +
    +    implements_match = re.search(r'\bimplements\s+([\w.<>, ]+?)(?:\s*\{|$)', class_decl_match)
    +    implements_interfaces = []
    +    if implements_match:
    +        impl_str = implements_match.group(1).strip().rstrip('{').strip()
    +        implements_interfaces = [i.strip() for i in impl_str.split(',') if i.strip()]
    +
    +    # Parse public methods
    +    public_methods = extract_public_methods(content, class_decl_pos)
    +
    +    # Build the component
    +    component = JavaComponent()
    +    component.name = class_name
    +    component.package = package_name
    +    component.module = module_name
    +    component.component_type = comp_type
    +    component.description = javadoc_info["description"]
    +    component.since_version = javadoc_info["since"] or annotation_since
    +    component.author = javadoc_info["author"]
    +    component.see_refs = javadoc_info["see"]
    +    component.examples = javadoc_info["examples"]
    +    component.extends_class = extends_class
    +    component.implements_interfaces = implements_interfaces
    +    component.declaration_line = class_decl_match.rstrip('{').strip()
    +    component.public_methods = public_methods
    +    component.source_path = os.path.relpath(filepath, PROJECT_ROOT)
    +
    +    return component
    +
    +
    +def extract_public_methods(content, class_start_pos):
    +    """Extract public methods from the class body."""
    +    methods = []
    +    # Only look at content after class declaration
    +    body = content[class_start_pos:]
    +
    +    for match in PUBLIC_METHOD_RE.finditer(body):
    +        javadoc_text = match.group(1)
    +        full_signature = match.group(2)
    +        method_name = match.group(3)
    +
    +        # Skip constructors, getters/setters that are trivial
    +        if method_name in ('toString', 'hashCode', 'equals', 'clone'):
    +            continue
    +
    +        method = JavaMethod()
    +        method.name = method_name
    +        method.signature = full_signature.strip()
    +
    +        if javadoc_text:
    +            method_jd = parse_javadoc(javadoc_text)
    +            method.description = method_jd["description"]
    +            method.since_version = method_jd["since"]
    +            method.examples = method_jd["examples"]
    +
    +        methods.append(method)
    +
    +    return methods[:20]  # Limit to 20 methods per class to keep docs manageable
    +
    +
    +# ──────────────────────────────────────────────
    +# Wiki Page Generator
    +# ──────────────────────────────────────────────
    +
    +def generate_wiki_page(component):
    +    """Generate a Markdown wiki page for a Java component."""
    +    lines = []
    +
    +    # Title
    +    type_label = component.component_type.capitalize()
    +    lines.append(f"# {component.name}")
    +    lines.append("")
    +
    +    # Metadata badge line
    +    badges = []
    +    badges.append(f"**Type:** `{type_label}`")
    +    badges.append(f"**Module:** `{component.module}`")
    +    badges.append(f"**Package:** `{component.package}`")
    +    if component.since_version:
    +        badges.append(f"**Since:** `{component.since_version}`")
    +    lines.append(" | ".join(badges))
    +    lines.append("")
    +
    +    # Source link
    +    lines.append(f"> **Source:** [`{component.source_path}`]"
    +                 f"(https://github.com/microsphere-projects/{ARTIFACT_ID}/blob/main/{component.source_path})")
    +    lines.append("")
    +
    +    # ── Overview ──
    +    lines.append("## Overview")
    +    lines.append("")
    +    if component.description:
    +        desc_md = convert_javadoc_to_markdown(component.description)
    +        lines.append(desc_md)
    +    else:
    +        lines.append(f"`{component.name}` is a {type_label.lower()} in the "
    +                     f"`{component.package}` package of the `{component.module}` module.")
    +    lines.append("")
    +
    +    # Declaration
    +    lines.append("### Declaration")
    +    lines.append("")
    +    lines.append("```java")
    +    lines.append(component.declaration_line)
    +    lines.append("```")
    +    lines.append("")
    +
    +    # ── Author ──
    +    if component.author:
    +        lines.append(f"**Author:** {component.author}")
    +        lines.append("")
    +
    +    # ── Since / Version Info ──
    +    lines.append("## Version Information")
    +    lines.append("")
    +    if component.since_version:
    +        lines.append(f"- **Introduced in:** `{component.since_version}`")
    +    else:
    +        lines.append(f"- **Introduced in:** `{PROJECT_VERSION}` (current)")
    +    lines.append(f"- **Current Project Version:** `{PROJECT_VERSION}`")
    +    lines.append("")
    +
    +    # ── Version Compatibility ──
    +    lines.append("## Version Compatibility")
    +    lines.append("")
    +    lines.append("This component is tested and compatible with the following Java versions:")
    +    lines.append("")
    +    lines.append("| Java Version | Status |")
    +    lines.append("|:---:|:---:|")
    +    for v in JAVA_VERSIONS:
    +        lines.append(f"| Java {v} | ✅ Compatible |")
    +    lines.append("")
    +
    +    # ── Examples ──
    +    has_examples = bool(component.examples)
    +    if not has_examples:
    +        # Check methods for examples
    +        for method in component.public_methods:
    +            if method.examples:
    +                has_examples = True
    +                break
    +
    +    if has_examples:
    +        lines.append("## Examples")
    +        lines.append("")
    +
    +        if component.examples:
    +            for i, example in enumerate(component.examples, 1):
    +                if len(component.examples) > 1:
    +                    lines.append(f"### Example {i}")
    +                    lines.append("")
    +                lines.append("```java")
    +                lines.append(example)
    +                lines.append("```")
    +                lines.append("")
    +
    +        # Method-level examples
    +        method_examples_added = False
    +        for method in component.public_methods:
    +            if method.examples:
    +                if not method_examples_added:
    +                    lines.append(f"### Method Examples")
    +                    lines.append("")
    +                    method_examples_added = True
    +                lines.append(f"#### `{method.name}`")
    +                lines.append("")
    +                for example in method.examples:
    +                    lines.append("```java")
    +                    lines.append(example)
    +                    lines.append("```")
    +                    lines.append("")
    +
    +    # ── Usage Guide ──
    +    lines.append("## Usage")
    +    lines.append("")
    +    lines.append("### Maven Dependency")
    +    lines.append("")
    +    lines.append("Add the following dependency to your `pom.xml`:")
    +    lines.append("")
    +    lines.append("```xml")
    +    lines.append("")
    +    lines.append("    io.github.microsphere-projects")
    +    lines.append(f"    {component.module}")
    +    lines.append(f"    ${{{ARTIFACT_ID}.version}}")
    +    lines.append("")
    +    lines.append("```")
    +    lines.append("")
    +    lines.append(f"> **Tip:** Use the BOM (`{ARTIFACT_ID}-dependencies`) for consistent version management. "
    +                 f"See the [Getting Started](https://github.com/microsphere-projects/{ARTIFACT_ID}#getting-started) guide.")
    +    lines.append("")
    +
    +    # ── Import ──
    +    lines.append("### Import")
    +    lines.append("")
    +    lines.append("```java")
    +    lines.append(f"import {component.fully_qualified_name};")
    +    lines.append("```")
    +    lines.append("")
    +
    +    # ── Public API ──
    +    if component.public_methods:
    +        lines.append("## API Reference")
    +        lines.append("")
    +        lines.append("### Public Methods")
    +        lines.append("")
    +        lines.append("| Method | Description |")
    +        lines.append("|--------|-------------|")
    +        for method in component.public_methods:
    +            desc = method.description.split('\n')[0] if method.description else ""
    +            desc = convert_javadoc_to_markdown(desc)
    +            # Truncate long descriptions for the table
    +            if len(desc) > 120:
    +                desc = desc[:117] + "..."
    +            sig = method.signature.replace('|', '\\|')
    +            lines.append(f"| `{method.name}` | {desc} |")
    +        lines.append("")
    +
    +        # Detailed method descriptions
    +        has_detailed_methods = any(
    +            m.description and len(m.description) > 50 for m in component.public_methods
    +        )
    +        if has_detailed_methods:
    +            lines.append("### Method Details")
    +            lines.append("")
    +            for method in component.public_methods:
    +                if method.description and len(method.description) > 50:
    +                    lines.append(f"#### `{method.name}`")
    +                    lines.append("")
    +                    lines.append(f"```java")
    +                    lines.append(method.signature)
    +                    lines.append("```")
    +                    lines.append("")
    +                    desc_md = convert_javadoc_to_markdown(method.description)
    +                    lines.append(desc_md)
    +                    lines.append("")
    +                    if method.since_version:
    +                        lines.append(f"*Since: {method.since_version}*")
    +                        lines.append("")
    +
    +    # ── See Also ──
    +    if component.see_refs:
    +        lines.append("## See Also")
    +        lines.append("")
    +        for ref in component.see_refs:
    +            ref_clean = ref.strip()
    +            if ref_clean:
    +                lines.append(f"- `{ref_clean}`")
    +        lines.append("")
    +
    +    # ── Footer ──
    +    lines.append("---")
    +    lines.append("")
    +    lines.append(f"*This documentation was auto-generated from the source code of "
    +                 f"[{ARTIFACT_ID}](https://github.com/microsphere-projects/{ARTIFACT_ID}).*")
    +    lines.append("")
    +
    +    return '\n'.join(lines)
    +
    +
    +def generate_home_page(components_by_module):
    +    """Generate the Home (index) wiki page."""
    +    lines = []
    +    lines.append(f"# {PROJECT_TITLE} - API Documentation")
    +    lines.append("")
    +    lines.append(f"Welcome to the **{PROJECT_TITLE}** wiki! This documentation is auto-generated "
    +                 f"from the project source code and provides detailed information about each Java component.")
    +    lines.append("")
    +    lines.append("## Project Information")
    +    lines.append("")
    +    lines.append(f"- **Current Version:** `{PROJECT_VERSION}`")
    +    lines.append(f"- **Java Compatibility:** {', '.join('Java ' + v for v in JAVA_VERSIONS)}")
    +    lines.append("- **License:** Apache License 2.0")
    +    lines.append(f"- **Repository:** [microsphere-projects/{ARTIFACT_ID}]"
    +                 f"(https://github.com/microsphere-projects/{ARTIFACT_ID})")
    +    lines.append("")
    +
    +    # Table of Contents by module
    +    lines.append("## Modules")
    +    lines.append("")
    +
    +    for module_name, components in components_by_module.items():
    +        lines.append(f"### {module_name}")
    +        lines.append("")
    +
    +        # Group by package
    +        by_package = OrderedDict()
    +        for comp in components:
    +            pkg = comp.package or "(default)"
    +            if pkg not in by_package:
    +                by_package[pkg] = []
    +            by_package[pkg].append(comp)
    +
    +        for pkg, comps in by_package.items():
    +            lines.append(f"**`{pkg}`**")
    +            lines.append("")
    +            for comp in sorted(comps, key=lambda c: c.name):
    +                type_icon = {
    +                    "class": "📦",
    +                    "interface": "🔌",
    +                    "enum": "🔢",
    +                    "annotation": "🏷️",
    +                }.get(comp.component_type, "📄")
    +                wiki_link = comp.wiki_page_name
    +                lines.append(f"- {type_icon} [{comp.name}]({wiki_link}) - "
    +                             f"{comp.component_type.capitalize()}"
    +                             f"{' - Since ' + comp.since_version if comp.since_version else ''}")
    +            lines.append("")
    +
    +    # Quick links
    +    lines.append("## Quick Links")
    +    lines.append("")
    +    lines.append(f"- [Getting Started](https://github.com/microsphere-projects/{ARTIFACT_ID}#getting-started)")
    +    lines.append(f"- [Building from Source](https://github.com/microsphere-projects/{ARTIFACT_ID}#building-from-source)")
    +    lines.append(f"- [Contributing](https://github.com/microsphere-projects/{ARTIFACT_ID}#contributing)")
    +    lines.append("- [JavaDoc](https://javadoc.io/doc/io.github.microsphere-projects)")
    +    lines.append("")
    +    lines.append("---")
    +    lines.append("")
    +    lines.append(f"*This wiki is auto-generated from the source code of "
    +                 f"[{ARTIFACT_ID}](https://github.com/microsphere-projects/{ARTIFACT_ID}). "
    +                 f"To update, trigger the `wiki-publish` workflow.*")
    +    lines.append("")
    +
    +    return '\n'.join(lines)
    +
    +
    +def generate_sidebar(components_by_module):
    +    """Generate the _Sidebar wiki page for navigation."""
    +    lines = []
    +    lines.append("**[Home](Home)**")
    +    lines.append("")
    +
    +    for module_name, components in components_by_module.items():
    +        # Shorten module name for sidebar
    +        short_name = module_name.replace("microsphere-", "")
    +        lines.append(f"**{short_name}**")
    +        lines.append("")
    +        for comp in sorted(components, key=lambda c: c.name):
    +            wiki_link = comp.wiki_page_name
    +            lines.append(f"- [{comp.name}]({wiki_link})")
    +        lines.append("")
    +
    +    return '\n'.join(lines)
    +
    +
    +# ──────────────────────────────────────────────
    +# Main
    +# ──────────────────────────────────────────────
    +
    +def discover_java_files(project_root, modules):
    +    """Discover all main Java source files in the given modules."""
    +    java_files = []
    +    for module in modules:
    +        src_dir = os.path.join(project_root, module, SRC_MAIN_JAVA)
    +        if not os.path.isdir(src_dir):
    +            continue
    +        for root, _dirs, files in os.walk(src_dir):
    +            for fname in files:
    +                if fname.endswith('.java') and fname != 'package-info.java' and fname != 'module-info.java':
    +                    java_files.append((os.path.join(root, fname), module))
    +    return java_files
    +
    +
    +def main():
    +    parser = argparse.ArgumentParser(description=f"Generate wiki documentation for {ARTIFACT_ID}")
    +    parser.add_argument(
    +        "--output", "-o",
    +        default=os.path.join(PROJECT_ROOT, "wiki"),
    +        help="Output directory for generated wiki pages (default: /wiki)",
    +    )
    +    parser.add_argument(
    +        "--project-root",
    +        default=PROJECT_ROOT,
    +        help=f"Root directory of the {ARTIFACT_ID} project",
    +    )
    +    args = parser.parse_args()
    +
    +    project_root = args.project_root
    +    output_dir = args.output
    +
    +    print(f"{PROJECT_TITLE} Wiki Documentation Generator")
    +    print(f"  Project root: {project_root}")
    +    print(f"  Output dir:   {output_dir}")
    +    print()
    +
    +    # Discover Java files
    +    java_files = discover_java_files(project_root, MODULES)
    +    print(f"Found {len(java_files)} Java source files across {len(MODULES)} modules")
    +    print()
    +
    +    # Parse all files
    +    components = []
    +    for filepath, module_name in java_files:
    +        component = parse_java_file(filepath, module_name)
    +        if component:
    +            components.append(component)
    +
    +    print(f"Parsed {len(components)} Java components")
    +    print()
    +
    +    # Group by module
    +    components_by_module = OrderedDict()
    +    for module in MODULES:
    +        module_components = [c for c in components if c.module == module]
    +        if module_components:
    +            components_by_module[module] = sorted(module_components, key=lambda c: (c.package, c.name))
    +
    +    # Create output directory
    +    os.makedirs(output_dir, exist_ok=True)
    +
    +    # Generate individual wiki pages
    +    page_count = 0
    +    for module_name, module_components in components_by_module.items():
    +        for comp in module_components:
    +            page_content = generate_wiki_page(comp)
    +            page_filename = f"{comp.wiki_page_name}.md"
    +            page_path = os.path.join(output_dir, page_filename)
    +            with open(page_path, 'w', encoding='utf-8') as f:
    +                f.write(page_content)
    +            page_count += 1
    +
    +    print(f"Generated {page_count} wiki pages")
    +
    +    # Generate Home page
    +    home_content = generate_home_page(components_by_module)
    +    with open(os.path.join(output_dir, "Home.md"), 'w', encoding='utf-8') as f:
    +        f.write(home_content)
    +    print("Generated Home.md")
    +
    +    # Generate Sidebar
    +    sidebar_content = generate_sidebar(components_by_module)
    +    with open(os.path.join(output_dir, "_Sidebar.md"), 'w', encoding='utf-8') as f:
    +        f.write(sidebar_content)
    +    print("Generated _Sidebar.md")
    +
    +    print()
    +    print(f"Wiki documentation generated successfully in: {output_dir}")
    +    print(f"Total pages: {page_count + 2} ({page_count} components + Home + Sidebar)")
    +
    +    return 0
    +
    +
    +if __name__ == "__main__":
    +    sys.exit(main())
    diff --git a/.github/workflows/maven-build.yml b/.github/workflows/maven-build.yml
    index df4fe003..366b2dc0 100644
    --- a/.github/workflows/maven-build.yml
    +++ b/.github/workflows/maven-build.yml
    @@ -10,39 +10,43 @@ name: Maven Build
     
     on:
       push:
    -    branches: [ 'dev-1.x' ]
    +    branches: [ 'main', 'dev' ]
       pull_request:
    -    branches: [ 'dev-1.x' , 'release-1.x' ]
    +    branches: [ 'main', 'dev' , 'release' ]
     
     jobs:
       build:
         runs-on: ubuntu-latest
         strategy:
           matrix:
    -        java: [ '8', '11' , '17' , '21' ]
    -        maven-profile-spring-cloud: [ 'spring-cloud-hoxton' , 'spring-cloud-2020' , 'spring-cloud-2021' ]
    +        java: [ '17' , '21' , '25' ]
    +        maven-profile-spring-cloud: [ 'spring-cloud-2022' , 'spring-cloud-2023' , 'spring-cloud-2024' , 'spring-cloud-2025' ]
         steps:
           - name: Checkout Source
    -        uses: actions/checkout@v4
    +        uses: actions/checkout@v5
    +
    +      - name: Setup Testcontainers Cloud Client
    +        uses: atomicjar/testcontainers-cloud-setup-action@v1
    +        with:
    +          token: ${{ secrets.TC_CLOUD_TOKEN }}
     
           - name: Setup JDK ${{ matrix.Java }}
    -        uses: actions/setup-java@v4
    +        uses: actions/setup-java@v5
             with:
               distribution: 'temurin'
               java-version: ${{ matrix.java }}
    -          cache: maven
     
           - name: Build with Maven
    -        run: mvn
    +        run: ./mvnw
               --batch-mode
               --update-snapshots
               --file pom.xml
               -Drevision=0.0.1-SNAPSHOT
               test
    -          --activate-profiles test,coverage,${{ matrix.maven-profile-spring-cloud }}
    +          --activate-profiles test,coverage,testcontainers,${{ matrix.maven-profile-spring-cloud }}
     
           - name: Upload coverage reports to Codecov
             uses: codecov/codecov-action@v5
             with:
               token: ${{ secrets.CODECOV_TOKEN }}
    -          slug: microsphere-projects/microsphere-spring-boot
    \ No newline at end of file
    +          slug: microsphere-projects/microsphere-spring-cloud
    \ No newline at end of file
    diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml
    index 241727bb..7b1b44ed 100644
    --- a/.github/workflows/maven-publish.yml
    +++ b/.github/workflows/maven-publish.yml
    @@ -10,26 +10,33 @@ name: Maven Publish
     
     on:
       push:
    -    branches: [ 'release-1.x' ]
    +    branches: [ 'release' ]
       workflow_dispatch:
         inputs:
           revision:
    -        description: 'The version to publish'
    +        description: 'The version to publish for Spring Cloud 2022+ and JDK 17+'
             required: true
    -        default: '0.0.1-SNAPSHOT'
    +        default: '${major}.${minor}.${patch}'
     
     jobs:
       build:
         runs-on: ubuntu-latest
    -    if:  ${{ inputs.revision }}
    +    if: ${{ inputs.revision }}
         steps:
    +      - name: Validate version format
    +        run: |
    +          if ! echo "${{ inputs.revision }}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
    +          echo "Error: version '${{ inputs.revision }}' does not match the required pattern major.minor.patch"
    +          exit 1
    +          fi
    +          
           - name: Checkout Source
    -        uses: actions/checkout@v4
    +        uses: actions/checkout@v5
     
           - name: Setup Maven Central Repository
    -        uses: actions/setup-java@v4
    +        uses: actions/setup-java@v5
             with:
    -          java-version: '11'
    +          java-version: '17'
               distribution: 'temurin'
               server-id: ossrh
               server-username: MAVEN_USERNAME
    @@ -37,10 +44,72 @@ jobs:
               cache: maven
     
           - name: Publish package
    -        run: mvn --batch-mode --update-snapshots -Drevision=${{ inputs.revision }} -Dgpg.skip=true -Prelease,ci clean deploy
    +        run: mvn
    +          --batch-mode
    +          --update-snapshots
    +          --file pom.xml
    +          -Drevision=${{ inputs.revision }}
    +          -Dgpg.skip=true
    +          deploy
    +          --activate-profiles publish,ci
             env:
               MAVEN_USERNAME: ${{ secrets.OSS_SONATYPE_USERNAME }}
               MAVEN_PASSWORD: ${{ secrets.OSS_SONATYPE_PASSWORD }}
    -          SIGN_KEY_ID:  ${{ secrets.OSS_SIGNING_KEY_ID_LONG }}
    +          SIGN_KEY_ID: ${{ secrets.OSS_SIGNING_KEY_ID_LONG }}
               SIGN_KEY: ${{ secrets.OSS_SIGNING_KEY }}
               SIGN_KEY_PASS: ${{ secrets.OSS_SIGNING_PASSWORD }}
    +
    +
    +  release:
    +    runs-on: ubuntu-latest
    +    needs: build
    +    permissions:
    +      contents: write
    +    steps:
    +      - name: Checkout Source
    +        uses: actions/checkout@v5
    +        with:
    +          fetch-depth: 0
    +
    +      - name: Create Tag
    +        run: |
    +          git config user.name "github-actions[bot]"
    +          git config user.email "github-actions[bot]@users.noreply.github.com"
    +          if git rev-parse "${{ inputs.revision }}" >/dev/null 2>&1; then
    +            echo "Tag ${{ inputs.revision }} already exists, skipping."
    +          else
    +            git tag ${{ inputs.revision }}
    +            git push origin ${{ inputs.revision }}
    +          fi
    +
    +      - name: Create Release
    +        run: |
    +          if gh release view "v${{ inputs.revision }}" >/dev/null 2>&1; then
    +            echo "Release v${{ inputs.revision }} already exists, skipping."
    +          else
    +            gh release create v${{ inputs.revision }} \
    +              --title "v${{ inputs.revision }}" \
    +              --generate-notes \
    +              --latest
    +          fi
    +        env:
    +          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    +
    +      - name: Increment patch version
    +        run: |
    +          CURRENT="${{ inputs.revision }}"
    +          MAJOR=$(echo "$CURRENT" | cut -d. -f1)
    +          MINOR=$(echo "$CURRENT" | cut -d. -f2)
    +          PATCH=$(echo "$CURRENT" | cut -d. -f3)
    +          NEXT_PATCH=$((PATCH + 1))
    +          NEXT_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}-SNAPSHOT"
    +          sed -i "s|^\( *\)[^<]*|\1${NEXT_VERSION}|" pom.xml
    +          echo "Bumped version from ${CURRENT} to ${NEXT_VERSION}"
    +
    +      - name: Commit and push next version
    +        run: |
    +          git config user.name "github-actions[bot]"
    +          git config user.email "github-actions[bot]@users.noreply.github.com"
    +          git add pom.xml
    +          git diff --cached --quiet && echo "No changes to commit" || \
    +            git commit -m "chore: bump version to next patch after publishing ${{ inputs.revision }}" && git push
    \ No newline at end of file
    diff --git a/.github/workflows/merge-main-to-branches.yml b/.github/workflows/merge-main-to-branches.yml
    new file mode 100644
    index 00000000..f4715238
    --- /dev/null
    +++ b/.github/workflows/merge-main-to-branches.yml
    @@ -0,0 +1,66 @@
    +# This workflow automatically merges the 'main' branch into 'dev' and 'release' branches
    +# whenever changes are pushed to 'main', without requiring manual confirmation.
    +
    +name: Merge Main to Dev and Release
    +
    +on:
    +  push:
    +    branches:
    +      - main
    +
    +concurrency:
    +  group: merge-main-to-branches
    +  cancel-in-progress: false
    +
    +jobs:
    +  merge-to-dev:
    +    name: Merge main → dev
    +    runs-on: ubuntu-latest
    +    permissions:
    +      contents: write
    +    steps:
    +      - name: Checkout Repository
    +        uses: actions/checkout@v5
    +        with:
    +          fetch-depth: 0
    +          token: ${{ secrets.GITHUB_TOKEN }}
    +
    +      - name: Merge main into dev
    +        run: |
    +          git config user.name "github-actions[bot]"
    +          git config user.email "github-actions[bot]@users.noreply.github.com"
    +          if ! git checkout dev; then
    +            echo "::error::Branch 'dev' does not exist. Skipping merge."
    +            exit 1
    +          fi
    +          if ! git merge --no-ff origin/main -m "chore: merge main into dev [skip ci]"; then
    +            echo "::error::Merge conflict detected when merging main into dev. Manual intervention required."
    +            exit 1
    +          fi
    +          git push origin dev
    +
    +  merge-to-release:
    +    name: Merge main → release
    +    runs-on: ubuntu-latest
    +    permissions:
    +      contents: write
    +    steps:
    +      - name: Checkout Repository
    +        uses: actions/checkout@v5
    +        with:
    +          fetch-depth: 0
    +          token: ${{ secrets.GITHUB_TOKEN }}
    +
    +      - name: Merge main into release
    +        run: |
    +          git config user.name "github-actions[bot]"
    +          git config user.email "github-actions[bot]@users.noreply.github.com"
    +          if ! git checkout release; then
    +            echo "::error::Branch 'release' does not exist. Skipping merge."
    +            exit 1
    +          fi
    +          if ! git merge --no-ff origin/main -m "chore: merge main into release [skip ci]"; then
    +            echo "::error::Merge conflict detected when merging main into release. Manual intervention required."
    +            exit 1
    +          fi
    +          git push origin release
    diff --git a/.github/workflows/wiki-publish.yml b/.github/workflows/wiki-publish.yml
    new file mode 100644
    index 00000000..53f78fb6
    --- /dev/null
    +++ b/.github/workflows/wiki-publish.yml
    @@ -0,0 +1,79 @@
    +name: Generate and Publish Wiki Documentation
    +
    +on:
    +  push:
    +    branches: [ main ]
    +    paths:
    +      - '*/src/main/java/**/*.java'
    +      - '.github/scripts/generate-wiki-docs.py'
    +      - '.github/workflows/wiki-publish.yml'
    +  workflow_dispatch:
    +
    +permissions:
    +  contents: write
    +
    +jobs:
    +  generate-wiki:
    +    name: Generate Wiki Documentation
    +    runs-on: ubuntu-latest
    +
    +    steps:
    +      - name: Checkout source repository
    +        uses: actions/checkout@v5
    +        with:
    +          fetch-depth: 1
    +
    +      - name: Set up Python
    +        uses: actions/setup-python@v5
    +        with:
    +          python-version: '3.x'
    +
    +      - name: Generate wiki pages
    +        run: |
    +          python .github/scripts/generate-wiki-docs.py --output wiki-output
    +          echo "Generated wiki pages:"
    +          ls -la wiki-output/ | head -20
    +          echo "Total pages: $(ls wiki-output/*.md | wc -l)"
    +
    +      - name: Checkout wiki repository
    +        uses: actions/checkout@v5
    +        with:
    +          repository: ${{ github.repository }}.wiki
    +          path: wiki-repo
    +          token: ${{ secrets.GITHUB_TOKEN }}
    +        continue-on-error: true
    +
    +      - name: Initialize wiki repository if needed
    +        run: |
    +          if [ ! -d "wiki-repo/.git" ]; then
    +            echo "Wiki repository not found. Creating initial wiki structure..."
    +            mkdir -p wiki-repo
    +            cd wiki-repo
    +            git init
    +            git remote add origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.wiki.git"
    +          fi
    +
    +      - name: Copy generated pages to wiki
    +        run: |
    +          # Remove all existing markdown files from wiki repo to ensure
    +          # pages for renamed or deleted classes are properly cleaned up
    +          find wiki-repo -maxdepth 1 -name '*.md' -type f -delete 2>/dev/null || true
    +          # Copy all generated markdown files to the wiki repo
    +          cp wiki-output/*.md wiki-repo/
    +          echo "Copied wiki pages to wiki repository"
    +          ls -la wiki-repo/*.md | head -20
    +
    +      - name: Push to wiki
    +        run: |
    +          cd wiki-repo
    +          git config user.name "github-actions[bot]"
    +          git config user.email "github-actions[bot]@users.noreply.github.com"
    +          git add -A
    +          if git diff --cached --quiet; then
    +            echo "No changes to wiki documentation"
    +          else
    +            TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
    +            git commit -m "Update wiki documentation - ${TIMESTAMP}"
    +            git push origin HEAD:master || git push origin HEAD:main
    +            echo "Wiki documentation updated successfully"
    +          fi
    diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar
    deleted file mode 100755
    index bf82ff01..00000000
    Binary files a/.mvn/wrapper/maven-wrapper.jar and /dev/null differ
    diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
    index d83cf136..423c23e5 100644
    --- a/.mvn/wrapper/maven-wrapper.properties
    +++ b/.mvn/wrapper/maven-wrapper.properties
    @@ -1,16 +1,3 @@
    -# Copyright 2013-2023 the original author or authors.
    -#
    -# Licensed under the Apache License, Version 2.0 (the "License");
    -# you may not use this file except in compliance with the License.
    -# You may obtain a copy of the License at
    -#
    -#      https://www.apache.org/licenses/LICENSE-2.0
    -#
    -# Unless required by applicable law or agreed to in writing, software
    -# distributed under the License is distributed on an "AS IS" BASIS,
    -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    -# See the License for the specific language governing permissions and
    -# limitations under the License.
    -
    -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.0/apache-maven-3.9.0-bin.zip
    -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
    \ No newline at end of file
    +wrapperVersion=3.3.4
    +distributionType=only-script
    +distributionUrl=https://maven.aliyun.com/repository/public/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
    diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
    new file mode 100644
    index 00000000..d57aa809
    --- /dev/null
    +++ b/CODE_OF_CONDUCT.md
    @@ -0,0 +1,48 @@
    +# Contributor Code of Conduct
    +
    +As contributors and maintainers of this project, and in the interest of
    +fostering an open and welcoming community, we pledge to respect all people who
    +contribute through reporting issues, posting feature requests, updating
    +documentation, submitting pull requests or patches, and other activities.
    +
    +We are committed to making participation in this project a harassment-free
    +experience for everyone, regardless of level of experience, gender, gender
    +identity and expression, sexual orientation, disability, personal appearance,
    +body size, race, ethnicity, age, religion, or nationality.
    +
    +Examples of unacceptable behavior by participants include:
    +
    +* The use of sexualized language or imagery
    +* Personal attacks
    +* Trolling or insulting/derogatory comments
    +* Public or private harassment
    +* Publishing other's private information, such as physical or electronic
    +  addresses, without explicit permission
    +* Other unethical or unprofessional conduct
    +
    +Project maintainers have the right and responsibility to remove, edit, or
    +reject comments, commits, code, wiki edits, issues, and other contributions
    +that are not aligned to this Code of Conduct, or to ban temporarily or
    +permanently any contributor for other behaviors that they deem inappropriate,
    +threatening, offensive, or harmful.
    +
    +By adopting this Code of Conduct, project maintainers commit themselves to
    +fairly and consistently applying these principles to every aspect of managing
    +this project. Project maintainers who do not follow or enforce the Code of
    +Conduct may be permanently removed from the project team.
    +
    +This Code of Conduct applies both within project spaces and in public spaces
    +when an individual is representing the project or its community.
    +
    +Instances of abusive, harassing, or otherwise unacceptable behavior may be
    +reported by contacting a project maintainer at [mercyblitz@gmail.com](mailto:mercyblitz@gmail.com). All
    +complaints will be reviewed and investigated and will result in a response that
    +is deemed necessary and appropriate to the circumstances. Maintainers are
    +obligated to maintain confidentiality with regard to the reporter of an
    +incident.
    +
    +
    +This Code of Conduct is adapted from the [Contributor Covenant][homepage],
    +version 1.3.0, available at https://www.contributor-covenant.org/version/1/3/0/code-of-conduct.html
    +
    +[homepage]: https://www.contributor-covenant.org
    \ No newline at end of file
    diff --git a/README.md b/README.md
    index e2791caa..eae413cb 100644
    --- a/README.md
    +++ b/README.md
    @@ -1,2 +1,143 @@
    -# microsphere-spring-cloud
    -Microsphere Projects for Spring Cloud
    +# Microsphere Spring Cloud
    +
    +> Microsphere Projects for Spring Cloud
    +
    +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/microsphere-projects/microsphere-spring-cloud)
    +[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/microsphere-projects/microsphere-spring-cloud)
    +[![Maven Build](https://github.com/microsphere-projects/microsphere-spring-cloud/actions/workflows/maven-build.yml/badge.svg)](https://github.com/microsphere-projects/microsphere-spring-cloud/actions/workflows/maven-build.yml)
    +[![Codecov](https://codecov.io/gh/microsphere-projects/microsphere-spring-cloud/branch/main/graph/badge.svg)](https://app.codecov.io/gh/microsphere-projects/microsphere-spring-cloud)
    +![Maven](https://img.shields.io/maven-central/v/io.github.microsphere-projects/microsphere-spring-cloud.svg)
    +![License](https://img.shields.io/github/license/microsphere-projects/microsphere-spring-cloud.svg)
    +
    +
    +Microsphere Spring Cloud is an extension library for Spring Cloud that enhances and optimizes its capabilities,
    +particularly focused on providing dynamic runtime configuration changes without application restarts. It's designed to
    +solve common pain points when working with distributed systems in Spring Cloud.
    +
    +## Purpose and Scope
    +
    +Microsphere Spring Cloud is a comprehensive extension framework that enhances Spring Cloud applications with advanced
    +service registration capabilities, dynamic OpenFeign client configuration, and fault tolerance features. This project
    +provides production-ready enhancements to the core Spring Cloud ecosystem, focusing on operational reliability and
    +dynamic configuration management.
    +
    +The framework supports multiple Spring Cloud versions (2022.x, 2023.x, 2024.x and 2025.x) and integrates seamlessly with
    +various
    +service discovery systems including Nacos, Eureka, Consul, and Zookeeper. For detailed information about specific
    +subsystems, see Project Structure, Service Registration System, OpenFeign Auto-Refresh System, and Fault Tolerance.
    +
    +## Modules
    +
    +| **Module**                                | **Purpose**                                                                         |
    +|-------------------------------------------|-------------------------------------------------------------------------------------|
    +| **microsphere-spring-cloud-parent**       | Defines the parent POM with dependency management and Spring Cloud version profiles |
    +| **microsphere-spring-cloud-dependencies** | Centralizes dependency management for all project modules                           |
    +| **microsphere-spring-cloud-commons**      | Common utilities for service discovery, registry, and fault tolerance               |
    +| **microsphere-spring-cloud-openfeign**    | Extensions for Spring Cloud OpenFeign with auto-refresh capabilities                |
    +
    +## Getting Started
    +
    +The easiest way to get started is by adding the Microsphere Spring Cloud BOM (Bill of Materials) to your project's
    +pom.xml:
    +
    +```xml
    +
    +    
    +        ...
    +        
    +        
    +            io.github.microsphere-projects
    +            microsphere-spring-cloud-dependencies
    +            ${microsphere-spring-cloud.version}
    +            pom
    +            import
    +        
    +        ...
    +    
    +
    +```
    +
    +`${microsphere-spring-boot.version}` has two branches:
    +
    +| **Branches** | **Purpose**                                      | **Latest Version** |
    +|--------------|--------------------------------------------------|--------------------|
    +| **0.2.x**    | Compatible with Spring Cloud 2022.0.x - 2025.0.x | 0.2.5              |
    +| **0.1.x**    | Compatible with Spring Cloud Hoxton - 2021.0.x   | 0.1.5              |
    +
    +Then add the specific modules you need:
    +
    +```xml
    +
    +    
    +    
    +        io.github.microsphere-projects
    +        microsphere-spring-cloud-commons
    +    
    +
    +    
    +    
    +        io.github.microsphere-projects
    +        microsphere-spring-cloud-openfeign
    +    
    +
    +```
    +
    +## Building from Source
    +
    +You don't need to build from source unless you want to try out the latest code or contribute to the project.
    +
    +To build the project, follow these steps:
    +
    +1. Clone the repository:
    +
    +```bash
    +git clone https://github.com/microsphere-projects/microsphere-spring-cloud.git
    +```
    +
    +2. Build the source:
    +
    +- Linux/MacOS:
    +
    +```bash
    +./mvnw package
    +```
    +
    +- Windows:
    +
    +```powershell
    +mvnw.cmd package
    +```
    +
    +## Contributing
    +
    +We welcome your contributions! Please read [Code of Conduct](./CODE_OF_CONDUCT.md) before submitting a pull request.
    +
    +## Reporting Issues
    +
    +* Before you log a bug, please search
    +  the [issues](https://github.com/microsphere-projects/microsphere-spring-cloud/issues)
    +  to see if someone has already reported the problem.
    +* If the issue doesn't already
    +  exist, [create a new issue](https://github.com/microsphere-projects/microsphere-spring-cloud/issues/new).
    +* Please provide as much information as possible with the issue report.
    +
    +## Documentation
    +
    +### User Guide
    +
    +[DeepWiki Host](https://deepwiki.com/microsphere-projects/microsphere-spring-cloud)
    +
    +[ZRead Host](https://zread.ai/microsphere-projects/microsphere-spring-cloud)
    +
    +### Wiki
    +
    +[Github Host](https://github.com/microsphere-projects/microsphere-spring-cloud/wiki)
    +
    +### JavaDoc
    +
    +- [microsphere-spring-cloud-commons](https://javadoc.io/doc/io.github.microsphere-projects/microsphere-spring-cloud-commons)
    +- [microsphere-spring-cloud-openfeign](https://javadoc.io/doc/io.github.microsphere-projects/microsphere-spring-cloud-openfeign)
    +
    +## License
    +
    +The Microsphere Spring is released under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).
    diff --git a/microsphere-spring-cloud-commons/pom.xml b/microsphere-spring-cloud-commons/pom.xml
    index bcc2a392..be5392c8 100644
    --- a/microsphere-spring-cloud-commons/pom.xml
    +++ b/microsphere-spring-cloud-commons/pom.xml
    @@ -67,12 +67,57 @@
                 true
             
     
    +        
    +            org.springframework.cloud
    +            spring-cloud-loadbalancer
    +            true
    +        
    +
    +        
    +        
    +            org.springframework.cloud
    +            spring-cloud-netflix-eureka-client
    +            true
    +        
    +
    +        
    +            com.netflix.eureka
    +            eureka-client
    +            true
    +        
    +
    +        
    +        
    +            com.alibaba.cloud
    +            spring-cloud-starter-alibaba-nacos-discovery
    +            true
    +        
    +
    +        
    +        
    +            org.springframework.cloud
    +            spring-cloud-starter-zookeeper-discovery
    +            true
    +        
    +
    +        
    +        
    +            org.springframework.cloud
    +            spring-cloud-starter-consul-discovery
    +            true
    +        
    +
             
             
                 io.github.microsphere-projects
                 microsphere-spring-boot-core
             
     
    +        
    +            io.github.microsphere-projects
    +            microsphere-spring-boot-actuator
    +        
    +
             
                 io.github.microsphere-projects
                 microsphere-spring-webmvc
    @@ -92,31 +137,24 @@
                 test
             
     
    -        
    -        
    -            org.testcontainers
    -            junit-jupiter
    -            test
    -        
    -
    -        
    -        
    -            org.springframework.cloud
    -            spring-cloud-netflix-eureka-client
    -            test
    -        
    -
    +        
             
    -            com.netflix.eureka
    -            eureka-client
    +            io.github.microsphere-projects
    +            microsphere-spring-test
                 test
             
     
    -        
    +        
             
    -            com.alibaba.cloud
    -            spring-cloud-starter-alibaba-nacos-discovery
    +            org.testcontainers
    +            testcontainers-junit-jupiter
                 test
    +            
    +                
    +                    junit
    +                    junit
    +                
    +            
             
     
         
    diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/condition/ConditionalOnFeaturesEnabled.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/condition/ConditionalOnFeaturesEnabled.java
    index 2509dd8a..d7058b9b 100644
    --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/condition/ConditionalOnFeaturesEnabled.java
    +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/condition/ConditionalOnFeaturesEnabled.java
    @@ -21,15 +21,15 @@
     import org.springframework.cloud.client.CommonsClientAutoConfiguration;
     import org.springframework.cloud.client.actuator.FeaturesEndpoint;
     import org.springframework.cloud.client.actuator.HasFeatures;
    -import org.springframework.core.annotation.AliasFor;
     
     import java.lang.annotation.Documented;
    -import java.lang.annotation.ElementType;
     import java.lang.annotation.Retention;
    -import java.lang.annotation.RetentionPolicy;
     import java.lang.annotation.Target;
     
     import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.FEATURES_ENABLED_PROPERTY_NAME;
    +import static java.lang.annotation.ElementType.METHOD;
    +import static java.lang.annotation.ElementType.TYPE;
    +import static java.lang.annotation.RetentionPolicy.RUNTIME;
     
     /**
      * The conditional annotation meta-annotates {@link ConditionalOnProperty @ConditionalOnProperty} for
    @@ -43,18 +43,9 @@
      * @see ConditionalOnProperty
      * @since 1.0.0
      */
    -@Retention(RetentionPolicy.RUNTIME)
    -@Target({ElementType.TYPE, ElementType.METHOD})
    +@Retention(RUNTIME)
    +@Target({TYPE, METHOD})
     @Documented
    -@ConditionalOnProperty(name = FEATURES_ENABLED_PROPERTY_NAME)
    +@ConditionalOnProperty(name = FEATURES_ENABLED_PROPERTY_NAME, matchIfMissing = true)
     public @interface ConditionalOnFeaturesEnabled {
    -
    -    /**
    -     * Specify if the condition should match if the property is not set. Defaults to
    -     * {@code true}.
    -     *
    -     * @return if the condition should match if the property is missing
    -     */
    -    @AliasFor(annotation = ConditionalOnProperty.class, attribute = "matchIfMissing")
    -    boolean matchIfMissing() default true;
     }
    diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/ReactiveDiscoveryClientAdapter.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/ReactiveDiscoveryClientAdapter.java
    new file mode 100644
    index 00000000..73ba2fe2
    --- /dev/null
    +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/ReactiveDiscoveryClientAdapter.java
    @@ -0,0 +1,115 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *     http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package io.microsphere.spring.cloud.client.discovery;
    +
    +import org.springframework.cloud.client.ServiceInstance;
    +import org.springframework.cloud.client.discovery.DiscoveryClient;
    +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
    +import reactor.core.publisher.Flux;
    +import reactor.core.publisher.Mono;
    +
    +import java.util.List;
    +
    +import static io.microsphere.lang.function.ThrowableSupplier.execute;
    +import static reactor.core.scheduler.Schedulers.isInNonBlockingThread;
    +
    +/**
    + * An adapter {@link DiscoveryClient} class based on {@link ReactiveDiscoveryClient}
    + *
    + * @author Mercy
    + * @see DiscoveryClient
    + * @since 1.0.0
    + */
    +public class ReactiveDiscoveryClientAdapter implements DiscoveryClient {
    +
    +    private final ReactiveDiscoveryClient reactiveDiscoveryClient;
    +
    +    /**
    +     * Create a new {@link ReactiveDiscoveryClientAdapter} that wraps the given
    +     * {@link ReactiveDiscoveryClient} as a blocking {@link DiscoveryClient}.
    +     *
    +     * 

    Example Usage: + *

    {@code
    +     * ReactiveDiscoveryClient reactiveClient = new SimpleReactiveDiscoveryClient(properties);
    +     * DiscoveryClient adapter = new ReactiveDiscoveryClientAdapter(reactiveClient);
    +     * List services = adapter.getServices();
    +     * }
    + * + * @param reactiveDiscoveryClient the {@link ReactiveDiscoveryClient} to adapt, must not be {@code null} + */ + public ReactiveDiscoveryClientAdapter(ReactiveDiscoveryClient reactiveDiscoveryClient) { + this.reactiveDiscoveryClient = reactiveDiscoveryClient; + } + + /** + * {@inheritDoc} + *

    Delegates to the underlying {@link ReactiveDiscoveryClient#description()}. + */ + @Override + public String description() { + return this.reactiveDiscoveryClient.description(); + } + + /** + * {@inheritDoc} + *

    Delegates to {@link ReactiveDiscoveryClient#getInstances(String)} and collects the + * reactive {@link Flux} result into a blocking {@link List}. + */ + @Override + public List getInstances(String serviceId) { + Flux flux = this.reactiveDiscoveryClient.getInstances(serviceId); + return toList(flux); + } + + /** + * {@inheritDoc} + *

    Delegates to {@link ReactiveDiscoveryClient#getServices()} and collects the + * reactive {@link Flux} result into a blocking {@link List}. + */ + @Override + public List getServices() { + Flux flux = this.reactiveDiscoveryClient.getServices(); + return toList(flux); + } + + /** + * {@inheritDoc} + *

    Delegates to {@link ReactiveDiscoveryClient#probe()}. + */ + @Override + public void probe() { + this.reactiveDiscoveryClient.probe(); + } + + /** + * {@inheritDoc} + *

    Delegates to {@link ReactiveDiscoveryClient#getOrder()}. + */ + @Override + public int getOrder() { + return this.reactiveDiscoveryClient.getOrder(); + } + + static List toList(Flux flux) { + Mono> mono = flux.collectList(); + if (isInNonBlockingThread()) { + return execute(() -> mono.toFuture().get()); + } + return mono.block(); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/UnionDiscoveryClient.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/UnionDiscoveryClient.java index bf517416..8b19619c 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/UnionDiscoveryClient.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/UnionDiscoveryClient.java @@ -16,19 +16,23 @@ */ package io.microsphere.spring.cloud.client.discovery; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import static io.microsphere.collection.CollectionUtils.isNotEmpty; import static io.microsphere.reflect.TypeUtils.getClassName; +import static io.microsphere.spring.beans.BeanUtils.getSortedBeans; import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.COMPOSITE_DISCOVERY_CLIENT_CLASS_NAME; /** @@ -38,56 +42,78 @@ * @see CompositeDiscoveryClient * @since 1.0.0 */ -public final class UnionDiscoveryClient implements DiscoveryClient, SmartInitializingSingleton, DisposableBean { +public final class UnionDiscoveryClient implements DiscoveryClient, ApplicationContextAware, SmartInitializingSingleton, DisposableBean { - private final ObjectProvider discoveryClientsProvider; + private ApplicationContext context; private List discoveryClients; - public UnionDiscoveryClient(ObjectProvider discoveryClientsProvider) { - this.discoveryClientsProvider = discoveryClientsProvider; - } - + /** + * {@inheritDoc} + * + * @return the description "Union Discovery Client" + */ @Override public String description() { return "Union Discovery Client"; } + /** + * {@inheritDoc} + *

    Aggregates service instances from all underlying {@link DiscoveryClient DiscoveryClients}. + */ @Override public List getInstances(String serviceId) { List serviceInstances = new LinkedList<>(); List discoveryClients = getDiscoveryClients(); for (DiscoveryClient discoveryClient : discoveryClients) { List instances = discoveryClient.getInstances(serviceId); - if (instances != null && !instances.isEmpty()) { + if (isNotEmpty(instances)) { serviceInstances.addAll(instances); } } return serviceInstances; } + /** + * {@inheritDoc} + *

    Returns a deduplicated union of service names from all underlying {@link DiscoveryClient DiscoveryClients}. + */ @Override public List getServices() { LinkedHashSet services = new LinkedHashSet<>(); List discoveryClients = getDiscoveryClients(); for (DiscoveryClient discoveryClient : discoveryClients) { List serviceForClient = discoveryClient.getServices(); - if (serviceForClient != null) { + if (isNotEmpty(serviceForClient)) { services.addAll(serviceForClient); } } return new ArrayList<>(services); } + /** + * Returns the sorted list of underlying {@link DiscoveryClient DiscoveryClients}, excluding + * {@link CompositeDiscoveryClient} and this instance itself. The list is lazily initialized + * from the {@link ApplicationContext} on first access and cached for subsequent calls. + * + *

    Example Usage: + *

    {@code
    +     * UnionDiscoveryClient unionClient = applicationContext.getBean(UnionDiscoveryClient.class);
    +     * List clients = unionClient.getDiscoveryClients();
    +     * clients.forEach(c -> System.out.println(c.description()));
    +     * }
    + * + * @return an unmodifiable list of {@link DiscoveryClient} instances + */ public List getDiscoveryClients() { List discoveryClients = this.discoveryClients; if (discoveryClients != null) { return discoveryClients; } - discoveryClients = new LinkedList<>(); - - for (DiscoveryClient discoveryClient : discoveryClientsProvider) { + discoveryClients = new ArrayList<>(); + for (DiscoveryClient discoveryClient : getSortedBeans(this.context, DiscoveryClient.class)) { String className = getClassName(discoveryClient.getClass()); if (COMPOSITE_DISCOVERY_CLIENT_CLASS_NAME.equals(className) || this.equals(discoveryClient)) { // excludes CompositeDiscoveryClient and self @@ -95,21 +121,44 @@ public List getDiscoveryClients() { } discoveryClients.add(discoveryClient); } + this.discoveryClients = discoveryClients; return discoveryClients; } + /** + * {@inheritDoc} + * + * @return {@link #HIGHEST_PRECEDENCE} to ensure this client takes priority + */ @Override public int getOrder() { return HIGHEST_PRECEDENCE; } + /** + * {@inheritDoc} + *

    Eagerly initializes the list of {@link DiscoveryClient DiscoveryClients} after all singletons are instantiated. + */ @Override public void afterSingletonsInstantiated() { this.discoveryClients = getDiscoveryClients(); } + /** + * {@inheritDoc} + *

    Clears the cached list of {@link DiscoveryClient DiscoveryClients} on bean destruction. + */ @Override public void destroy() throws Exception { this.discoveryClients.clear(); } -} + + /** + * {@inheritDoc} + *

    Stores the {@link ApplicationContext} used to look up {@link DiscoveryClient} beans. + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.context = applicationContext; + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/DiscoveryClientAutoConfiguration.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/DiscoveryClientAutoConfiguration.java index 25302cba..7fc2c6eb 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/DiscoveryClientAutoConfiguration.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/DiscoveryClientAutoConfiguration.java @@ -16,8 +16,8 @@ */ package io.microsphere.spring.cloud.client.discovery.autoconfigure; +import io.microsphere.annotation.ConfigurationProperty; import io.microsphere.spring.cloud.client.discovery.UnionDiscoveryClient; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -28,11 +28,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import static io.microsphere.annotation.ConfigurationProperty.APPLICATION_SOURCE; import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.COMMONS_CLIENT_AUTO_CONFIGURATION_CLASS_NAME; import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.DISCOVERY_CLIENT_CLASS_NAME; -import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.DISCOVERY_CLIENT_PROPERTY_PREFIX; -import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.MODE_PROPERTY_NAME; -import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.UNION_DISCOVERY_CLIENT_MODE; +import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.MICROSPHERE_SPRING_CLOUD_PROPERTY_NAME_PREFIX; /** * {@link UnionDiscoveryClient} Auto-Configuration Class @@ -53,13 +52,50 @@ }) public class DiscoveryClientAutoConfiguration { + /** + * The property prefix of {@link DiscoveryClient} : "microsphere.spring.cloud.client.discovery." + */ + public static final String DISCOVERY_CLIENT_PROPERTY_PREFIX = MICROSPHERE_SPRING_CLOUD_PROPERTY_NAME_PREFIX + "client.discovery."; + + /** + * The property name of mode : "mode" + */ + public static final String MODE_PROPERTY_NAME = "mode"; + + /** + * The {@link DiscoveryClient} "mode" for {@link UnionDiscoveryClient} : "union" + */ + public static final String UNION_DISCOVERY_CLIENT_MODE = "union"; + + /** + * The property name of DiscoveryClient mode : "microsphere.spring.cloud.client.discovery.mode" + */ + @ConfigurationProperty( + source = APPLICATION_SOURCE + ) + public static final String DISCOVERY_CLIENT_MODE_PROPERTY_NAME = DISCOVERY_CLIENT_PROPERTY_PREFIX + MODE_PROPERTY_NAME; + + @Configuration(proxyBeanMethods = false) - @ConditionalOnProperty(prefix = DISCOVERY_CLIENT_PROPERTY_PREFIX, name = MODE_PROPERTY_NAME, havingValue = UNION_DISCOVERY_CLIENT_MODE) + @ConditionalOnProperty(name = DISCOVERY_CLIENT_MODE_PROPERTY_NAME, havingValue = UNION_DISCOVERY_CLIENT_MODE) public static class UnionConfiguration { + /** + * Creates a {@link UnionDiscoveryClient} bean that aggregates all {@link DiscoveryClient} + * instances in the {@link org.springframework.context.ApplicationContext}. + * + *

    Example Usage: + *

    {@code
    +         * // Activated when microsphere.spring.cloud.client.discovery.mode=union
    +         * UnionDiscoveryClient client = applicationContext.getBean(UnionDiscoveryClient.class);
    +         * List services = client.getServices();
    +         * }
    + * + * @return a new {@link UnionDiscoveryClient} instance + */ @Bean - public UnionDiscoveryClient unionDiscoveryClient(ObjectProvider discoveryClientsProvider) { - return new UnionDiscoveryClient(discoveryClientsProvider); + public UnionDiscoveryClient unionDiscoveryClient() { + return new UnionDiscoveryClient(); } } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/ReactiveDiscoveryClientAutoConfiguration.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/ReactiveDiscoveryClientAutoConfiguration.java new file mode 100644 index 00000000..8cb3a906 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/ReactiveDiscoveryClientAutoConfiguration.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.discovery.autoconfigure; + +import io.microsphere.spring.cloud.client.discovery.ReactiveDiscoveryClientAdapter; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled; +import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; +import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.DISCOVERY_CLIENT_CLASS_NAME; +import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.REACTIVE_COMMONS_CLIENT_AUTO_CONFIGURATION_CLASS_NAME; +import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.REACTIVE_COMPOSITE_DISCOVERY_CLIENT_AUTO_CONFIGURATION_CLASS_NAME; +import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.SIMPLE_REACTIVE_DISCOVERY_CLIENT_AUTO_CONFIGURATION_CLASS_NAME; + +/** + * The Auto-Configuration class for {@link ReactiveDiscoveryClient} + * + * @author Mercy + * @see ReactiveDiscoveryClient + * @see DiscoveryClientAutoConfiguration + * @since 1.0.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(name = { + DISCOVERY_CLIENT_CLASS_NAME +}) +@ConditionalOnDiscoveryEnabled +@ConditionalOnReactiveDiscoveryEnabled +@AutoConfigureBefore(name = { + REACTIVE_COMMONS_CLIENT_AUTO_CONFIGURATION_CLASS_NAME +}) +@AutoConfigureAfter(name = { + SIMPLE_REACTIVE_DISCOVERY_CLIENT_AUTO_CONFIGURATION_CLASS_NAME, + REACTIVE_COMPOSITE_DISCOVERY_CLIENT_AUTO_CONFIGURATION_CLASS_NAME +}) +public class ReactiveDiscoveryClientAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBlockingDiscoveryEnabled + public static class BlockingConfiguration { + + /** + * Creates a {@link ReactiveDiscoveryClientAdapter} bean that adapts a + * {@link ReactiveDiscoveryClient} to the blocking {@link org.springframework.cloud.client.discovery.DiscoveryClient} interface. + * + *

    Example Usage: + *

    {@code
    +         * // Auto-configured when both reactive and blocking discovery are enabled
    +         * DiscoveryClient client = applicationContext.getBean(ReactiveDiscoveryClientAdapter.class);
    +         * List services = client.getServices();
    +         * }
    + * + * @param reactiveDiscoveryClient the {@link ReactiveDiscoveryClient} to adapt + * @return a new {@link ReactiveDiscoveryClientAdapter} instance + */ + @Bean + @ConditionalOnBean(ReactiveDiscoveryClient.class) + public ReactiveDiscoveryClientAdapter reactiveDiscoveryClientAdapter(ReactiveDiscoveryClient reactiveDiscoveryClient) { + return new ReactiveDiscoveryClientAdapter(reactiveDiscoveryClient); + } + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/constants/DiscoveryClientConstants.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/constants/DiscoveryClientConstants.java index a41950f8..5cce42f7 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/constants/DiscoveryClientConstants.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/constants/DiscoveryClientConstants.java @@ -16,12 +16,12 @@ */ package io.microsphere.spring.cloud.client.discovery.constants; -import io.microsphere.spring.cloud.client.discovery.UnionDiscoveryClient; import org.springframework.cloud.client.CommonsClientAutoConfiguration; +import org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient; - -import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.MICROSPHERE_SPRING_CLOUD_PROPERTY_NAME_PREFIX; +import org.springframework.cloud.client.discovery.composite.reactive.ReactiveCompositeDiscoveryClientAutoConfiguration; +import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration; /** * The constants for {@link DiscoveryClient} @@ -32,33 +32,44 @@ public interface DiscoveryClientConstants { /** - * The property prefix of {@link DiscoveryClient} + * The class name of {@link DiscoveryClient} + * + * @see org.springframework.cloud.client.discovery.DiscoveryClient */ - String DISCOVERY_CLIENT_PROPERTY_PREFIX = MICROSPHERE_SPRING_CLOUD_PROPERTY_NAME_PREFIX + "client.discovery"; + String DISCOVERY_CLIENT_CLASS_NAME = "org.springframework.cloud.client.discovery.DiscoveryClient"; /** - * The property name of "mode" + * The class name of {@link CompositeDiscoveryClient} + * + * @see org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient */ - String MODE_PROPERTY_NAME = "mode"; + String COMPOSITE_DISCOVERY_CLIENT_CLASS_NAME = "org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient"; /** - * The {@link DiscoveryClient} "mode" for {@link UnionDiscoveryClient} + * The class name of {@link CommonsClientAutoConfiguration} + * + * @see org.springframework.cloud.client.CommonsClientAutoConfiguration */ - String UNION_DISCOVERY_CLIENT_MODE = "union"; + String COMMONS_CLIENT_AUTO_CONFIGURATION_CLASS_NAME = "org.springframework.cloud.client.CommonsClientAutoConfiguration"; /** - * The class name of {@link DiscoveryClient} + * The class name of {@link ReactiveCommonsClientAutoConfiguration} + * + * @see org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration */ - String DISCOVERY_CLIENT_CLASS_NAME = "org.springframework.cloud.client.discovery.DiscoveryClient"; + String REACTIVE_COMMONS_CLIENT_AUTO_CONFIGURATION_CLASS_NAME = "org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration"; /** - * The class name of {@link CompositeDiscoveryClient} + * The class name of {@link SimpleReactiveDiscoveryClientAutoConfiguration} + * + * @see org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration */ - String COMPOSITE_DISCOVERY_CLIENT_CLASS_NAME = "org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient"; + String SIMPLE_REACTIVE_DISCOVERY_CLIENT_AUTO_CONFIGURATION_CLASS_NAME = "org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration"; /** - * The class name of {@link CommonsClientAutoConfiguration} + * The class name of {@link ReactiveCompositeDiscoveryClientAutoConfiguration} + * + * @see org.springframework.cloud.client.discovery.composite.reactive.ReactiveCompositeDiscoveryClientAutoConfiguration */ - String COMMONS_CLIENT_AUTO_CONFIGURATION_CLASS_NAME = "org.springframework.cloud.client.CommonsClientAutoConfiguration"; - -} + String REACTIVE_COMPOSITE_DISCOVERY_CLIENT_AUTO_CONFIGURATION_CLASS_NAME = "org.springframework.cloud.client.discovery.composite.reactive.ReactiveCompositeDiscoveryClientAutoConfiguration"; +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/util/DiscoveryUtils.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/util/DiscoveryUtils.java new file mode 100644 index 00000000..db3a521f --- /dev/null +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/discovery/util/DiscoveryUtils.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.discovery.util; + +import io.microsphere.annotation.Nonnull; +import io.microsphere.util.Utils; +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryProperties; +import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryProperties; + +import java.util.List; +import java.util.Map; + +import static io.microsphere.reflect.MethodUtils.invokeMethod; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.setProperties; + +/** + * The utilities class for Spring Cloud Discovery + * + * @author Mercy + * @see Utils + * @since 1.0.0 + */ +public abstract class DiscoveryUtils implements Utils { + + /** + * Get the instances map from {@link SimpleDiscoveryProperties} + * + * @param properties {@link SimpleDiscoveryProperties} + * @return the instances map + */ + @Nonnull + public static Map> getInstancesMap(@Nonnull SimpleDiscoveryProperties properties) { + return properties.getInstances(); + } + + /** + * Get the instances map from {@link SimpleReactiveDiscoveryProperties} + * + * @param properties {@link SimpleReactiveDiscoveryProperties} + * @return the instances map + */ + @Nonnull + public static Map> getInstancesMap(@Nonnull SimpleReactiveDiscoveryProperties properties) { + return invokeMethod(properties, "getInstances"); + } + + /** + * Convert {@link SimpleDiscoveryProperties} to {@link SimpleReactiveDiscoveryProperties} + * + * @param properties {@link SimpleDiscoveryProperties} + * @return {@link SimpleReactiveDiscoveryProperties} + */ + @Nonnull + public static SimpleReactiveDiscoveryProperties simpleReactiveDiscoveryProperties(@Nonnull SimpleDiscoveryProperties properties) { + SimpleReactiveDiscoveryProperties simpleReactiveDiscoveryProperties = new SimpleReactiveDiscoveryProperties(); + simpleReactiveDiscoveryProperties.setOrder(properties.getOrder()); + + DefaultServiceInstance local = properties.getLocal(); + DefaultServiceInstance targetLocal = simpleReactiveDiscoveryProperties.getLocal(); + setProperties(targetLocal, local); + + Map> instances = getInstancesMap(properties); + simpleReactiveDiscoveryProperties.setInstances(instances); + + return simpleReactiveDiscoveryProperties; + } + + /** + * Convert {@link SimpleReactiveDiscoveryProperties} to {@link SimpleDiscoveryProperties} + * + * @param properties {@link SimpleReactiveDiscoveryProperties} + * @return {@link SimpleDiscoveryProperties} + */ + @Nonnull + public static SimpleDiscoveryProperties simpleDiscoveryProperties(@Nonnull SimpleReactiveDiscoveryProperties properties) { + SimpleDiscoveryProperties simpleDiscoveryProperties = new SimpleDiscoveryProperties(); + simpleDiscoveryProperties.setOrder(properties.getOrder()); + + DefaultServiceInstance local = properties.getLocal(); + simpleDiscoveryProperties.setInstance(local.getServiceId(), local.getHost(), local.getPort()); + + Map> instances = invokeMethod(properties, "getInstances"); + simpleDiscoveryProperties.setInstances(instances); + + return simpleDiscoveryProperties; + } + + private DiscoveryUtils() { + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/event/ServiceInstancesChangedEvent.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/event/ServiceInstancesChangedEvent.java index e13600c2..2515d02b 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/event/ServiceInstancesChangedEvent.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/event/ServiceInstancesChangedEvent.java @@ -16,13 +16,6 @@ */ package io.microsphere.spring.cloud.client.event; -/** - * TODO Comment - * - * @author Mercy - * @since TODO - */ - import org.springframework.cloud.client.ServiceInstance; import org.springframework.context.ApplicationEvent; import org.springframework.context.event.ApplicationEventMulticaster; @@ -31,9 +24,10 @@ import java.util.List; import static java.util.Collections.unmodifiableList; +import static org.springframework.util.Assert.notEmpty; /** - * An event raised after the {@link ServiceInstance instances} of one service has been + * An event raised when the {@link ServiceInstance instances} of one service has been * changed. * * @author Mercy @@ -58,6 +52,7 @@ public class ServiceInstancesChangedEvent extends ApplicationEvent { public ServiceInstancesChangedEvent(String serviceName, List serviceInstances) { super(serviceName); + notEmpty(serviceInstances, () -> "The arguments 'serviceInstances' must not be empty!"); this.serviceInstances = unmodifiableList(serviceInstances); } @@ -91,4 +86,4 @@ public boolean isProcessed() { return processed; } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/DefaultRegistration.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/DefaultRegistration.java index d15acf15..bd3eb25b 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/DefaultRegistration.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/DefaultRegistration.java @@ -27,4 +27,4 @@ * @since 1.0.0 */ public class DefaultRegistration extends DefaultServiceInstance implements Registration { -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/InMemoryServiceRegistry.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/InMemoryServiceRegistry.java index b094c8dc..7d1840c3 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/InMemoryServiceRegistry.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/InMemoryServiceRegistry.java @@ -35,23 +35,73 @@ public class InMemoryServiceRegistry implements ServiceRegistry { private final ConcurrentMap storage = new ConcurrentHashMap<>(1); + /** + * Registers the given {@link Registration} instance in the in-memory storage, + * keyed by its instance ID. + * + *

    Example Usage: + *

    {@code
    +     * InMemoryServiceRegistry registry = new InMemoryServiceRegistry();
    +     * Registration registration = createRegistration();
    +     * registry.register(registration);
    +     * }
    + * + * @param registration the {@link Registration} to store + */ @Override public void register(Registration registration) { String id = registration.getInstanceId(); storage.put(id, registration); } + /** + * Removes the given {@link Registration} instance from the in-memory storage. + * + *

    Example Usage: + *

    {@code
    +     * InMemoryServiceRegistry registry = new InMemoryServiceRegistry();
    +     * Registration registration = createRegistration();
    +     * registry.register(registration);
    +     * registry.deregister(registration);
    +     * }
    + * + * @param registration the {@link Registration} to remove + */ @Override public void deregister(Registration registration) { String id = registration.getInstanceId(); storage.remove(id, registration); } + /** + * Closes this registry by clearing all stored {@link Registration} instances. + * + *

    Example Usage: + *

    {@code
    +     * InMemoryServiceRegistry registry = new InMemoryServiceRegistry();
    +     * registry.register(registration);
    +     * registry.close();
    +     * }
    + */ @Override public void close() { storage.clear(); } + /** + * Sets the status of the given {@link Registration} by storing it in + * the registration's metadata under the {@code _status_} key. + * + *

    Example Usage: + *

    {@code
    +     * InMemoryServiceRegistry registry = new InMemoryServiceRegistry();
    +     * registry.register(registration);
    +     * registry.setStatus(registration, "UP");
    +     * }
    + * + * @param registration the {@link Registration} whose status is to be set + * @param status the status value to set + */ @Override public void setStatus(Registration registration, String status) { Map metadata = getMetadata(registration); @@ -60,6 +110,20 @@ public void setStatus(Registration registration, String status) { } } + /** + * Retrieves the status of the given {@link Registration} from its metadata. + * + *

    Example Usage: + *

    {@code
    +     * InMemoryServiceRegistry registry = new InMemoryServiceRegistry();
    +     * registry.register(registration);
    +     * registry.setStatus(registration, "UP");
    +     * Object status = registry.getStatus(registration); // "UP"
    +     * }
    + * + * @param registration the {@link Registration} whose status is to be retrieved + * @return the status value, or {@code null} if not set or registration not found + */ @Override public Object getStatus(Registration registration) { Map metadata = getMetadata(registration); @@ -69,12 +133,26 @@ public Object getStatus(Registration registration) { return null; } + /** + * Retrieves the metadata {@link Map} for the given {@link Registration} + * from the in-memory storage. + * + *

    Example Usage: + *

    {@code
    +     * InMemoryServiceRegistry registry = new InMemoryServiceRegistry();
    +     * registry.register(registration);
    +     * Map metadata = registry.getMetadata(registration);
    +     * }
    + * + * @param registration the {@link Registration} whose metadata is to be retrieved + * @return the metadata map, or {@code null} if the registration is not found + */ protected Map getMetadata(Registration registration) { String id = registration.getInstanceId(); Registration instance = storage.get(id); - if (storage != null) { + if (instance != null) { return instance.getMetadata(); } return null; } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/MultipleAutoServiceRegistration.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/MultipleAutoServiceRegistration.java index 3d860b2b..227305f5 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/MultipleAutoServiceRegistration.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/MultipleAutoServiceRegistration.java @@ -17,8 +17,27 @@ public class MultipleAutoServiceRegistration extends AbstractAutoServiceRegistration { private final AutoServiceRegistrationProperties autoServiceRegistrationProperties; + private final MultipleRegistration multipleRegistration; + /** + * Constructs a new {@link MultipleAutoServiceRegistration} with the specified + * {@link MultipleRegistration}, {@link ServiceRegistry}, and + * {@link AutoServiceRegistrationProperties}. + * + *

    Example Usage: + *

    {@code
    +     * MultipleRegistration registration = new MultipleRegistration(registrations);
    +     * ServiceRegistry serviceRegistry = new InMemoryServiceRegistry();
    +     * AutoServiceRegistrationProperties properties = new AutoServiceRegistrationProperties();
    +     * MultipleAutoServiceRegistration autoReg =
    +     *     new MultipleAutoServiceRegistration(registration, serviceRegistry, properties);
    +     * }
    + * + * @param multipleRegistration the {@link MultipleRegistration} to manage + * @param serviceRegistry the {@link ServiceRegistry} to delegate to + * @param properties the {@link AutoServiceRegistrationProperties} for configuration + */ public MultipleAutoServiceRegistration(MultipleRegistration multipleRegistration, ServiceRegistry serviceRegistry, AutoServiceRegistrationProperties properties) { @@ -27,23 +46,70 @@ public MultipleAutoServiceRegistration(MultipleRegistration multipleRegistration this.multipleRegistration = multipleRegistration; } + /** + * Returns the configuration object for this auto service registration. + * This implementation always returns {@code null}. + * + *

    Example Usage: + *

    {@code
    +     * MultipleAutoServiceRegistration autoReg = ...;
    +     * Object config = autoReg.getConfiguration(); // null
    +     * }
    + * + * @return {@code null} + */ @Override protected Object getConfiguration() { return null; } + /** + * Determines whether this auto service registration is enabled based on the + * {@link AutoServiceRegistrationProperties}. + * + *

    Example Usage: + *

    {@code
    +     * MultipleAutoServiceRegistration autoReg = ...;
    +     * boolean enabled = autoReg.isEnabled();
    +     * }
    + * + * @return {@code true} if auto service registration is enabled + */ @Override protected boolean isEnabled() { return this.autoServiceRegistrationProperties.isEnabled(); } + /** + * Returns the {@link MultipleRegistration} managed by this auto service registration. + * + *

    Example Usage: + *

    {@code
    +     * MultipleAutoServiceRegistration autoReg = ...;
    +     * MultipleRegistration registration = autoReg.getRegistration();
    +     * }
    + * + * @return the {@link MultipleRegistration} instance + */ @Override protected MultipleRegistration getRegistration() { return this.multipleRegistration; } + /** + * Returns the management {@link MultipleRegistration}, which is the same as + * the primary registration in this implementation. + * + *

    Example Usage: + *

    {@code
    +     * MultipleAutoServiceRegistration autoReg = ...;
    +     * MultipleRegistration mgmtRegistration = autoReg.getManagementRegistration();
    +     * }
    + * + * @return the {@link MultipleRegistration} instance used for management + */ @Override protected MultipleRegistration getManagementRegistration() { return this.multipleRegistration; } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/MultipleRegistration.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/MultipleRegistration.java index 75d3fd7b..aba1ff63 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/MultipleRegistration.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/MultipleRegistration.java @@ -1,13 +1,18 @@ package io.microsphere.spring.cloud.client.service.registry; import org.springframework.cloud.client.serviceregistry.Registration; -import org.springframework.util.CollectionUtils; import java.net.URI; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; +import static io.microsphere.util.Assert.assertNotEmpty; +import static io.microsphere.util.ClassUtils.findAllClasses; +import static io.microsphere.util.ClassUtils.isAssignableFrom; +import static org.springframework.aop.framework.AopProxyUtils.ultimateTargetClass; + /** * The Delegating {@link Registration} for the multiple service registration * @@ -24,55 +29,180 @@ public class MultipleRegistration implements Registration { private final RegistrationMetaData metaData; + /** + * Constructs a new {@link MultipleRegistration} from the given collection of + * {@link Registration} instances. The last registration in the collection becomes + * the default registration. + * + *

    Example Usage: + *

    {@code
    +     * DefaultRegistration registration = new DefaultRegistration();
    +     * registration.setServiceId("test-service");
    +     * MultipleRegistration multipleRegistration =
    +     *     new MultipleRegistration(List.of(registration));
    +     * }
    + * + * @param registrations the collection of {@link Registration} instances, must not be empty + */ public MultipleRegistration(Collection registrations) { - if (CollectionUtils.isEmpty(registrations)) - throw new IllegalArgumentException("registrations cannot be empty"); + assertNotEmpty(registrations, () -> "registrations cannot be empty"); //init map for (Registration registration : registrations) { - Class clazz = registration.getClass(); - this.registrationMap.put(clazz, registration); + Class clazz = (Class) ultimateTargetClass(registration); + List> classes = (List) findAllClasses(clazz, type -> isAssignableFrom(Registration.class, type) && !Registration.class.equals(type)); + classes.forEach(type -> registrationMap.put(type, registration)); this.defaultRegistration = registration; } this.metaData = new RegistrationMetaData(registrations); } + /** + * Returns the instance ID from the default {@link Registration}. + * + *

    Example Usage: + *

    {@code
    +     * MultipleRegistration multipleRegistration = new MultipleRegistration(registrations);
    +     * String instanceId = multipleRegistration.getInstanceId();
    +     * }
    + * + * @return the instance ID of the default registration + */ + @Override + public String getInstanceId() { + return getDefaultRegistration().getInstanceId(); + } + + /** + * Returns the service ID from the default {@link Registration}. + * + *

    Example Usage: + *

    {@code
    +     * MultipleRegistration multipleRegistration = new MultipleRegistration(registrations);
    +     * String serviceId = multipleRegistration.getServiceId();
    +     * }
    + * + * @return the service ID of the default registration + */ @Override public String getServiceId() { return getDefaultRegistration().getServiceId(); } + /** + * Returns the host from the default {@link Registration}. + * + *

    Example Usage: + *

    {@code
    +     * MultipleRegistration multipleRegistration = new MultipleRegistration(registrations);
    +     * String host = multipleRegistration.getHost();
    +     * }
    + * + * @return the host of the default registration + */ @Override public String getHost() { return getDefaultRegistration().getHost(); } + /** + * Returns the port from the default {@link Registration}. + * + *

    Example Usage: + *

    {@code
    +     * MultipleRegistration multipleRegistration = new MultipleRegistration(registrations);
    +     * int port = multipleRegistration.getPort();
    +     * }
    + * + * @return the port of the default registration + */ @Override public int getPort() { return getDefaultRegistration().getPort(); } + /** + * Returns whether the default {@link Registration} is secure. + * + *

    Example Usage: + *

    {@code
    +     * MultipleRegistration multipleRegistration = new MultipleRegistration(registrations);
    +     * boolean secure = multipleRegistration.isSecure();
    +     * }
    + * + * @return {@code true} if the default registration is secure + */ @Override public boolean isSecure() { return getDefaultRegistration().isSecure(); } + /** + * Returns the {@link URI} from the default {@link Registration}. + * + *

    Example Usage: + *

    {@code
    +     * MultipleRegistration multipleRegistration = new MultipleRegistration(registrations);
    +     * URI uri = multipleRegistration.getUri();
    +     * }
    + * + * @return the URI of the default registration + */ @Override public URI getUri() { return getDefaultRegistration().getUri(); } + /** + * Returns the aggregated {@link RegistrationMetaData} that synchronizes metadata + * across all underlying {@link Registration} instances. + * + *

    Example Usage: + *

    {@code
    +     * MultipleRegistration multipleRegistration = new MultipleRegistration(registrations);
    +     * Map metadata = multipleRegistration.getMetadata();
    +     * }
    + * + * @return the aggregated metadata map + */ @Override public Map getMetadata() { return metaData; } + /** + * Returns the default {@link Registration} instance, which is the last registration + * provided during construction. + * + *

    Example Usage: + *

    {@code
    +     * MultipleRegistration multipleRegistration = new MultipleRegistration(registrations);
    +     * Registration defaultReg = multipleRegistration.getDefaultRegistration();
    +     * }
    + * + * @return the default {@link Registration} + */ public Registration getDefaultRegistration() { return defaultRegistration; } + /** + * Retrieves a specific {@link Registration} by its class type. If the specified + * class is {@link Registration} itself, returns this {@link MultipleRegistration}. + * + *

    Example Usage: + *

    {@code
    +     * MultipleRegistration multipleRegistration = new MultipleRegistration(registrations);
    +     * DefaultRegistration specific = multipleRegistration.special(DefaultRegistration.class);
    +     * Registration self = multipleRegistration.special(Registration.class);
    +     * }
    + * + * @param specialClass the specific {@link Registration} subclass to look up + * @param the type of the registration + * @return the matching registration, or {@code null} if not found + */ public T special(Class specialClass) { - if (specialClass.equals(Registration.class)) + if (Registration.class.equals(specialClass)) return (T) this; return (T) this.registrationMap.getOrDefault(specialClass, null); } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/MultipleServiceRegistry.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/MultipleServiceRegistry.java index ffaf3f45..d87c3118 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/MultipleServiceRegistry.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/MultipleServiceRegistry.java @@ -1,11 +1,7 @@ package io.microsphere.spring.cloud.client.service.registry; -import org.springframework.aop.framework.AopProxyUtils; -import org.springframework.aop.support.AopUtils; import org.springframework.cloud.client.serviceregistry.Registration; import org.springframework.cloud.client.serviceregistry.ServiceRegistry; -import org.springframework.core.ResolvableType; -import org.springframework.util.CollectionUtils; import java.util.HashMap; import java.util.List; @@ -13,6 +9,9 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; +import static io.microsphere.util.Assert.assertNotEmpty; +import static org.springframework.aop.framework.AopProxyUtils.ultimateTargetClass; +import static org.springframework.core.ResolvableType.forClass; import static org.springframework.core.io.support.SpringFactoriesLoader.loadFactoryNames; import static org.springframework.util.ClassUtils.resolveClassName; @@ -35,12 +34,26 @@ public class MultipleServiceRegistry implements ServiceRegistry> beanNameToRegistrationTypesMap; private ServiceRegistry defaultServiceRegistry; + private String defaultRegistrationBeanName; + /** + * Constructs a new {@link MultipleServiceRegistry} from the given map of bean names + * to {@link ServiceRegistry} instances. Each registry is mapped to its corresponding + * {@link Registration} type. + * + *

    Example Usage: + *

    {@code
    +     * ServiceRegistry simpleRegistry = new InMemoryServiceRegistry();
    +     * MultipleServiceRegistry registry =
    +     *     new MultipleServiceRegistry(Map.of("default", simpleRegistry));
    +     * }
    + * + * @param registriesMap the map of Spring bean names to {@link ServiceRegistry} instances, + * must not be empty + */ public MultipleServiceRegistry(Map registriesMap) { - if (CollectionUtils.isEmpty(registriesMap)) { - throw new IllegalArgumentException("service registry cannot be empty"); - } + assertNotEmpty(registriesMap, () -> "registrations cannot be empty"); this.registriesMap = registriesMap; this.beanNameToRegistrationTypesMap = new HashMap<>(registriesMap.size()); @@ -48,28 +61,78 @@ public MultipleServiceRegistry(Map registriesMap) { for (Map.Entry entry : registriesMap.entrySet()) { String beanName = entry.getKey(); ServiceRegistry serviceRegistry = entry.getValue(); - Class registrationClass = getRegistrationClass(serviceRegistry.getClass(), entry.getValue()); + Class registrationClass = getRegistrationClass(ultimateTargetClass(serviceRegistry)); beanNameToRegistrationTypesMap.put(beanName, registrationClass); defaultServiceRegistry = serviceRegistry; defaultRegistrationBeanName = beanName; } } + /** + * Registers the given {@link MultipleRegistration} by delegating to each underlying + * {@link ServiceRegistry} with the corresponding specific {@link Registration}. + * + *

    Example Usage: + *

    {@code
    +     * MultipleServiceRegistry registry = new MultipleServiceRegistry(registriesMap);
    +     * MultipleRegistration registration = new MultipleRegistration(registrations);
    +     * registry.register(registration);
    +     * }
    + * + * @param registration the {@link MultipleRegistration} to register + */ @Override public void register(MultipleRegistration registration) { iterate(registration, (reg, registry) -> registry.register(reg)); } + /** + * Deregisters the given {@link MultipleRegistration} by delegating to each underlying + * {@link ServiceRegistry} with the corresponding specific {@link Registration}. + * + *

    Example Usage: + *

    {@code
    +     * MultipleServiceRegistry registry = new MultipleServiceRegistry(registriesMap);
    +     * MultipleRegistration registration = new MultipleRegistration(registrations);
    +     * registry.register(registration);
    +     * registry.deregister(registration);
    +     * }
    + * + * @param registration the {@link MultipleRegistration} to deregister + */ @Override public void deregister(MultipleRegistration registration) { iterate(registration, (reg, registry) -> registry.deregister(reg)); } + /** + * Closes all underlying {@link ServiceRegistry} instances. + * + *

    Example Usage: + *

    {@code
    +     * MultipleServiceRegistry registry = new MultipleServiceRegistry(registriesMap);
    +     * registry.close();
    +     * }
    + */ @Override public void close() { iterate(ServiceRegistry::close); } + /** + * Sets the status of the given {@link MultipleRegistration} by delegating to each + * underlying {@link ServiceRegistry} with the corresponding specific {@link Registration}. + * + *

    Example Usage: + *

    {@code
    +     * MultipleServiceRegistry registry = new MultipleServiceRegistry(registriesMap);
    +     * registry.register(registration);
    +     * registry.setStatus(registration, "UP");
    +     * }
    + * + * @param registration the {@link MultipleRegistration} whose status is to be set + * @param status the status value to set + */ @Override public void setStatus(MultipleRegistration registration, String status) { iterate(registration, (reg, registry) -> registry.setStatus(reg, status)); @@ -89,6 +152,22 @@ private void iterate(Consumer action) { registriesMap.values().forEach(action); } + /** + * Retrieves the status of the given {@link MultipleRegistration} from the default + * {@link ServiceRegistry}. + * + *

    Example Usage: + *

    {@code
    +     * MultipleServiceRegistry registry = new MultipleServiceRegistry(registriesMap);
    +     * registry.register(registration);
    +     * registry.setStatus(registration, "UP");
    +     * Object status = registry.getStatus(registration); // "UP"
    +     * }
    + * + * @param registration the {@link MultipleRegistration} whose status is to be retrieved + * @param the type of the status value + * @return the status from the default service registry + */ @Override public T getStatus(MultipleRegistration registration) { Class registrationClass = beanNameToRegistrationTypesMap.get(defaultRegistrationBeanName); @@ -96,8 +175,23 @@ public T getStatus(MultipleRegistration registration) { return (T) defaultServiceRegistry.getStatus(targetRegistration); } - private static Class getRegistrationClass(Class serviceRegistryClass, ServiceRegistry serviceRegistry) { - Class registrationClass = ResolvableType.forClass(serviceRegistryClass) + /** + * Resolves the {@link Registration} class for the given {@link ServiceRegistry} class + * using generic type resolution. Falls back to {@code SpringFactoriesLoader} when the + * generic type resolves to {@link Registration} itself. + * + *

    Example Usage: + *

    {@code
    +     * Class regClass =
    +     *     MultipleServiceRegistry.getRegistrationClass(NacosServiceRegistry.class);
    +     * // returns NacosRegistration.class
    +     * }
    + * + * @param serviceRegistryClass the {@link ServiceRegistry} implementation class + * @return the resolved {@link Registration} subclass + */ + static Class getRegistrationClass(Class serviceRegistryClass) { + Class registrationClass = forClass(serviceRegistryClass) .as(ServiceRegistry.class) .getGeneric(0) .resolve(); @@ -107,11 +201,7 @@ private static Class getRegistrationClass(Class registrationClassNames; - if (AopUtils.isAopProxy(serviceRegistry)) { - registrationClassNames = loadFactoryNames(AopProxyUtils.ultimateTargetClass(serviceRegistry), classLoader); - } else { - registrationClassNames = loadFactoryNames(serviceRegistryClass, classLoader); - } + registrationClassNames = loadFactoryNames(serviceRegistryClass, classLoader); for (String registrationClassName : registrationClassNames) { registrationClass = resolveClassName(registrationClassName, classLoader); @@ -119,4 +209,4 @@ private static Class getRegistrationClass(Class) registrationClass; } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/RegistrationCustomizer.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/RegistrationCustomizer.java index eccef247..8884b42b 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/RegistrationCustomizer.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/RegistrationCustomizer.java @@ -33,4 +33,4 @@ public interface RegistrationCustomizer { * @param registration {@link Registration} */ void customize(Registration registration); -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/RegistrationMetaData.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/RegistrationMetaData.java index 8a35c2aa..638acce7 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/RegistrationMetaData.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/RegistrationMetaData.java @@ -9,26 +9,58 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import static io.microsphere.reflect.MethodUtils.invokeMethod; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.removeMetadata; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.setMetadata; +import static io.microsphere.util.Assert.assertNotEmpty; +import static org.springframework.aop.framework.AopProxyUtils.ultimateTargetClass; + /** * @author 韩超 * @since 1.0.0 */ public final class RegistrationMetaData implements Map { + /** + * The class name of {@link org.springframework.cloud.zookeeper.serviceregistry.ServiceInstanceRegistration} + */ + static final String ZOOKEEPER_REGISTRATION_CLASS_NAME = "org.springframework.cloud.zookeeper.serviceregistry.ServiceInstanceRegistration"; + + /** + * The method name of {@link org.springframework.cloud.zookeeper.serviceregistry.ServiceInstanceRegistration#getServiceInstance()} + */ + static final String GET_SERVICE_INSTANCE_METHOD_NAME = "getServiceInstance"; + /** * MetaData information manually added by the application,usually specified by configuration */ private final Map applicationMetaData; + private final Collection registrations; + private final Object lock = new Object(); + /** + * Constructs a new {@link RegistrationMetaData} that aggregates metadata from the given + * collection of {@link Registration} instances. Metadata changes are synchronized across + * all registrations. + * + *

    Example Usage: + *

    {@code
    +     * DefaultRegistration registration = new DefaultRegistration();
    +     * registration.getMetadata().put("key1", "value1");
    +     * RegistrationMetaData metaData = new RegistrationMetaData(List.of(registration));
    +     * }
    + * + * @param registrations the collection of {@link Registration} instances, must not be empty + */ public RegistrationMetaData(Collection registrations) { - if (CollectionUtils.isEmpty(registrations)) - throw new IllegalArgumentException("registrations cannot be empty"); - + assertNotEmpty(registrations, () -> "registrations cannot be empty"); this.registrations = registrations; this.applicationMetaData = new ConcurrentHashMap<>(); for (Registration registration : registrations) { + initializeIfZookeeperRegistrationAvailable(registration); + Map metaData = registration.getMetadata(); if (!CollectionUtils.isEmpty(metaData)) { //check key and value must not be null @@ -41,51 +73,153 @@ public RegistrationMetaData(Collection registrations) { } } + /** + * Returns the number of metadata entries. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationMetaData metaData = new RegistrationMetaData(registrations);
    +     * int count = metaData.size(); // e.g. 3
    +     * }
    + * + * @return the number of key-value mappings in this metadata + */ @Override public int size() { return applicationMetaData.size(); } + /** + * Returns whether this metadata map is empty. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationMetaData metaData = new RegistrationMetaData(registrations);
    +     * boolean empty = metaData.isEmpty(); // false if registrations have metadata
    +     * }
    + * + * @return {@code true} if this metadata contains no entries + */ @Override public boolean isEmpty() { return this.applicationMetaData.isEmpty(); } + /** + * Returns whether this metadata contains the specified key. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationMetaData metaData = new RegistrationMetaData(registrations);
    +     * boolean hasKey = metaData.containsKey("key1"); // true
    +     * boolean missing = metaData.containsKey("unknown"); // false
    +     * }
    + * + * @param key the key to check for + * @return {@code true} if this metadata contains the specified key + */ @Override public boolean containsKey(Object key) { return this.applicationMetaData.containsKey(key); } + /** + * Returns whether this metadata contains the specified value. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationMetaData metaData = new RegistrationMetaData(registrations);
    +     * boolean hasValue = metaData.containsValue("value1"); // true
    +     * boolean missing = metaData.containsValue("unknown"); // false
    +     * }
    + * + * @param value the value to check for + * @return {@code true} if this metadata contains the specified value + */ @Override public boolean containsValue(Object value) { return this.applicationMetaData.containsValue(value); } + /** + * Returns the metadata value associated with the specified key. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationMetaData metaData = new RegistrationMetaData(registrations);
    +     * String value = metaData.get("key1"); // "value1"
    +     * String missing = metaData.get("unknown"); // null
    +     * }
    + * + * @param key the key whose associated value is to be returned + * @return the value associated with the key, or {@code null} if not found + */ @Override public String get(Object key) { return this.applicationMetaData.get(key); } + /** + * Puts a metadata entry and synchronizes it across all underlying {@link Registration} + * instances. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationMetaData metaData = new RegistrationMetaData(registrations);
    +     * metaData.put("key4", "value4");
    +     * String value = metaData.get("key4"); // "value4"
    +     * }
    + * + * @param key the metadata key + * @param value the metadata value + * @return the previous value associated with the key, or {@code null} + */ @Override public String put(String key, String value) { synchronized (lock) { this.registrations.forEach(registration -> { - registration.getMetadata().put(key, value); + setMetadata(registration, key, value); }); } return this.applicationMetaData.put(key, value); } + /** + * Removes the metadata entry for the specified key and synchronizes the removal + * across all underlying {@link Registration} instances. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationMetaData metaData = new RegistrationMetaData(registrations);
    +     * metaData.remove("key1");
    +     * String value = metaData.get("key1"); // null
    +     * }
    + * + * @param key the key whose mapping is to be removed + * @return the previous value associated with the key, or {@code null} + */ @Override public String remove(Object key) { synchronized (lock) { this.registrations.forEach(registration -> { - registration.getMetadata().remove(key); + removeMetadata(registration, (String) key); }); } return this.applicationMetaData.remove(key); } + /** + * Copies all entries from the specified map into this metadata and synchronizes + * them across all underlying {@link Registration} instances. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationMetaData metaData = new RegistrationMetaData(registrations);
    +     * metaData.putAll(Map.of("key4", "value4", "key5", "value5"));
    +     * }
    + * + * @param m the map of entries to add + */ @Override public void putAll(Map m) { synchronized (lock) { @@ -96,6 +230,17 @@ public void putAll(Map m) { this.applicationMetaData.putAll(m); } + /** + * Clears all metadata entries and synchronizes the clearing across all underlying + * {@link Registration} instances. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationMetaData metaData = new RegistrationMetaData(registrations);
    +     * metaData.clear();
    +     * int size = metaData.size(); // 0
    +     * }
    + */ @Override public void clear() { synchronized (lock) { @@ -104,18 +249,63 @@ public void clear() { this.applicationMetaData.clear(); } + /** + * Returns an unmodifiable {@link Set} view of the metadata keys. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationMetaData metaData = new RegistrationMetaData(registrations);
    +     * Set keys = metaData.keySet();
    +     * boolean hasKey = keys.contains("key1"); // true
    +     * }
    + * + * @return an unmodifiable set of metadata keys + */ @Override public Set keySet() { return Collections.unmodifiableSet(this.applicationMetaData.keySet()); } + /** + * Returns an unmodifiable {@link Collection} view of the metadata values. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationMetaData metaData = new RegistrationMetaData(registrations);
    +     * Collection values = metaData.values();
    +     * boolean hasValue = values.contains("value1"); // true
    +     * }
    + * + * @return an unmodifiable collection of metadata values + */ @Override public Collection values() { return Collections.unmodifiableCollection(this.applicationMetaData.values()); } + /** + * Returns a modifiable {@link Set} view of the metadata entries. Unlike + * {@link #keySet()} and {@link #values()}, the returned set is not wrapped + * in an unmodifiable view. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationMetaData metaData = new RegistrationMetaData(registrations);
    +     * Set> entries = metaData.entrySet();
    +     * }
    + * + * @return a set of metadata entries + */ @Override public Set> entrySet() { return this.applicationMetaData.entrySet(); } -} + + private void initializeIfZookeeperRegistrationAvailable(Registration registration) { + Class registrationClass = ultimateTargetClass(registration); + if (ZOOKEEPER_REGISTRATION_CLASS_NAME.equals(registrationClass.getName())) { + // init ServiceInstance + invokeMethod(registration, GET_SERVICE_INSTANCE_METHOD_NAME); + } + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/SimpleAutoServiceRegistration.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/SimpleAutoServiceRegistration.java index 30082573..26c3c4d2 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/SimpleAutoServiceRegistration.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/SimpleAutoServiceRegistration.java @@ -34,30 +34,94 @@ public class SimpleAutoServiceRegistration extends AbstractAutoServiceRegistrati private final Registration registration; - protected SimpleAutoServiceRegistration(ServiceRegistry serviceRegistry, + /** + * Constructs a new {@link SimpleAutoServiceRegistration} with the specified + * {@link ServiceRegistry}, {@link AutoServiceRegistrationProperties}, and + * {@link Registration}. + * + *

    Example Usage: + *

    {@code
    +     * InMemoryServiceRegistry serviceRegistry = new InMemoryServiceRegistry();
    +     * AutoServiceRegistrationProperties properties = new AutoServiceRegistrationProperties();
    +     * Registration registration = createRegistration();
    +     * SimpleAutoServiceRegistration autoReg =
    +     *     new SimpleAutoServiceRegistration(serviceRegistry, properties, registration);
    +     * }
    + * + * @param serviceRegistry the {@link ServiceRegistry} to delegate to + * @param properties the {@link AutoServiceRegistrationProperties} for configuration + * @param registration the {@link Registration} to manage + */ + public SimpleAutoServiceRegistration(ServiceRegistry serviceRegistry, AutoServiceRegistrationProperties properties, Registration registration) { super(serviceRegistry, properties); this.properties = properties; this.registration = registration; } + /** + * Returns the {@link AutoServiceRegistrationProperties} as the configuration object. + * + *

    Example Usage: + *

    {@code
    +     * SimpleAutoServiceRegistration autoReg = ...;
    +     * Object config = autoReg.getConfiguration();
    +     * }
    + * + * @return the {@link AutoServiceRegistrationProperties} instance + */ @Override protected Object getConfiguration() { return properties; } + /** + * Determines whether this auto service registration is enabled based on the + * {@link AutoServiceRegistrationProperties}. + * + *

    Example Usage: + *

    {@code
    +     * SimpleAutoServiceRegistration autoReg = ...;
    +     * boolean enabled = autoReg.isEnabled();
    +     * }
    + * + * @return {@code true} if auto service registration is enabled + */ @Override protected boolean isEnabled() { return properties.isEnabled(); } + /** + * Returns the {@link Registration} managed by this auto service registration. + * + *

    Example Usage: + *

    {@code
    +     * SimpleAutoServiceRegistration autoReg = ...;
    +     * Registration registration = autoReg.getRegistration();
    +     * }
    + * + * @return the {@link Registration} instance + */ @Override protected Registration getRegistration() { return registration; } + /** + * Returns the management {@link Registration}, which is the same as the primary + * registration in this implementation. + * + *

    Example Usage: + *

    {@code
    +     * SimpleAutoServiceRegistration autoReg = ...;
    +     * Registration mgmtRegistration = autoReg.getManagementRegistration();
    +     * }
    + * + * @return the {@link Registration} instance used for management + */ @Override protected Registration getManagementRegistration() { return registration; } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/SimpleServiceRegistry.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/SimpleServiceRegistry.java new file mode 100644 index 00000000..386bd33c --- /dev/null +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/SimpleServiceRegistry.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.registry; + +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryProperties; +import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryProperties; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static io.microsphere.spring.cloud.client.discovery.util.DiscoveryUtils.getInstancesMap; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.getMetadata; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.setMetadata; + +/** + * Simple {@link ServiceRegistry} class that is based on {@link SimpleDiscoveryProperties} + * or {@link SimpleReactiveDiscoveryProperties} to register + * {@link DefaultRegistration}. + * + * @author Mercy + * @see ServiceRegistry + * @see DefaultRegistration + * @see SimpleDiscoveryProperties#getInstances() + * @see SimpleReactiveDiscoveryProperties#getInstances() + * @since 1.0.0 + */ +public class SimpleServiceRegistry implements ServiceRegistry { + + public static final String STATUS_KEY = "_status_"; + + private final Map> instancesMap; + + /** + * Constructs a new {@link SimpleServiceRegistry} using the given + * {@link SimpleDiscoveryProperties} to obtain the instances map. + * + *

    Example Usage: + *

    {@code
    +     * SimpleDiscoveryProperties properties = new SimpleDiscoveryProperties();
    +     * SimpleServiceRegistry registry = new SimpleServiceRegistry(properties);
    +     * }
    + * + * @param properties the {@link SimpleDiscoveryProperties} to use + */ + public SimpleServiceRegistry(SimpleDiscoveryProperties properties) { + this(getInstancesMap(properties)); + } + + /** + * Constructs a new {@link SimpleServiceRegistry} using the given + * {@link SimpleReactiveDiscoveryProperties} to obtain the instances map. + * + *

    Example Usage: + *

    {@code
    +     * SimpleReactiveDiscoveryProperties reactiveProperties = ...;
    +     * SimpleServiceRegistry registry = new SimpleServiceRegistry(reactiveProperties);
    +     * }
    + * + * @param properties the {@link SimpleReactiveDiscoveryProperties} to use + */ + public SimpleServiceRegistry(SimpleReactiveDiscoveryProperties properties) { + this(getInstancesMap(properties)); + } + + /** + * Constructs a new {@link SimpleServiceRegistry} with the given instances map. + * + *

    Example Usage: + *

    {@code
    +     * Map> instancesMap = new HashMap<>();
    +     * SimpleServiceRegistry registry = new SimpleServiceRegistry(instancesMap);
    +     * }
    + * + * @param instancesMap the map of service IDs to {@link DefaultServiceInstance} lists + */ + public SimpleServiceRegistry(Map> instancesMap) { + this.instancesMap = instancesMap; + } + + /** + * Registers the given {@link DefaultRegistration} by adding it to the instances list + * for its service ID. + * + *

    Example Usage: + *

    {@code
    +     * SimpleServiceRegistry registry = new SimpleServiceRegistry(properties);
    +     * DefaultRegistration registration = new DefaultRegistration();
    +     * registration.setServiceId("test-service");
    +     * registry.register(registration);
    +     * }
    + * + * @param registration the {@link DefaultRegistration} to register + */ + @Override + public void register(DefaultRegistration registration) { + List instances = getInstances(registration); + instances.add(registration); + } + + /** + * Deregisters the given {@link DefaultRegistration} by removing it from the instances + * list for its service ID. + * + *

    Example Usage: + *

    {@code
    +     * SimpleServiceRegistry registry = new SimpleServiceRegistry(properties);
    +     * registry.register(registration);
    +     * registry.deregister(registration);
    +     * }
    + * + * @param registration the {@link DefaultRegistration} to deregister + */ + @Override + public void deregister(DefaultRegistration registration) { + List instances = getInstances(registration); + instances.remove(registration); + } + + /** + * Closes this registry. This implementation is a no-op. + * + *

    Example Usage: + *

    {@code
    +     * SimpleServiceRegistry registry = new SimpleServiceRegistry(properties);
    +     * registry.close();
    +     * }
    + */ + @Override + public void close() { + } + + /** + * Sets the status of the given {@link DefaultRegistration} by storing it in the + * registration's metadata under the {@link #STATUS_KEY} key. + * + *

    Example Usage: + *

    {@code
    +     * SimpleServiceRegistry registry = new SimpleServiceRegistry(properties);
    +     * registry.register(registration);
    +     * registry.setStatus(registration, "UP");
    +     * }
    + * + * @param registration the {@link DefaultRegistration} whose status is to be set + * @param status the status value to set + */ + @Override + public void setStatus(DefaultRegistration registration, String status) { + setMetadata(registration, STATUS_KEY, status); + } + + /** + * Retrieves the status of the given {@link DefaultRegistration} from its metadata. + * + *

    Example Usage: + *

    {@code
    +     * SimpleServiceRegistry registry = new SimpleServiceRegistry(properties);
    +     * registry.register(registration);
    +     * registry.setStatus(registration, "UP");
    +     * String status = registry.getStatus(registration); // "UP"
    +     * }
    + * + * @param registration the {@link DefaultRegistration} whose status is to be retrieved + * @return the status value, or {@code null} if not set + */ + @Override + public String getStatus(DefaultRegistration registration) { + return getMetadata(registration, STATUS_KEY); + } + + /** + * Returns the list of {@link DefaultServiceInstance} instances for the given + * {@link DefaultRegistration}'s service ID. + * + *

    Example Usage: + *

    {@code
    +     * SimpleServiceRegistry registry = new SimpleServiceRegistry(properties);
    +     * registry.register(registration);
    +     * List instances = registry.getInstances(registration);
    +     * }
    + * + * @param registration the {@link DefaultRegistration} to look up instances for + * @return the list of instances for the registration's service ID + */ + List getInstances(DefaultRegistration registration) { + return getInstances(registration.getServiceId()); + } + + /** + * Returns the list of {@link DefaultServiceInstance} instances for the given service ID, + * creating an empty list if none exists. + * + *

    Example Usage: + *

    {@code
    +     * SimpleServiceRegistry registry = new SimpleServiceRegistry(properties);
    +     * List instances = registry.getInstances("test-service");
    +     * }
    + * + * @param serviceId the service ID to look up + * @return the list of instances for the service ID + */ + List getInstances(String serviceId) { + return this.instancesMap.computeIfAbsent(serviceId, k -> new ArrayList<>()); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/actuate/autoconfigure/ServiceRegistrationEndpointAutoConfiguration.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/actuate/autoconfigure/ServiceRegistrationEndpointAutoConfiguration.java index c2a73cbb..531fc200 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/actuate/autoconfigure/ServiceRegistrationEndpointAutoConfiguration.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/actuate/autoconfigure/ServiceRegistrationEndpointAutoConfiguration.java @@ -49,6 +49,19 @@ }) public class ServiceRegistrationEndpointAutoConfiguration { + /** + * Creates a {@link ServiceRegistrationEndpoint} bean for managing service registration via actuator. + * + *

    Example Usage: + *

    {@code
    +     * // The endpoint is auto-configured and accessible at /actuator/serviceRegistration
    +     * @Autowired
    +     * ServiceRegistrationEndpoint endpoint;
    +     * Map metadata = endpoint.metadata();
    +     * }
    + * + * @return a new {@link ServiceRegistrationEndpoint} instance + */ @Bean @ConditionalOnMissingBean @ConditionalOnAvailableEndpoint @@ -56,10 +69,23 @@ public ServiceRegistrationEndpoint serviceRegistrationEndpoint() { return new ServiceRegistrationEndpoint(); } + /** + * Creates a {@link ServiceDeregistrationEndpoint} bean for managing service deregistration via actuator. + * + *

    Example Usage: + *

    {@code
    +     * // The endpoint is auto-configured and accessible at /actuator/serviceDeregistration
    +     * @Autowired
    +     * ServiceDeregistrationEndpoint endpoint;
    +     * boolean wasRunning = endpoint.stop();
    +     * }
    + * + * @return a new {@link ServiceDeregistrationEndpoint} instance + */ @Bean @ConditionalOnMissingBean @ConditionalOnAvailableEndpoint public ServiceDeregistrationEndpoint serviceDeregistrationEndpoint() { return new ServiceDeregistrationEndpoint(); } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/aspect/EventPublishingRegistrationAspect.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/aspect/EventPublishingRegistrationAspect.java index 57a45a06..0c99526a 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/aspect/EventPublishingRegistrationAspect.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/aspect/EventPublishingRegistrationAspect.java @@ -59,40 +59,113 @@ public class EventPublishingRegistrationAspect implements ApplicationContextAwar private ObjectProvider registrationCustomizers; + /** + * AOP advice executed before {@link ServiceRegistry#register(Registration)}, publishing a + * {@link RegistrationPreRegisteredEvent} and applying {@link RegistrationCustomizer customizations}. + * + *

    Example Usage: + *

    {@code
    +     * // This advice is triggered automatically when ServiceRegistry.register() is called:
    +     * serviceRegistry.register(registration);
    +     * // A RegistrationPreRegisteredEvent is published before actual registration
    +     * }
    + * + * @param registry the target {@link ServiceRegistry} + * @param registration the {@link Registration} being registered + */ @Before(value = REGISTER_POINTCUT_EXPRESSION, argNames = "registry, registration") public void beforeRegister(ServiceRegistry registry, Registration registration) { - if (registry.getClass().isAssignableFrom(MultipleServiceRegistry.class)) - return;//Remove redundant register + if (isIgnored(registry)) { + return; // Remove redundant deregister + } context.publishEvent(new RegistrationPreRegisteredEvent(registry, registration)); registrationCustomizers.forEach(customizer -> { customizer.customize(registration); }); } + /** + * AOP advice executed before {@link ServiceRegistry#deregister(Registration)}, publishing a + * {@link RegistrationPreDeregisteredEvent}. + * + *

    Example Usage: + *

    {@code
    +     * // This advice is triggered automatically when ServiceRegistry.deregister() is called:
    +     * serviceRegistry.deregister(registration);
    +     * // A RegistrationPreDeregisteredEvent is published before actual deregistration
    +     * }
    + * + * @param registry the target {@link ServiceRegistry} + * @param registration the {@link Registration} being deregistered + */ @Before(value = DEREGISTER_POINTCUT_EXPRESSION, argNames = "registry, registration") public void beforeDeregister(ServiceRegistry registry, Registration registration) { - if (registry.getClass().isAssignableFrom(MultipleServiceRegistry.class)) - return;//Remove redundant deregister + if (isIgnored(registry)) { + return; // Remove redundant deregister + } context.publishEvent(new RegistrationPreDeregisteredEvent(registry, registration)); } + /** + * AOP advice executed after {@link ServiceRegistry#register(Registration)}, publishing a + * {@link RegistrationRegisteredEvent}. + * + *

    Example Usage: + *

    {@code
    +     * // This advice is triggered automatically after ServiceRegistry.register() completes:
    +     * serviceRegistry.register(registration);
    +     * // A RegistrationRegisteredEvent is published after successful registration
    +     * }
    + * + * @param registry the target {@link ServiceRegistry} + * @param registration the {@link Registration} that was registered + */ @After(value = REGISTER_POINTCUT_EXPRESSION, argNames = "registry, registration") public void afterRegister(ServiceRegistry registry, Registration registration) { - if (registry.getClass().isAssignableFrom(MultipleServiceRegistry.class)) - return;//Remove redundant register + if (isIgnored(registry)) { + return; // Remove redundant deregister + } context.publishEvent(new RegistrationRegisteredEvent(registry, registration)); } + /** + * AOP advice executed after {@link ServiceRegistry#deregister(Registration)}, publishing a + * {@link RegistrationDeregisteredEvent}. + * + *

    Example Usage: + *

    {@code
    +     * // This advice is triggered automatically after ServiceRegistry.deregister() completes:
    +     * serviceRegistry.deregister(registration);
    +     * // A RegistrationDeregisteredEvent is published after successful deregistration
    +     * }
    + * + * @param registry the target {@link ServiceRegistry} + * @param registration the {@link Registration} that was deregistered + */ @After(value = DEREGISTER_POINTCUT_EXPRESSION, argNames = "registry, registration") public void afterDeregister(ServiceRegistry registry, Registration registration) { - if (registry.getClass().isAssignableFrom(MultipleServiceRegistry.class)) - return;//Remove redundant deregister + if (isIgnored(registry)) { + return; // Remove redundant deregister + } context.publishEvent(new RegistrationDeregisteredEvent(registry, registration)); } + boolean isIgnored(ServiceRegistry registry) { + return MultipleServiceRegistry.class.isAssignableFrom(registry.getClass()); + } + + /** + * {@inheritDoc} + * + *

    Example Usage: + *

    {@code
    +     * EventPublishingRegistrationAspect aspect = new EventPublishingRegistrationAspect();
    +     * aspect.setApplicationContext(applicationContext);
    +     * }
    + */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context = applicationContext; this.registrationCustomizers = applicationContext.getBeanProvider(RegistrationCustomizer.class); } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/ServiceRegistryAutoConfiguration.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/ServiceRegistryAutoConfiguration.java index 2fa3b76e..9eb53ecc 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/ServiceRegistryAutoConfiguration.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/ServiceRegistryAutoConfiguration.java @@ -55,6 +55,20 @@ public class ServiceRegistryAutoConfiguration { @ConditionalOnMultipleRegistrationEnabled static class MultipleConfiguration { + /** + * Creates a primary {@link MultipleRegistration} bean that aggregates all available + * {@link Registration} instances. + * + *

    Example Usage: + *

    {@code
    +         * @Autowired
    +         * MultipleRegistration multipleRegistration;
    +         * // Access individual registrations from the composite
    +         * }
    + * + * @param registrations the collection of {@link Registration} instances + * @return a new {@link MultipleRegistration} aggregating the provided registrations + */ @Primary @Bean @ConditionalOnMissingBean @@ -62,6 +76,21 @@ public MultipleRegistration multipleRegistration(Collection regist return new MultipleRegistration(registrations); } + /** + * Creates a primary {@link MultipleServiceRegistry} bean that delegates to all available + * {@link ServiceRegistry} instances. + * + *

    Example Usage: + *

    {@code
    +         * @Autowired
    +         * MultipleServiceRegistry multipleServiceRegistry;
    +         * // Register with all service registries at once
    +         * multipleServiceRegistry.register(registration);
    +         * }
    + * + * @param registriesMap a map of bean names to {@link ServiceRegistry} instances + * @return a new {@link MultipleServiceRegistry} delegating to all registries + */ @Bean @Primary @ConditionalOnMissingBean @@ -69,6 +98,23 @@ public MultipleServiceRegistry multipleServiceRegistry(MapExample Usage: + *
    {@code
    +         * @Autowired
    +         * MultipleAutoServiceRegistration autoRegistration;
    +         * // Auto-registration is managed by the Spring lifecycle
    +         * boolean running = autoRegistration.isRunning();
    +         * }
    + * + * @param multipleRegistration the composite {@link MultipleRegistration} + * @param multipleServiceRegistry the composite {@link MultipleServiceRegistry} + * @param properties the {@link AutoServiceRegistrationProperties} + * @return a new {@link MultipleAutoServiceRegistration} instance + */ @ConditionalOnBean(AutoServiceRegistrationProperties.class) @Primary @Bean @@ -80,4 +126,4 @@ public MultipleAutoServiceRegistration multipleAutoServiceRegistration(MultipleR } } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/SimpleAutoServiceRegistrationAutoConfiguration.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/SimpleAutoServiceRegistrationAutoConfiguration.java index 7115ef0a..c3028236 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/SimpleAutoServiceRegistrationAutoConfiguration.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/SimpleAutoServiceRegistrationAutoConfiguration.java @@ -16,6 +16,8 @@ */ package io.microsphere.spring.cloud.client.service.registry.autoconfigure; +import io.microsphere.annotation.ConfigurationProperty; +import io.microsphere.constants.PropertyConstants; import io.microsphere.spring.cloud.client.service.registry.DefaultRegistration; import io.microsphere.spring.cloud.client.service.registry.InMemoryServiceRegistry; import io.microsphere.spring.cloud.client.service.registry.SimpleAutoServiceRegistration; @@ -26,6 +28,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration; import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration; import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration; import org.springframework.cloud.client.serviceregistry.Registration; @@ -36,8 +39,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import static io.microsphere.constants.PropertyConstants.ENABLED_PROPERTY_NAME; -import static io.microsphere.spring.cloud.client.service.registry.autoconfigure.SimpleAutoServiceRegistrationAutoConfiguration.PROPERTY_NAME_PREFIX; +import static io.microsphere.annotation.ConfigurationProperty.APPLICATION_SOURCE; +import static io.microsphere.spring.cloud.client.service.registry.autoconfigure.SimpleAutoServiceRegistrationAutoConfiguration.ENABLED_PROPERTY_NAME; import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.MICROSPHERE_SPRING_CLOUD_PROPERTY_NAME_PREFIX; /** @@ -48,14 +51,15 @@ * @since 1.0.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnProperty(prefix = PROPERTY_NAME_PREFIX, name = ENABLED_PROPERTY_NAME) +@ConditionalOnProperty(name = ENABLED_PROPERTY_NAME) @ConditionalOnAutoServiceRegistrationEnabled @AutoConfigureBefore(value = { AutoServiceRegistrationAutoConfiguration.class }) @AutoConfigureAfter(value = { UtilAutoConfiguration.class, - AutoServiceRegistrationConfiguration.class + AutoServiceRegistrationConfiguration.class, + SimpleDiscoveryClientAutoConfiguration.class }) @Import(value = { SimpleAutoServiceRegistration.class @@ -63,10 +67,35 @@ public class SimpleAutoServiceRegistrationAutoConfiguration { /** - * The property name prefix + * The property name prefix : "microsphere.spring.cloud.service-registry.auto-registration.simple." */ - public static final String PROPERTY_NAME_PREFIX = MICROSPHERE_SPRING_CLOUD_PROPERTY_NAME_PREFIX + "simple."; + public static final String PROPERTY_NAME_PREFIX = MICROSPHERE_SPRING_CLOUD_PROPERTY_NAME_PREFIX + "service-registry.auto-registration.simple."; + /** + * The property name : "microsphere.spring.cloud.service-registry.auto-registration.simple.enabled" + */ + @ConfigurationProperty( + type = boolean.class, + source = APPLICATION_SOURCE + ) + public static final String ENABLED_PROPERTY_NAME = PROPERTY_NAME_PREFIX + PropertyConstants.ENABLED_PROPERTY_NAME; + + /** + * Creates a {@link Registration} bean from the application name, server properties, and network info. + * + *

    Example Usage: + *

    {@code
    +     * // Auto-configured via Spring Boot; the bean is available for injection:
    +     * @Autowired
    +     * Registration registration;
    +     * String serviceId = registration.getServiceId();
    +     * }
    + * + * @param applicationName the Spring application name resolved from {@code spring.application.name} + * @param serverProperties the {@link ServerProperties} providing the server port + * @param inetUtils the {@link InetUtils} for resolving the host address + * @return a new {@link DefaultRegistration} instance + */ @Bean public Registration registration( @Value("${spring.application.name:default}") String applicationName, @@ -85,10 +114,22 @@ public Registration registration( return registration; } - + /** + * Creates an {@link InMemoryServiceRegistry} bean as the default {@link ServiceRegistry} implementation. + * + *

    Example Usage: + *

    {@code
    +     * // Auto-configured when no other ServiceRegistry bean is present:
    +     * @Autowired
    +     * ServiceRegistry serviceRegistry;
    +     * serviceRegistry.register(registration);
    +     * }
    + * + * @return a new {@link InMemoryServiceRegistry} instance + */ @Bean @ConditionalOnMissingBean public ServiceRegistry serviceRegistry() { return new InMemoryServiceRegistry(); } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebFluxServiceRegistryAutoConfiguration.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebFluxServiceRegistryAutoConfiguration.java index 0cb30de1..87ede268 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebFluxServiceRegistryAutoConfiguration.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebFluxServiceRegistryAutoConfiguration.java @@ -16,12 +16,9 @@ */ package io.microsphere.spring.cloud.client.service.registry.autoconfigure; -import io.microsphere.spring.cloud.client.service.registry.condition.ConditionalOnAutoServiceRegistrationEnabled; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import io.microsphere.spring.web.metadata.WebEndpointMapping; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.cloud.client.serviceregistry.ServiceRegistry; -import org.springframework.context.annotation.Configuration; import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.REACTIVE; @@ -31,15 +28,42 @@ * @author Mercy * @since 1.0.0 */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(name = { - "io.microsphere.spring.web.metadata.WebEndpointMapping", - "io.microsphere.spring.web.event.WebEndpointMappingsReadyEvent" -}) @ConditionalOnWebApplication(type = REACTIVE) -@ConditionalOnAutoServiceRegistrationEnabled -@AutoConfigureAfter(value = { - ServiceRegistryAutoConfiguration.class -}) -public class WebFluxServiceRegistryAutoConfiguration { -} +public class WebFluxServiceRegistryAutoConfiguration extends WebServiceRegistryAutoConfiguration { + + /** + * {@inheritDoc} + *

    Returns an empty string as WebFlux applications do not use a servlet context path. + * + *

    Example Usage: + *

    {@code
    +     * WebFluxServiceRegistryAutoConfiguration config = new WebFluxServiceRegistryAutoConfiguration();
    +     * String contextPath = config.getContextPath(); // returns ""
    +     * }
    + * + * @return an empty string + */ + @Override + protected String getContextPath() { + return ""; + } + + /** + * {@inheritDoc} + *

    Always returns {@code false} for WebFlux applications, as no mappings are excluded. + * + *

    Example Usage: + *

    {@code
    +     * WebFluxServiceRegistryAutoConfiguration config = new WebFluxServiceRegistryAutoConfiguration();
    +     * boolean excluded = config.isExcludedMapping(mapping, patterns); // always false
    +     * }
    + * + * @param mapping the {@link WebEndpointMapping} to evaluate + * @param patterns the URL patterns associated with the mapping + * @return always {@code false} + */ + @Override + protected boolean isExcludedMapping(WebEndpointMapping mapping, String[] patterns) { + return false; + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebMvcServiceRegistryAutoConfiguration.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebMvcServiceRegistryAutoConfiguration.java index deb59f3c..b160d563 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebMvcServiceRegistryAutoConfiguration.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebMvcServiceRegistryAutoConfiguration.java @@ -16,34 +16,23 @@ */ package io.microsphere.spring.cloud.client.service.registry.autoconfigure; -import io.microsphere.logging.Logger; -import io.microsphere.logging.LoggerFactory; -import io.microsphere.spring.cloud.client.service.registry.condition.ConditionalOnAutoServiceRegistrationEnabled; -import io.microsphere.spring.web.event.WebEndpointMappingsReadyEvent; import io.microsphere.spring.web.metadata.WebEndpointMapping; +import io.microsphere.util.ValueHolder; +import jakarta.servlet.Filter; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.cloud.client.serviceregistry.Registration; import org.springframework.cloud.client.serviceregistry.ServiceRegistry; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.event.EventListener; -import javax.servlet.Filter; -import java.util.Arrays; import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; import java.util.Objects; -import java.util.Set; -import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.attachMetadata; +import static io.microsphere.util.ArrayUtils.EMPTY_STRING_ARRAY; +import static io.microsphere.util.ArrayUtils.arrayEquals; +import static java.lang.Boolean.FALSE; import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.SERVLET; /** @@ -52,28 +41,11 @@ * @author Mercy * @since 1.0.0 */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(name = { - "io.microsphere.spring.web.metadata.WebEndpointMapping", - "io.microsphere.spring.web.event.WebEndpointMappingsReadyEvent" -}) -@ConditionalOnBean(Registration.class) @ConditionalOnWebApplication(type = SERVLET) -@ConditionalOnAutoServiceRegistrationEnabled -@AutoConfigureAfter(value = { - ServiceRegistryAutoConfiguration.class -}) -public class WebMvcServiceRegistryAutoConfiguration { - - private static final Logger logger = LoggerFactory.getLogger(WebMvcServiceRegistryAutoConfiguration.class); +public class WebMvcServiceRegistryAutoConfiguration extends WebServiceRegistryAutoConfiguration { private static final String[] DEFAULT_URL_MAPPINGS = {"/*"}; - @Autowired - private Registration registration; - - @Value("${management.endpoints.web.base-path:/actuator}") - private String actuatorBasePath; @Value("${server.servlet.context-path:}") private String contextPath; @@ -83,34 +55,40 @@ public class WebMvcServiceRegistryAutoConfiguration { @Autowired private ObjectProvider dispatcherServletRegistrationBeanProvider; - @EventListener(WebEndpointMappingsReadyEvent.class) - public void onApplicationEvent(WebEndpointMappingsReadyEvent event) { - Collection webEndpointMappings = event.getMappings(); - attachWebMappingsMetadata(registration, webEndpointMappings); + /** + * {@inheritDoc} + *

    Returns the servlet context path configured via {@code server.servlet.context-path}. + * + *

    Example Usage: + *

    {@code
    +     * // With application property: server.servlet.context-path=/api
    +     * String contextPath = config.getContextPath(); // returns "/api"
    +     * }
    + * + * @return the servlet context path + */ + @Override + protected String getContextPath() { + return this.contextPath; } - private void attachWebMappingsMetadata(Registration registration, Collection webEndpointMappings) { - Set mappings = new HashSet<>(webEndpointMappings); - excludeMappings(mappings); - attachMetadata(contextPath, registration, mappings); - } - - private void excludeMappings(Set mappings) { - Iterator iterator = mappings.iterator(); - while (iterator.hasNext()) { - WebEndpointMapping mapping = iterator.next(); - String[] patterns = mapping.getPatterns(); - if (isBuiltInFilterMapping(patterns) - || isDispatcherServletMapping(mapping, patterns) - || isActuatorWebEndpointMapping(patterns) - ) { - if (logger.isTraceEnabled()) { - logger.trace("The '{}' was removed", mapping); - } - iterator.remove(); - } - - } + /** + * {@inheritDoc} + *

    Excludes built-in Spring filter mappings and the default DispatcherServlet mapping. + * + *

    Example Usage: + *

    {@code
    +     * boolean excluded = config.isExcludedMapping(mapping, new String[]{"/*"});
    +     * // returns true if the mapping matches a built-in filter or DispatcherServlet
    +     * }
    + * + * @param mapping the {@link WebEndpointMapping} to evaluate + * @param patterns the URL patterns associated with the mapping + * @return {@code true} if the mapping is a built-in filter or DispatcherServlet mapping + */ + @Override + protected boolean isExcludedMapping(WebEndpointMapping mapping, String[] patterns) { + return isBuiltInFilterMapping(patterns) || isDispatcherServletMapping(mapping, patterns); } private boolean isBuiltInFilterMapping(String[] patterns) { @@ -129,26 +107,16 @@ private boolean isBuiltInFilterMapping(String[] patterns) { } private boolean isDispatcherServletMapping(WebEndpointMapping mapping, String[] patterns) { - DispatcherServletRegistrationBean registrationBean = dispatcherServletRegistrationBeanProvider.getIfAvailable(); - if (registrationBean != null) { + ValueHolder found = new ValueHolder<>(FALSE); + this.dispatcherServletRegistrationBeanProvider.ifAvailable(registrationBean -> { Object source = mapping.getEndpoint(); String servletName = registrationBean.getServletName(); if (Objects.equals(source, servletName)) { Collection urlMappings = registrationBean.getUrlMappings(); - return matchUrlPatterns(urlMappings, patterns); - } - } - return false; - } - - - private boolean isActuatorWebEndpointMapping(String[] patterns) { - for (String pattern : patterns) { - if (pattern.startsWith(actuatorBasePath)) { - return true; + found.setValue(matchUrlPatterns(urlMappings, patterns)); } - } - return false; + }); + return found.getValue(); } private boolean matchFilter(FilterRegistrationBean filterRegistrationBean, String[] patterns) { @@ -157,8 +125,7 @@ private boolean matchFilter(FilterRegistrationBean filterRegistrationBean, Strin } private boolean matchUrlPatterns(Collection urlPatterns, String[] patterns) { - String[] urlPatternsArray = urlPatterns.isEmpty() ? DEFAULT_URL_MAPPINGS : urlPatterns.toArray(new String[0]); - return Arrays.equals(urlPatternsArray, patterns); + String[] urlPatternsArray = urlPatterns.isEmpty() ? DEFAULT_URL_MAPPINGS : urlPatterns.toArray(EMPTY_STRING_ARRAY); + return arrayEquals(urlPatternsArray, patterns); } - -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebServiceRegistryAutoConfiguration.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebServiceRegistryAutoConfiguration.java new file mode 100644 index 00000000..427f88f1 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebServiceRegistryAutoConfiguration.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.microsphere.spring.cloud.client.service.registry.autoconfigure; + +import io.microsphere.logging.Logger; +import io.microsphere.spring.cloud.client.service.registry.condition.ConditionalOnAutoServiceRegistrationEnabled; +import io.microsphere.spring.web.event.WebEndpointMappingsReadyEvent; +import io.microsphere.spring.web.metadata.WebEndpointMapping; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Configuration; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import static io.microsphere.logging.LoggerFactory.getLogger; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.attachMetadata; + +/** + * Auto-Configuration class for {@link ServiceRegistry ServiceRegistry} on the Spring WebMVC Application + * + * @author Mercy + * @since 1.0.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(name = { + "io.microsphere.spring.web.metadata.WebEndpointMapping", + "io.microsphere.spring.web.event.WebEndpointMappingsReadyEvent" +}) +@ConditionalOnBean(Registration.class) +@ConditionalOnAutoServiceRegistrationEnabled +@AutoConfigureAfter(value = { + ServiceRegistryAutoConfiguration.class +}) +public abstract class WebServiceRegistryAutoConfiguration implements ApplicationListener { + + protected final Logger logger = getLogger(getClass()); + + @Value("${management.endpoints.web.base-path:/actuator}") + protected String actuatorBasePath; + + /** + * Handles {@link WebEndpointMappingsReadyEvent} by attaching web endpoint mapping metadata + * to all available {@link Registration} instances. + * + *

    Example Usage: + *

    {@code
    +     * // This listener is invoked automatically by the Spring event system:
    +     * // When WebEndpointMappingsReadyEvent is published, metadata is attached
    +     * // to each Registration bean in the ApplicationContext.
    +     * }
    + * + * @param event the {@link WebEndpointMappingsReadyEvent} containing the web endpoint mappings + */ + @Override + public final void onApplicationEvent(WebEndpointMappingsReadyEvent event) { + ApplicationContext context = event.getApplicationContext(); + ObjectProvider registrationProvider = context.getBeanProvider(Registration.class); + Collection webEndpointMappings = event.getMappings(); + registrationProvider.forEach(registration -> attachWebMappingsMetadata(registration, webEndpointMappings)); + } + + private void attachWebMappingsMetadata(Registration registration, Collection webEndpointMappings) { + Set mappings = new HashSet<>(webEndpointMappings); + excludeMappings(mappings); + attachMetadata(getContextPath(), registration, mappings); + } + + void excludeMappings(Collection mappings) { + Iterator iterator = mappings.iterator(); + while (iterator.hasNext()) { + WebEndpointMapping mapping = iterator.next(); + String[] patterns = mapping.getPatterns(); + if (isExcludedMapping(mapping, patterns) || isActuatorWebEndpointMapping(mapping, patterns)) { + if (logger.isTraceEnabled()) { + logger.trace("The '{}' was excluded", mapping); + } + iterator.remove(); + } + } + } + + /** + * Get the context path of the Spring Web Application + * + * @return context path + */ + protected abstract String getContextPath(); + + /** + * Is excluded mapping + * + * @param mapping {@link WebEndpointMapping} + * @param patterns patterns + * @return if excluded mapping, return true, or false + */ + protected abstract boolean isExcludedMapping(WebEndpointMapping mapping, String[] patterns); + + /** + * Is actuator {@link WebEndpointMapping} + * + * @param mapping {@link WebEndpointMapping} + * @param patterns patterns + * @return if actuator {@link WebEndpointMapping}, return true, or false + */ + protected boolean isActuatorWebEndpointMapping(WebEndpointMapping mapping, String[] patterns) { + for (String pattern : patterns) { + if (pattern.startsWith(actuatorBasePath)) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/condition/ConditionalOnAutoServiceRegistrationEnabled.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/condition/ConditionalOnAutoServiceRegistrationEnabled.java index 9da6d954..720d5f0e 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/condition/ConditionalOnAutoServiceRegistrationEnabled.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/condition/ConditionalOnAutoServiceRegistrationEnabled.java @@ -21,12 +21,13 @@ import org.springframework.core.annotation.AliasFor; import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.SERVICE_REGISTRY_AUTO_REGISTRATION_ENABLED_PROPERTY_NAME; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * The conditional annotation meta-annotates {@link ConditionalOnProperty @ConditionalOnProperty} for @@ -37,10 +38,10 @@ * @see ConditionalOnProperty * @since 1.0.0 */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RUNTIME) +@Target({TYPE, METHOD}) @Documented -@ConditionalOnProperty(name = SERVICE_REGISTRY_AUTO_REGISTRATION_ENABLED_PROPERTY_NAME) +@ConditionalOnProperty(name = SERVICE_REGISTRY_AUTO_REGISTRATION_ENABLED_PROPERTY_NAME, matchIfMissing = true) public @interface ConditionalOnAutoServiceRegistrationEnabled { /** @@ -51,4 +52,4 @@ */ @AliasFor(annotation = ConditionalOnProperty.class, attribute = "matchIfMissing") boolean matchIfMissing() default true; -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/condition/ConditionalOnMultipleRegistrationEnabled.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/condition/ConditionalOnMultipleRegistrationEnabled.java index 9e854728..7051e2ee 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/condition/ConditionalOnMultipleRegistrationEnabled.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/condition/ConditionalOnMultipleRegistrationEnabled.java @@ -4,12 +4,13 @@ import org.springframework.context.annotation.Conditional; import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.MULTIPLE_REGISTRATION_ENABLED_PROPERTY_NAME; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * {@link Conditional @Conditional} that checks whether the multiple service registry enabled @@ -18,10 +19,9 @@ * @author Mercy * @since 1.0 */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RUNTIME) +@Target({TYPE, METHOD}) @Documented @ConditionalOnProperty(name = MULTIPLE_REGISTRATION_ENABLED_PROPERTY_NAME) public @interface ConditionalOnMultipleRegistrationEnabled { - -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/constants/InstanceConstants.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/constants/InstanceConstants.java index d27e5587..a6c651e2 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/constants/InstanceConstants.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/constants/InstanceConstants.java @@ -33,4 +33,14 @@ public interface InstanceConstants { * The meta-data name of Web Context Path */ String WEB_CONTEXT_PATH_METADATA_NAME = "web.context-path"; -} + + /** + * The metadata name of management + */ + String MANAGEMENT_PORT_METADATA_NAME = "management-port"; + + /** + * The metadata name of start time + */ + String START_TIME_METADATA_NAME = "start-time"; +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/endpoint/AbstractServiceRegistrationEndpoint.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/endpoint/AbstractServiceRegistrationEndpoint.java index a0869aa0..c9737de9 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/endpoint/AbstractServiceRegistrationEndpoint.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/endpoint/AbstractServiceRegistrationEndpoint.java @@ -1,5 +1,6 @@ package io.microsphere.spring.cloud.client.service.registry.endpoint; +import io.microsphere.logging.Logger; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; @@ -11,13 +12,17 @@ import org.springframework.cloud.client.serviceregistry.ServiceRegistry; import org.springframework.context.ApplicationListener; +import static io.microsphere.logging.LoggerFactory.getLogger; + /** * Abstract Endpoint for Service Registration * * @author Mercy * @since 1.0.0 */ -public class AbstractServiceRegistrationEndpoint implements SmartInitializingSingleton, ApplicationListener { +public abstract class AbstractServiceRegistrationEndpoint implements SmartInitializingSingleton, ApplicationListener { + + protected final Logger logger = getLogger(getClass()); @Value("${spring.application.name}") protected String applicationName; @@ -41,6 +46,17 @@ public class AbstractServiceRegistrationEndpoint implements SmartInitializingSin protected static boolean running; + /** + * {@inheritDoc} + *

    Initializes the {@link Registration}, {@link ServiceRegistry}, and + * {@link AbstractAutoServiceRegistration} from available bean providers. + * + *

    Example Usage: + *

    {@code
    +     * // Called automatically by the Spring container after all singletons are instantiated.
    +     * // Ensures registration, serviceRegistry, and serviceRegistration fields are populated.
    +     * }
    + */ @Override public void afterSingletonsInstantiated() { this.registration = registrationProvider.getIfAvailable(); @@ -48,18 +64,69 @@ public void afterSingletonsInstantiated() { this.serviceRegistration = autoServiceRegistrationProvider.getIfAvailable(); } + /** + * {@inheritDoc} + *

    Captures the web server port and detects the running state of the + * {@link AbstractAutoServiceRegistration}. + * + *

    Example Usage: + *

    {@code
    +     * // Called automatically when the embedded web server has been initialized.
    +     * // After this event, the port and running state are available.
    +     * }
    + * + * @param event the {@link WebServerInitializedEvent} carrying the initialized web server + */ @Override public void onApplicationEvent(WebServerInitializedEvent event) { WebServer webServer = event.getWebServer(); this.port = webServer.getPort(); - this.running = serviceRegistration == null ? true : serviceRegistration.isRunning(); + this.running = detectRunning(serviceRegistration); + } + + /** + * Detects whether the given {@link AbstractAutoServiceRegistration} is currently running. + * + *

    Example Usage: + *

    {@code
    +     * boolean running = AbstractServiceRegistrationEndpoint.detectRunning(serviceRegistration);
    +     * }
    + * + * @param serviceRegistration the {@link AbstractAutoServiceRegistration} to check, may be {@code null} + * @return {@code true} if the service registration is running, {@code false} otherwise + */ + static boolean detectRunning(AbstractAutoServiceRegistration serviceRegistration) { + return serviceRegistration == null ? false : serviceRegistration.isRunning(); } + /** + * Returns whether the service registration is currently running. + * + *

    Example Usage: + *

    {@code
    +     * if (endpoint.isRunning()) {
    +     *     // service is registered and running
    +     * }
    +     * }
    + * + * @return {@code true} if the service registration is running, {@code false} otherwise + */ protected boolean isRunning() { return running; } + /** + * Sets the running state of the service registration. + * + *

    Example Usage: + *

    {@code
    +     * endpoint.setRunning(true);  // mark service as running
    +     * endpoint.setRunning(false); // mark service as stopped
    +     * }
    + * + * @param running {@code true} to mark the service as running, {@code false} otherwise + */ public void setRunning(boolean running) { this.running = running; } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/endpoint/ServiceDeregistrationEndpoint.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/endpoint/ServiceDeregistrationEndpoint.java index f1dbdf72..84b0e481 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/endpoint/ServiceDeregistrationEndpoint.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/endpoint/ServiceDeregistrationEndpoint.java @@ -1,7 +1,5 @@ package io.microsphere.spring.cloud.client.service.registry.endpoint; -import io.microsphere.logging.Logger; -import io.microsphere.logging.LoggerFactory; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; import org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration; @@ -17,14 +15,25 @@ @Endpoint(id = "serviceDeregistration") public class ServiceDeregistrationEndpoint extends AbstractServiceRegistrationEndpoint { - private final Logger logger = LoggerFactory.getLogger(getClass()); - + /** + * Deregisters the service from the {@link ServiceRegistry} if it is currently running. + * This is a write operation exposed via the {@code /actuator/serviceDeregistration} endpoint. + * + *

    Example Usage: + *

    {@code
    +     * // Via actuator HTTP POST to /actuator/serviceDeregistration
    +     * ServiceDeregistrationEndpoint endpoint = context.getBean(ServiceDeregistrationEndpoint.class);
    +     * boolean wasRunning = endpoint.stop();
    +     * }
    + * + * @return {@code true} if the service was running before deregistration, {@code false} otherwise + */ @WriteOperation public boolean stop() { boolean isRunning = isRunning(); if (isRunning) { serviceRegistry.deregister(registration); - if(logger.isInfoEnabled()) { + if (logger.isInfoEnabled()) { logger.info("Service[name : '{}'] is deregistered!", applicationName); } setRunning(false); @@ -35,4 +44,4 @@ public boolean stop() { } return isRunning; } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/endpoint/ServiceRegistrationEndpoint.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/endpoint/ServiceRegistrationEndpoint.java index 360f85bb..bd6e78ef 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/endpoint/ServiceRegistrationEndpoint.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/endpoint/ServiceRegistrationEndpoint.java @@ -1,17 +1,16 @@ package io.microsphere.spring.cloud.client.service.registry.endpoint; -import io.microsphere.logging.Logger; -import io.microsphere.logging.LoggerFactory; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; import org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration; -import org.springframework.util.ReflectionUtils; -import java.lang.reflect.Method; import java.util.LinkedHashMap; import java.util.Map; +import static io.microsphere.lang.function.ThrowableSupplier.execute; +import static io.microsphere.reflect.MethodUtils.invokeMethod; + /** * The {@link Endpoint @Endpoint} for Service Registration * @@ -23,43 +22,61 @@ @Endpoint(id = "serviceRegistration") public class ServiceRegistrationEndpoint extends AbstractServiceRegistrationEndpoint { - private final Logger logger = LoggerFactory.getLogger(getClass()); - + /** + * Returns metadata about the current service registration, including application name, + * registration details, port, status, and running state. + * This is a read operation exposed via the {@code /actuator/serviceRegistration} endpoint. + * + *

    Example Usage: + *

    {@code
    +     * // Via actuator HTTP GET to /actuator/serviceRegistration
    +     * ServiceRegistrationEndpoint endpoint = context.getBean(ServiceRegistrationEndpoint.class);
    +     * Map metadata = endpoint.metadata();
    +     * String appName = (String) metadata.get("application-name");
    +     * }
    + * + * @return a {@link Map} containing service registration metadata + */ @ReadOperation public Map metadata() { - - Map metadata = new LinkedHashMap<>(); - + Map metadata = new LinkedHashMap<>(16); metadata.put("application-name", applicationName); metadata.put("registration", registration); metadata.put("port", port); metadata.put("status", serviceRegistry.getStatus(registration)); metadata.put("running", isRunning()); - - if (serviceRegistration != null) { - metadata.put("enabled", invoke("isEnabled")); - metadata.put("phase", serviceRegistration.getPhase()); - metadata.put("order", serviceRegistration.getOrder()); - if (Boolean.TRUE.equals(invoke("shouldRegisterManagement"))) { - metadata.put("managementRegistration", invoke("getManagementRegistration")); - } - metadata.put("config", invoke("getConfiguration")); - } - + metadata.put("enabled", invoke("isEnabled")); + metadata.put("phase", serviceRegistration.getPhase()); + metadata.put("order", serviceRegistration.getOrder()); + metadata.put("managementRegistration", invoke("getManagementRegistration")); + metadata.put("config", invoke("getConfiguration")); return metadata; } + /** + * Registers the service with the {@link ServiceRegistry} if it is not already running. + * This is a write operation exposed via the {@code /actuator/serviceRegistration} endpoint. + * + *

    Example Usage: + *

    {@code
    +     * // Via actuator HTTP POST to /actuator/serviceRegistration
    +     * ServiceRegistrationEndpoint endpoint = context.getBean(ServiceRegistrationEndpoint.class);
    +     * boolean wasAlreadyRunning = endpoint.start();
    +     * }
    + * + * @return {@code true} if the service was already running, {@code false} if it was newly registered + */ @WriteOperation public boolean start() { boolean isRunning = isRunning(); if (!isRunning) { serviceRegistry.register(registration); setRunning(true); - if(logger.isTraceEnabled()) { - logger.trace("Service[name : '{}'] is registered!", applicationName); + if (logger.isInfoEnabled()) { + logger.info("Service[name : '{}'] is registered!", applicationName); } } else { - if(logger.isWarnEnabled()) { + if (logger.isWarnEnabled()) { logger.warn("Service[name : '{}'] was registered!", applicationName); } } @@ -67,15 +84,6 @@ public boolean start() { } private Object invoke(String methodName) { - Object returnValue = null; - try { - Class serviceRegistrationClass = AbstractAutoServiceRegistration.class; - Method method = serviceRegistrationClass.getDeclaredMethod(methodName); - ReflectionUtils.makeAccessible(method); - returnValue = method.invoke(serviceRegistration); - } catch (Throwable e) { - logger.error("Invocation on method :" + methodName + "is failed", e); - } - return returnValue; + return execute(() -> invokeMethod(serviceRegistration, methodName), e -> null); } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationDeregisteredEvent.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationDeregisteredEvent.java index 294583c2..45ef54ce 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationDeregisteredEvent.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationDeregisteredEvent.java @@ -19,6 +19,8 @@ import org.springframework.cloud.client.serviceregistry.Registration; import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import static io.microsphere.spring.cloud.client.service.registry.event.RegistrationEvent.Type.DEREGISTERED; + /** * The after-{@link ServiceRegistry#deregister(Registration) deregister} event. * @@ -28,17 +30,40 @@ */ public class RegistrationDeregisteredEvent extends RegistrationEvent { + /** + * Create a new {@link RegistrationDeregisteredEvent} indicating that a + * {@link Registration} has been deregistered from the {@link ServiceRegistry}. + * + *

    Example Usage: + *

    {@code
    +     * ServiceRegistry registry = ...;
    +     * Registration registration = ...;
    +     * RegistrationDeregisteredEvent event = new RegistrationDeregisteredEvent(registry, registration);
    +     * applicationContext.publishEvent(event);
    +     * }
    + * + * @param registry the {@link ServiceRegistry} that performed the deregistration + * @param source the {@link Registration} that was deregistered + */ public RegistrationDeregisteredEvent(ServiceRegistry registry, Registration source) { super(registry, source); } + /** + * Returns the {@link Type} of this event, which is always {@link Type#DEREGISTERED}. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationDeregisteredEvent event = ...;
    +     * RegistrationEvent.Type type = event.getType();
    +     * // type == RegistrationEvent.Type.DEREGISTERED
    +     * }
    + * + * @return {@link Type#DEREGISTERED} + */ @Override - public boolean isRegistered() { - return false; + public Type getType() { + return DEREGISTERED; } - @Override - public boolean isDeregistered() { - return true; - } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationEvent.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationEvent.java index d51d26a6..939afba7 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationEvent.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationEvent.java @@ -22,6 +22,11 @@ import org.springframework.lang.NonNull; import org.springframework.util.Assert; +import static io.microsphere.spring.cloud.client.service.registry.event.RegistrationEvent.Type.DEREGISTERED; +import static io.microsphere.spring.cloud.client.service.registry.event.RegistrationEvent.Type.PRE_DEREGISTERED; +import static io.microsphere.spring.cloud.client.service.registry.event.RegistrationEvent.Type.PRE_REGISTERED; +import static io.microsphere.spring.cloud.client.service.registry.event.RegistrationEvent.Type.REGISTERED; + /** * The Spring event for {@link ServiceRegistry} * @@ -37,6 +42,20 @@ public abstract class RegistrationEvent extends ApplicationEvent { private final ServiceRegistry registry; + /** + * Create a new {@link RegistrationEvent} with the given {@link ServiceRegistry} and {@link Registration}. + * + *

    Example Usage: + *

    {@code
    +     * ServiceRegistry registry = ...;
    +     * Registration registration = ...;
    +     * // Typically used through a subclass such as RegistrationRegisteredEvent
    +     * RegistrationRegisteredEvent event = new RegistrationRegisteredEvent(registry, registration);
    +     * }
    + * + * @param registry the {@link ServiceRegistry} that triggered the event + * @param source the {@link Registration} associated with the event + */ public RegistrationEvent(ServiceRegistry registry, Registration source) { super(source); Assert.notNull(registry, "The 'registry' must not be null"); @@ -80,7 +99,7 @@ public ServiceRegistry getRegistry() { * @return true if pre-registered */ public final boolean isPreRegistered() { - return !isRegistered(); + return getType() == PRE_REGISTERED; } /** @@ -89,7 +108,9 @@ public final boolean isPreRegistered() { * * @return true if registered */ - public abstract boolean isRegistered(); + public final boolean isRegistered() { + return getType() == REGISTERED; + } /** * Current event is raised before the {@link #getRegistration() registration} is @@ -98,7 +119,7 @@ public final boolean isPreRegistered() { * @return true if pre-deregistered */ public final boolean isPreDeregistered() { - return !isDeregistered(); + return getType() == PRE_DEREGISTERED; } /** @@ -107,6 +128,40 @@ public final boolean isPreDeregistered() { * * @return true if deregistered */ - public abstract boolean isDeregistered(); + public final boolean isDeregistered() { + return getType() == DEREGISTERED; + } -} + /** + * Get the {@link Type} of the {@link RegistrationEvent} + * + * @return non-null + */ + public abstract Type getType(); + + /** + * The {@link Type} of the {@link RegistrationEvent} + */ + public static enum Type { + + /** + * The {@link RegistrationPreRegisteredEvent} + */ + PRE_REGISTERED, + + /** + * The {@link RegistrationRegisteredEvent} + */ + REGISTERED, + + /** + * The {@link RegistrationPreDeregisteredEvent} + */ + PRE_DEREGISTERED, + + /** + * The {@link RegistrationDeregisteredEvent} + */ + DEREGISTERED + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationPreDeregisteredEvent.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationPreDeregisteredEvent.java index 6e194752..b98fc529 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationPreDeregisteredEvent.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationPreDeregisteredEvent.java @@ -19,6 +19,8 @@ import org.springframework.cloud.client.serviceregistry.Registration; import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import static io.microsphere.spring.cloud.client.service.registry.event.RegistrationEvent.Type.PRE_DEREGISTERED; + /** * The before-{@link ServiceRegistry#deregister(Registration) deregister} event. * @@ -28,17 +30,39 @@ */ public class RegistrationPreDeregisteredEvent extends RegistrationEvent { + /** + * Create a new {@link RegistrationPreDeregisteredEvent} indicating that a + * {@link Registration} is about to be deregistered from the {@link ServiceRegistry}. + * + *

    Example Usage: + *

    {@code
    +     * ServiceRegistry registry = ...;
    +     * Registration registration = ...;
    +     * RegistrationPreDeregisteredEvent event = new RegistrationPreDeregisteredEvent(registry, registration);
    +     * applicationContext.publishEvent(event);
    +     * }
    + * + * @param registry the {@link ServiceRegistry} that will perform the deregistration + * @param source the {@link Registration} to be deregistered + */ public RegistrationPreDeregisteredEvent(ServiceRegistry registry, Registration source) { super(registry, source); } + /** + * Returns the {@link Type} of this event, which is always {@link Type#PRE_DEREGISTERED}. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationPreDeregisteredEvent event = ...;
    +     * RegistrationEvent.Type type = event.getType();
    +     * // type == RegistrationEvent.Type.PRE_DEREGISTERED
    +     * }
    + * + * @return {@link Type#PRE_DEREGISTERED} + */ @Override - public boolean isRegistered() { - return false; - } - - @Override - public boolean isDeregistered() { - return false; + public Type getType() { + return PRE_DEREGISTERED; } } \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationPreRegisteredEvent.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationPreRegisteredEvent.java index 2e0d192d..8ae5c353 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationPreRegisteredEvent.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationPreRegisteredEvent.java @@ -19,6 +19,8 @@ import org.springframework.cloud.client.serviceregistry.Registration; import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import static io.microsphere.spring.cloud.client.service.registry.event.RegistrationEvent.Type.PRE_REGISTERED; + /** * The before-{@link ServiceRegistry#register(Registration) register} event. * @@ -28,17 +30,39 @@ */ public class RegistrationPreRegisteredEvent extends RegistrationEvent { + /** + * Create a new {@link RegistrationPreRegisteredEvent} indicating that a + * {@link Registration} is about to be registered with the {@link ServiceRegistry}. + * + *

    Example Usage: + *

    {@code
    +     * ServiceRegistry registry = ...;
    +     * Registration registration = ...;
    +     * RegistrationPreRegisteredEvent event = new RegistrationPreRegisteredEvent(registry, registration);
    +     * applicationContext.publishEvent(event);
    +     * }
    + * + * @param registry the {@link ServiceRegistry} that will perform the registration + * @param source the {@link Registration} to be registered + */ public RegistrationPreRegisteredEvent(ServiceRegistry registry, Registration source) { super(registry, source); } + /** + * Returns the {@link Type} of this event, which is always {@link Type#PRE_REGISTERED}. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationPreRegisteredEvent event = ...;
    +     * RegistrationEvent.Type type = event.getType();
    +     * // type == RegistrationEvent.Type.PRE_REGISTERED
    +     * }
    + * + * @return {@link Type#PRE_REGISTERED} + */ @Override - public boolean isRegistered() { - return false; - } - - @Override - public boolean isDeregistered() { - return false; + public Type getType() { + return PRE_REGISTERED; } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationRegisteredEvent.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationRegisteredEvent.java index cc20ed1d..42932dc7 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationRegisteredEvent.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/registry/event/RegistrationRegisteredEvent.java @@ -19,6 +19,8 @@ import org.springframework.cloud.client.serviceregistry.Registration; import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import static io.microsphere.spring.cloud.client.service.registry.event.RegistrationEvent.Type.REGISTERED; + /** * The after-{@link ServiceRegistry#register(Registration) register} event. * @@ -28,17 +30,39 @@ */ public class RegistrationRegisteredEvent extends RegistrationEvent { + /** + * Create a new {@link RegistrationRegisteredEvent} indicating that a + * {@link Registration} has been registered with the {@link ServiceRegistry}. + * + *

    Example Usage: + *

    {@code
    +     * ServiceRegistry registry = ...;
    +     * Registration registration = ...;
    +     * RegistrationRegisteredEvent event = new RegistrationRegisteredEvent(registry, registration);
    +     * applicationContext.publishEvent(event);
    +     * }
    + * + * @param registry the {@link ServiceRegistry} that performed the registration + * @param source the {@link Registration} that was registered + */ public RegistrationRegisteredEvent(ServiceRegistry registry, Registration source) { super(registry, source); } + /** + * Returns the {@link Type} of this event, which is always {@link Type#REGISTERED}. + * + *

    Example Usage: + *

    {@code
    +     * RegistrationRegisteredEvent event = ...;
    +     * RegistrationEvent.Type type = event.getType();
    +     * // type == RegistrationEvent.Type.REGISTERED
    +     * }
    + * + * @return {@link Type#REGISTERED} + */ @Override - public boolean isRegistered() { - return true; - } - - @Override - public boolean isDeregistered() { - return false; + public Type getType() { + return REGISTERED; } } \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/util/ServiceInstanceUtils.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/util/ServiceInstanceUtils.java index af21b9f1..1dae14d9 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/util/ServiceInstanceUtils.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/client/service/util/ServiceInstanceUtils.java @@ -16,24 +16,44 @@ */ package io.microsphere.spring.cloud.client.service.util; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; +import io.microsphere.annotation.Nonnull; +import io.microsphere.annotation.Nullable; +import io.microsphere.json.JSONArray; +import io.microsphere.json.JSONObject; import io.microsphere.logging.Logger; -import io.microsphere.logging.LoggerFactory; import io.microsphere.spring.web.metadata.WebEndpointMapping; +import io.microsphere.spring.web.metadata.WebEndpointMapping.Builder; import io.microsphere.util.BaseUtils; +import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.ServiceInstance; +import java.net.URI; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.StringJoiner; +import static io.microsphere.collection.ListUtils.newArrayList; +import static io.microsphere.constants.SeparatorConstants.LINE_SEPARATOR; +import static io.microsphere.constants.SymbolConstants.COLON_CHAR; +import static io.microsphere.constants.SymbolConstants.COMMA; +import static io.microsphere.constants.SymbolConstants.LEFT_SQUARE_BRACKET; +import static io.microsphere.constants.SymbolConstants.RIGHT_SQUARE_BRACKET; +import static io.microsphere.json.JSONUtils.jsonArray; +import static io.microsphere.json.JSONUtils.readArray; +import static io.microsphere.logging.LoggerFactory.getLogger; import static io.microsphere.net.URLUtils.decode; import static io.microsphere.net.URLUtils.encode; import static io.microsphere.spring.cloud.client.service.registry.constants.InstanceConstants.WEB_CONTEXT_PATH_METADATA_NAME; import static io.microsphere.spring.cloud.client.service.registry.constants.InstanceConstants.WEB_MAPPINGS_METADATA_NAME; +import static io.microsphere.spring.web.metadata.WebEndpointMapping.Kind.valueOf; +import static io.microsphere.spring.web.metadata.WebEndpointMapping.of; +import static io.microsphere.util.StringUtils.EMPTY_STRING; +import static io.microsphere.util.StringUtils.EMPTY_STRING_ARRAY; +import static io.microsphere.util.StringUtils.isBlank; +import static java.lang.String.valueOf; +import static java.net.URI.create; +import static java.util.Collections.emptyList; /** * {@link ServiceInstance} Utilities class @@ -43,36 +63,192 @@ */ public class ServiceInstanceUtils extends BaseUtils { - private static final Logger logger = LoggerFactory.getLogger(ServiceInstanceUtils.class); + private static final Logger logger = getLogger(ServiceInstanceUtils.class); + /** + * Attach {@link WebEndpointMapping} metadata to the given {@link ServiceInstance}. + * The web endpoint mappings are serialized as JSON and stored in the service instance's + * metadata under the {@link io.microsphere.spring.cloud.client.service.registry.constants.InstanceConstants#WEB_MAPPINGS_METADATA_NAME} key. + * + *

    Example Usage: + *

    {@code
    +     * ServiceInstance serviceInstance = new DefaultServiceInstance("id", "service", "localhost", 8080, false);
    +     * Collection mappings = new ArrayList<>();
    +     * ServiceInstanceUtils.attachMetadata("/context", serviceInstance, mappings);
    +     * }
    + * + * @param contextPath the web application context path + * @param serviceInstance the {@link ServiceInstance} to attach metadata to + * @param webEndpointMappings the collection of {@link WebEndpointMapping}s to attach + */ public static void attachMetadata(String contextPath, ServiceInstance serviceInstance, Collection webEndpointMappings) { Map metadata = serviceInstance.getMetadata(); - StringJoiner jsonBuilder = new StringJoiner(",", "[", "]"); + StringJoiner jsonBuilder = new StringJoiner(COMMA + LINE_SEPARATOR, LEFT_SQUARE_BRACKET, RIGHT_SQUARE_BRACKET); webEndpointMappings.stream().map(WebEndpointMapping::toJSON).forEach(jsonBuilder::add); String json = jsonBuilder.toString(); + if (logger.isTraceEnabled()) { + logger.trace("Web Endpoint Mappings JSON: \n{}", json); + } + json = json.replace(LINE_SEPARATOR, EMPTY_STRING); + String encodedJson = encode(json); + metadata.put(WEB_CONTEXT_PATH_METADATA_NAME, contextPath); - try { - String encodedJson = encode(json); - metadata.put(WEB_MAPPINGS_METADATA_NAME, encodedJson); - } catch (IllegalArgumentException e) { - logger.error("The JSON content of WebEndpointMappings can't be encoded : {}", json, e); + metadata.put(WEB_MAPPINGS_METADATA_NAME, encodedJson); + if (logger.isTraceEnabled()) { + logger.trace("ServiceInstance's metadata :"); + metadata.forEach((name, value) -> logger.trace("{} : {}", name, value)); } } + /** + * Get {@link WebEndpointMapping}s from {@link ServiceInstance} + * + * @param serviceInstance {@link ServiceInstance} + * @return {@link WebEndpointMapping}s + */ + @Nonnull public static Collection getWebEndpointMappings(ServiceInstance serviceInstance) { - List webEndpointMappings = Collections.emptyList(); + String encodedJSON = getMetadata(serviceInstance, WEB_MAPPINGS_METADATA_NAME); + return parseWebEndpointMappings(encodedJSON); + } + + /** + * Get the String representation of {@link ServiceInstance#getUri()} + * + * @param instance {@link ServiceInstance} + * @return the String representation of {@link ServiceInstance#getUri()} + */ + @Nonnull + public static String getUriString(ServiceInstance instance) { + boolean isSecure = instance.isSecure(); + String prefix = isSecure ? "https://" : "http://"; + String host = instance.getHost(); + int port = instance.getPort(); + if (port <= 0) { + port = isSecure ? 443 : 80; + } + String portString = valueOf(port); + StringBuilder urlStringBuilder = new StringBuilder((isSecure ? 9 : 8) + host.length() + portString.length()); + urlStringBuilder.append(prefix) + .append(host) + .append(COLON_CHAR) + .append(portString); + return urlStringBuilder.toString(); + } + + /** + * Alternative method of {@link ServiceInstance#getUri()} with the better performance + * + * @param serviceInstance {@link ServiceInstance} + * @return {@link URI} instance + * @see DefaultServiceInstance#getUri(ServiceInstance) + */ + @Nonnull + public static URI getUri(ServiceInstance serviceInstance) { + String uriString = getUriString(serviceInstance); + return create(uriString); + } + + /** + * Get metadata by metadataName + * + * @param serviceInstance {@link ServiceInstance} + * @param metadataName metadataName + * @return metadata value + */ + @Nullable + public static String getMetadata(ServiceInstance serviceInstance, String metadataName) { Map metadata = serviceInstance.getMetadata(); - String encodedJSON = metadata.get(WEB_MAPPINGS_METADATA_NAME); - if (encodedJSON != null) { - try { - String json = decode(encodedJSON); - ObjectMapper objectMapper = new ObjectMapper(); - webEndpointMappings = objectMapper.readValue(json, new TypeReference>() { - }); - } catch (Throwable e) { - logger.error("The encoded JSON content of WebEndpointMappings can't be parsed : {}", encodedJSON, e); - } + return metadata.get(metadataName); + } + + /** + * Set metadata by metadataName + * + * @param serviceInstance {@link ServiceInstance} + * @param metadataName metadataName + * @param metadataValue metadata value + * @return the previous value associated with metadataName if found, null otherwise + */ + public static String setMetadata(ServiceInstance serviceInstance, String metadataName, String metadataValue) { + Map metadata = serviceInstance.getMetadata(); + return metadata.put(metadataName, metadataValue); + } + + /** + * Remove metadata by metadataName + * + * @param serviceInstance {@link ServiceInstance} + * @param metadataName metadataName + * @return the value associated with metadataName if found, null otherwise + */ + public static String removeMetadata(ServiceInstance serviceInstance, String metadataName) { + Map metadata = serviceInstance.getMetadata(); + return metadata.remove(metadataName); + } + + /** + * Set properties from source to target + * + * @param source source {@link ServiceInstance} + * @param target target {@link DefaultServiceInstance} + */ + public static void setProperties(ServiceInstance source, DefaultServiceInstance target) { + target.setInstanceId(source.getInstanceId()); + target.setServiceId(source.getServiceId()); + target.setSecure(source.isSecure()); + target.setHost(source.getHost()); + target.setPort(source.getPort()); + Map metadata = source.getMetadata(); + metadata.clear(); + metadata.putAll(source.getMetadata()); + } + + static List parseWebEndpointMappings(String encodedJSON) { + if (isBlank(encodedJSON)) { + return emptyList(); + } + String json = decode(encodedJSON); + JSONArray jsonArray = jsonArray(json); + int size = jsonArray.length(); + List webEndpointMappings = newArrayList(size); + for (int i = 0; i < size; i++) { + JSONObject jsonObject = jsonArray.optJSONObject(i); + WebEndpointMapping webEndpointMapping = parseWebEndpointMapping(jsonObject); + webEndpointMappings.add(webEndpointMapping); } return webEndpointMappings; } -} + + static WebEndpointMapping parseWebEndpointMapping(JSONObject jsonObject) { + String kind = jsonObject.optString("kind"); + int id = jsonObject.optInt("id"); + boolean negated = jsonObject.optBoolean("negated"); + String[] patterns = getArray(jsonObject, "patterns"); + String[] methods = getArray(jsonObject, "methods"); + String[] params = getArray(jsonObject, "params"); + String[] headers = getArray(jsonObject, "headers"); + String[] consumes = getArray(jsonObject, "consumes"); + String[] produces = getArray(jsonObject, "produces"); + Builder builder = of(valueOf(kind)) + .endpoint(Integer.valueOf(id)) + .patterns(patterns) + .methods(methods) + .params(params) + .headers(headers) + .consumes(consumes) + .produces(produces); + if (negated) { + builder.negate(); + } + return builder.build(); + } + + static String[] getArray(JSONObject jsonObject, String name) { + JSONArray jsonArray = jsonObject.optJSONArray(name); + return jsonArray == null ? EMPTY_STRING_ARRAY : readArray(jsonArray, String.class); + } + + private ServiceInstanceUtils() { + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/commons/condition/ConditionalOnUtilEnabled.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/commons/condition/ConditionalOnUtilEnabled.java new file mode 100644 index 00000000..a24ed4cb --- /dev/null +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/commons/condition/ConditionalOnUtilEnabled.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.microsphere.spring.cloud.commons.condition; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.commons.util.UtilAutoConfiguration; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static io.microsphere.spring.cloud.commons.constants.SpringCloudPropertyConstants.UTIL_ENABLED_PROPERTY_NAME; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * The conditional annotation meta-annotates {@link ConditionalOnProperty @ConditionalOnProperty} for + * {@link UtilAutoConfiguration} enabled. + * + * @author
    Mercy + * @see UtilAutoConfiguration + * @see ConditionalOnProperty + * @since 1.0.0 + */ +@Retention(RUNTIME) +@Target({TYPE, METHOD}) +@Documented +@ConditionalOnProperty(name = UTIL_ENABLED_PROPERTY_NAME, matchIfMissing = true) +public @interface ConditionalOnUtilEnabled { +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/commons/constants/CommonsPropertyConstants.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/commons/constants/CommonsPropertyConstants.java index 62082630..b30fbe48 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/commons/constants/CommonsPropertyConstants.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/commons/constants/CommonsPropertyConstants.java @@ -16,9 +16,9 @@ */ package io.microsphere.spring.cloud.commons.constants; -import org.springframework.cloud.client.CommonsClientAutoConfiguration; -import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration; +import io.microsphere.annotation.ConfigurationProperty; +import static io.microsphere.annotation.ConfigurationProperty.APPLICATION_SOURCE; import static io.microsphere.constants.PropertyConstants.ENABLED_PROPERTY_NAME; import static io.microsphere.constants.PropertyConstants.MICROSPHERE_PROPERTY_NAME_PREFIX; @@ -28,32 +28,7 @@ * @author Mercy * @since 1.0.0 */ -public interface CommonsPropertyConstants { - - /** - * The property name prefix of Spring Cloud properties : "spring.cloud." - */ - String SPRING_CLOUD_PROPERTY_PREFIX = "spring.cloud."; - - /** - * The property name prefix of Spring Cloud Service Registry : "spring.cloud.service-registry." - */ - String SERVICE_REGISTRY_PROPERTY_PREFIX = SPRING_CLOUD_PROPERTY_PREFIX + "service-registry."; - - /** - * The property name for Spring Cloud Service Registry Auto-Registration Feature : - * "spring.cloud.service-registry.auto-registration.enabled" - * - * @see AutoServiceRegistrationAutoConfiguration - */ - String SERVICE_REGISTRY_AUTO_REGISTRATION_ENABLED_PROPERTY_NAME = SERVICE_REGISTRY_PROPERTY_PREFIX + "auto-registration." + ENABLED_PROPERTY_NAME; - - /** - * The property name for enabling Spring Cloud Features : "spring.cloud.features.enabled" - * - * @see CommonsClientAutoConfiguration.ActuatorConfiguration - */ - String FEATURES_ENABLED_PROPERTY_NAME = SPRING_CLOUD_PROPERTY_PREFIX + "features." + ENABLED_PROPERTY_NAME; +public interface CommonsPropertyConstants extends SpringCloudPropertyConstants { /** * The property name prefix of Microsphere Cloud : "microsphere.spring.cloud." @@ -68,21 +43,39 @@ public interface CommonsPropertyConstants { /** * The property name for Multiple Service Registry Enabled Feature : "microsphere.spring.cloud.multiple-registration.enabled" */ + @ConfigurationProperty( + type = boolean.class, + defaultValue = "false", + source = APPLICATION_SOURCE + ) String MULTIPLE_REGISTRATION_ENABLED_PROPERTY_NAME = MICROSPHERE_SPRING_CLOUD_PROPERTY_NAME_PREFIX + "multiple-registration." + ENABLED_PROPERTY_NAME; /** * The property name for Default Service Registry Type : "microsphere.spring.cloud.default-registration.type" */ + @ConfigurationProperty( + type = Class.class, + source = APPLICATION_SOURCE + ) String MULTIPLE_REGISTRATION_DEFAULT_REGISTRATION_PROPERTY_NAME = MICROSPHERE_SPRING_CLOUD_PROPERTY_NAME_PREFIX + "default-registration.type"; /** * The property name for Default Service Registry Type : "microsphere.spring.cloud.default-service-registry.type" */ + @ConfigurationProperty( + type = Class.class, + source = APPLICATION_SOURCE + ) String MULTIPLE_REGISTRATION_DEFAULT_REGISTRY_PROPERTY_NAME = MICROSPHERE_SPRING_CLOUD_PROPERTY_NAME_PREFIX + "default-service-registry.type"; /** * The property name for Composite Service Registry Enabled Feature : "microsphere.spring.cloud.composite-registration.enabled" */ + @ConfigurationProperty( + type = boolean.class, + defaultValue = "false", + source = APPLICATION_SOURCE + ) String COMPOSITE_REGISTRATION_ENABLED_PROPERTY_NAME = MICROSPHERE_SPRING_CLOUD_PROPERTY_NAME_PREFIX + "composite-registration." + ENABLED_PROPERTY_NAME; -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/commons/constants/SpringCloudPropertyConstants.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/commons/constants/SpringCloudPropertyConstants.java new file mode 100644 index 00000000..0fce3f30 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/commons/constants/SpringCloudPropertyConstants.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.commons.constants; + +import io.microsphere.annotation.ConfigurationProperty; +import org.springframework.cloud.client.CommonsClientAutoConfiguration; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration; +import org.springframework.cloud.commons.util.UtilAutoConfiguration; + +import static io.microsphere.annotation.ConfigurationProperty.APPLICATION_SOURCE; +import static io.microsphere.constants.PropertyConstants.ENABLED_PROPERTY_NAME; + +/** + * The property constants for Spring Cloud + * + * @author Mercy + * @see AutoServiceRegistrationAutoConfiguration + * @see CommonsClientAutoConfiguration + * @since 1.0.0 + */ +public interface SpringCloudPropertyConstants { + + /** + * The property name prefix of Spring Cloud properties : "spring.cloud." + */ + String SPRING_CLOUD_PROPERTY_PREFIX = "spring.cloud."; + + /** + * The property name prefix of Spring Cloud Service Registry : "spring.cloud.service-registry." + */ + String SERVICE_REGISTRY_PROPERTY_PREFIX = SPRING_CLOUD_PROPERTY_PREFIX + "service-registry."; + + /** + * The property name for Spring Cloud Service Registry Auto-Registration Feature : + * "spring.cloud.service-registry.auto-registration.enabled" + * + * @see AutoServiceRegistrationAutoConfiguration + */ + @ConfigurationProperty( + type = boolean.class, + defaultValue = "true", + source = APPLICATION_SOURCE + ) + String SERVICE_REGISTRY_AUTO_REGISTRATION_ENABLED_PROPERTY_NAME = SERVICE_REGISTRY_PROPERTY_PREFIX + "auto-registration." + ENABLED_PROPERTY_NAME; + + /** + * The property name for enabling Spring Cloud Features : "spring.cloud.features.enabled" + * + * @see CommonsClientAutoConfiguration.ActuatorConfiguration + */ + @ConfigurationProperty( + type = boolean.class, + defaultValue = "true", + source = APPLICATION_SOURCE + ) + String FEATURES_ENABLED_PROPERTY_NAME = SPRING_CLOUD_PROPERTY_PREFIX + "features." + ENABLED_PROPERTY_NAME; + + /** + * The property name for enabling Spring Cloud Load-Balancer : "spring.cloud.loadbalancer.enabled" + * + * @see org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration + */ + @ConfigurationProperty( + type = boolean.class, + defaultValue = "true", + source = APPLICATION_SOURCE + ) + String LOAD_BALANCER_ENABLED_PROPERTY_NAME = SPRING_CLOUD_PROPERTY_PREFIX + "loadbalancer." + ENABLED_PROPERTY_NAME; + + + /** + * The property name for enabling Spring Cloud Util : "spring.cloud.util.enabled" + * + * @see UtilAutoConfiguration + */ + @ConfigurationProperty( + type = boolean.class, + defaultValue = "true", + source = APPLICATION_SOURCE + ) + String UTIL_ENABLED_PROPERTY_NAME = SPRING_CLOUD_PROPERTY_PREFIX + "util." + ENABLED_PROPERTY_NAME; +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/constants/FaultTolerancePropertyConstants.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/constants/FaultTolerancePropertyConstants.java index c6071fe6..18b65053 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/constants/FaultTolerancePropertyConstants.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/constants/FaultTolerancePropertyConstants.java @@ -1,7 +1,10 @@ package io.microsphere.spring.cloud.fault.tolerance.constants; +import io.microsphere.annotation.ConfigurationProperty; + import java.util.concurrent.TimeUnit; +import static io.microsphere.annotation.ConfigurationProperty.APPLICATION_SOURCE; import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.MICROSPHERE_SPRING_CLOUD_PROPERTY_NAME_PREFIX; /** @@ -22,24 +25,24 @@ public interface FaultTolerancePropertyConstants { */ String LOAD_BALANCER_PROPERTY_PREFIX = FAULT_TOLERANCE_PROPERTY_NAME_PREFIX + "load-balancer."; - /** - * The metadata name of management - */ - String MANAGEMENT_PORT_METADATA_NAME = "management-port"; - - /** - * The metadata name of start time - */ - String START_TIME_METADATA_NAME = "start-time"; - /** * The metadata name of warm-up time */ + @ConfigurationProperty( + type = long.class, + defaultValue = "600000", + source = APPLICATION_SOURCE + ) String WARMUP_TIME_PROPERTY_NAME = FAULT_TOLERANCE_PROPERTY_NAME_PREFIX + "warmup-time"; /** * The property name of weight */ + @ConfigurationProperty( + type = int.class, + defaultValue = "100", + source = APPLICATION_SOURCE + ) String WEIGHT_PROPERTY_NAME = FAULT_TOLERANCE_PROPERTY_NAME_PREFIX + "weight"; /** @@ -51,5 +54,4 @@ public interface FaultTolerancePropertyConstants { * The default property value of weight : 100 */ int DEFAULT_WEIGHT_PROPERTY_VALUE = 100; - -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/loadbalancer/WeightedRoundRobin.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/loadbalancer/WeightedRoundRobin.java index b9596a00..b2991743 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/loadbalancer/WeightedRoundRobin.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/loadbalancer/WeightedRoundRobin.java @@ -15,44 +15,153 @@ public class WeightedRoundRobin { private volatile int weight; - private LongAdder current = new LongAdder(); + LongAdder current = new LongAdder(); private volatile long lastUpdate; + /** + * Create a new {@link WeightedRoundRobin} instance with the given identifier. + * + *

    Example Usage: + *

    {@code
    +     * WeightedRoundRobin wrr = new WeightedRoundRobin("server-1");
    +     * wrr.setWeight(5);
    +     * }
    + * + * @param id the unique identifier for this weighted round-robin entry + */ public WeightedRoundRobin(String id) { this.id = id; } + /** + * Get the unique identifier for this {@link WeightedRoundRobin} entry. + * + *

    Example Usage: + *

    {@code
    +     * WeightedRoundRobin wrr = new WeightedRoundRobin("server-1");
    +     * String id = wrr.getId(); // "server-1"
    +     * }
    + * + * @return the identifier + */ public String getId() { return id; } + /** + * Get the current weight of this {@link WeightedRoundRobin} entry. + * + *

    Example Usage: + *

    {@code
    +     * WeightedRoundRobin wrr = new WeightedRoundRobin("server-1");
    +     * wrr.setWeight(5);
    +     * int weight = wrr.getWeight(); // 5
    +     * }
    + * + * @return the current weight + */ public int getWeight() { return weight; } + /** + * Set the weight for this {@link WeightedRoundRobin} entry and reset the current counter. + * + *

    Example Usage: + *

    {@code
    +     * WeightedRoundRobin wrr = new WeightedRoundRobin("server-1");
    +     * wrr.setWeight(10);
    +     * }
    + * + * @param weight the new weight value + */ public void setWeight(int weight) { this.weight = weight; current.reset(); } + /** + * Increase the current counter by the weight value and return the updated value. + * Used during weighted round-robin selection to accumulate the weight for this entry. + * + *

    Example Usage: + *

    {@code
    +     * WeightedRoundRobin wrr = new WeightedRoundRobin("server-1");
    +     * wrr.setWeight(5);
    +     * long current = wrr.increaseCurrent(); // 5
    +     * current = wrr.increaseCurrent();      // 10
    +     * }
    + * + * @return the updated current counter value + */ public long increaseCurrent() { current.add(weight); return current.longValue(); } + /** + * Subtract the total weight from the current counter after this entry has been selected. + * This is part of the weighted round-robin algorithm to reduce the selected entry's counter. + * + *

    Example Usage: + *

    {@code
    +     * WeightedRoundRobin wrr = new WeightedRoundRobin("server-1");
    +     * wrr.setWeight(5);
    +     * wrr.increaseCurrent();
    +     * wrr.sel(10); // subtract total weight of all entries
    +     * }
    + * + * @param total the total weight of all entries to subtract + */ public void sel(int total) { current.add(-1 * total); } + /** + * Get the timestamp of the last update to this {@link WeightedRoundRobin} entry. + * + *

    Example Usage: + *

    {@code
    +     * WeightedRoundRobin wrr = new WeightedRoundRobin("server-1");
    +     * wrr.setLastUpdate(System.currentTimeMillis());
    +     * long lastUpdate = wrr.getLastUpdate();
    +     * }
    + * + * @return the last update timestamp in milliseconds + */ public long getLastUpdate() { return lastUpdate; } + /** + * Set the timestamp of the last update to this {@link WeightedRoundRobin} entry. + * + *

    Example Usage: + *

    {@code
    +     * WeightedRoundRobin wrr = new WeightedRoundRobin("server-1");
    +     * wrr.setLastUpdate(System.currentTimeMillis());
    +     * }
    + * + * @param lastUpdate the last update timestamp in milliseconds + */ public void setLastUpdate(long lastUpdate) { this.lastUpdate = lastUpdate; } + /** + * Returns a string representation of this {@link WeightedRoundRobin} including + * its id, weight, current counter, and last update timestamp. + * + *

    Example Usage: + *

    {@code
    +     * WeightedRoundRobin wrr = new WeightedRoundRobin("server-1");
    +     * wrr.setWeight(5);
    +     * String s = wrr.toString(); // "WeightedRoundRobin[id='server-1', weight=5, current=0, lastUpdate=0]"
    +     * }
    + * + * @return a string representation of this entry + */ @Override public String toString() { return new StringJoiner(", ", WeightedRoundRobin.class.getSimpleName() + "[", "]") @@ -62,4 +171,4 @@ public String toString() { .add("lastUpdate=" + lastUpdate) .toString(); } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/loadbalancer/util/LoadBalancerUtils.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/loadbalancer/util/LoadBalancerUtils.java index 52ed5717..83424784 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/loadbalancer/util/LoadBalancerUtils.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/loadbalancer/util/LoadBalancerUtils.java @@ -8,10 +8,6 @@ */ public abstract class LoadBalancerUtils { - private LoadBalancerUtils() { - throw new UnsupportedOperationException(); - } - /** * Calculate the weight according to the uptime proportion of warmup time * the new weight will be within 1(inclusive) to weight(inclusive) @@ -25,4 +21,7 @@ public static int calculateWarmupWeight(long uptime, long warmup, int weight) { int ww = (int) (Math.round(Math.pow((uptime / (double) warmup), 2) * weight)); return ww < 1 ? 1 : (Math.min(ww, weight)); } -} + + private LoadBalancerUtils() { + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/tomcat/autoconfigure/TomcatFaultToleranceAutoConfiguration.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/tomcat/autoconfigure/TomcatFaultToleranceAutoConfiguration.java index b4ee6190..b32ad811 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/tomcat/autoconfigure/TomcatFaultToleranceAutoConfiguration.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/tomcat/autoconfigure/TomcatFaultToleranceAutoConfiguration.java @@ -16,7 +16,8 @@ */ package io.microsphere.spring.cloud.fault.tolerance.tomcat.autoconfigure; -import io.microsphere.spring.cloud.fault.tolerance.constants.FaultTolerancePropertyConstants; +import io.microsphere.annotation.ConfigurationProperty; +import io.microsphere.constants.PropertyConstants; import io.microsphere.spring.cloud.fault.tolerance.tomcat.event.TomcatDynamicConfigurationListener; import org.apache.catalina.startup.Tomcat; import org.springframework.beans.factory.ObjectProvider; @@ -35,8 +36,9 @@ import org.springframework.cloud.context.environment.EnvironmentChangeEvent; import org.springframework.context.event.EventListener; -import static io.microsphere.constants.PropertyConstants.ENABLED_PROPERTY_NAME; -import static io.microsphere.spring.cloud.fault.tolerance.tomcat.autoconfigure.TomcatFaultToleranceAutoConfiguration.TOMCAT_PROPERTY_PREFIX; +import static io.microsphere.annotation.ConfigurationProperty.APPLICATION_SOURCE; +import static io.microsphere.spring.cloud.fault.tolerance.constants.FaultTolerancePropertyConstants.FAULT_TOLERANCE_PROPERTY_NAME_PREFIX; +import static io.microsphere.spring.cloud.fault.tolerance.tomcat.autoconfigure.TomcatFaultToleranceAutoConfiguration.ENABLED_PROPERTY_NAME; import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.SERVLET; /** @@ -44,7 +46,6 @@ * @since 1.0.0 */ @ConditionalOnProperty( - prefix = TOMCAT_PROPERTY_PREFIX, name = ENABLED_PROPERTY_NAME, matchIfMissing = true ) @@ -59,8 +60,30 @@ }) public class TomcatFaultToleranceAutoConfiguration { - public static final String TOMCAT_PROPERTY_PREFIX = FaultTolerancePropertyConstants.FAULT_TOLERANCE_PROPERTY_NAME_PREFIX + "tomcat"; + public static final String TOMCAT_PROPERTY_PREFIX = FAULT_TOLERANCE_PROPERTY_NAME_PREFIX + "tomcat"; + /** + * The property name to Tomcat's fault-tolerance enabled or not: "microsphere.spring.cloud.fault-tolerance.tomcat.enabled" + */ + @ConfigurationProperty( + type = boolean.class, + defaultValue = "true", + source = APPLICATION_SOURCE + ) + public static final String ENABLED_PROPERTY_NAME = TOMCAT_PROPERTY_PREFIX + "." + PropertyConstants.ENABLED_PROPERTY_NAME; + + /** + * Handles the {@link WebServerInitializedEvent} to register a {@link TomcatDynamicConfigurationListener} + * when the embedded web server is a {@link TomcatWebServer}. + * + *

    Example Usage: + *

    {@code
    +     * // Automatically invoked by Spring when WebServerInitializedEvent is published.
    +     * // The listener is registered as an ApplicationListener on the web application context.
    +     * }
    + * + * @param event the {@link WebServerInitializedEvent} triggered after the web server starts + */ @EventListener(WebServerInitializedEvent.class) public void onWebServerInitializedEvent(WebServerInitializedEvent event) { WebServerApplicationContext webServerApplicationContext = event.getApplicationContext(); @@ -77,4 +100,4 @@ public void onWebServerInitializedEvent(WebServerInitializedEvent event) { }); } } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/tomcat/event/TomcatDynamicConfigurationListener.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/tomcat/event/TomcatDynamicConfigurationListener.java index 712ca8b1..500e3f82 100644 --- a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/tomcat/event/TomcatDynamicConfigurationListener.java +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/fault/tolerance/tomcat/event/TomcatDynamicConfigurationListener.java @@ -18,7 +18,6 @@ import io.microsphere.logging.Logger; -import io.microsphere.logging.LoggerFactory; import org.apache.catalina.connector.Connector; import org.apache.coyote.AbstractProtocol; import org.apache.coyote.ProtocolHandler; @@ -37,6 +36,8 @@ import java.util.Set; import java.util.stream.Collectors; +import static io.microsphere.logging.LoggerFactory.getLogger; +import static io.microsphere.spring.beans.BeanUtils.isBeanPresent; import static io.microsphere.spring.boot.context.properties.bind.util.BindUtils.bind; import static io.microsphere.spring.core.env.EnvironmentUtils.getProperties; import static io.microsphere.spring.core.env.PropertySourcesUtils.getSubProperties; @@ -51,7 +52,7 @@ */ public class TomcatDynamicConfigurationListener implements ApplicationListener { - private static final Logger logger = LoggerFactory.getLogger(TomcatDynamicConfigurationListener.class); + private static final Logger logger = getLogger(TomcatDynamicConfigurationListener.class); private static final String SERVER_PROPERTIES_PREFIX = "server"; @@ -67,6 +68,25 @@ public class TomcatDynamicConfigurationListener implements ApplicationListenerExample Usage: + *
    {@code
    +     * TomcatWebServer tomcatWebServer = ...;
    +     * ServerProperties serverProperties = ...;
    +     * ConfigurableApplicationContext context = ...;
    +     * TomcatDynamicConfigurationListener listener =
    +     *     new TomcatDynamicConfigurationListener(tomcatWebServer, serverProperties, context);
    +     * context.addApplicationListener(listener);
    +     * }
    + * + * @param tomcatWebServer the {@link TomcatWebServer} to reconfigure dynamically + * @param serverProperties the current {@link ServerProperties} + * @param context the {@link ConfigurableApplicationContext} for environment access + */ public TomcatDynamicConfigurationListener(TomcatWebServer tomcatWebServer, ServerProperties serverProperties, ConfigurableApplicationContext context) { this.tomcatWebServer = tomcatWebServer; @@ -75,7 +95,7 @@ public TomcatDynamicConfigurationListener(TomcatWebServer tomcatWebServer, Serve this.context = context; this.environment = environment; - this.configurationPropertiesRebinderPresent = isBeanPresent(ConfigurationPropertiesRebinder.class); + this.configurationPropertiesRebinderPresent = isBeanPresent(context, ConfigurationPropertiesRebinder.class); initCurrentServerProperties(); } @@ -84,14 +104,22 @@ private void initCurrentServerProperties() { this.currentServerProperties = getCurrentServerProperties(environment); } - private boolean isBeanPresent(Class beanType) { - return context.getBeanProvider(beanType).getIfAvailable() != null; - } - + /** + * Handles an {@link EnvironmentChangeEvent} by reconfiguring the Tomcat connector + * if any server-related properties have changed. + * + *

    Example Usage: + *

    {@code
    +     * // Automatically invoked by Spring when an EnvironmentChangeEvent is published.
    +     * // Reconfigures Tomcat settings such as thread pool size, connection timeout, etc.
    +     * }
    + * + * @param event the {@link EnvironmentChangeEvent} containing the changed property keys + */ @Override public void onApplicationEvent(EnvironmentChangeEvent event) { if (!isSourceFrom(event)) { - if(logger.isTraceEnabled()) { + if (logger.isTraceEnabled()) { logger.trace("Current context[id : '{}'] receives the other changed property names : {}", context.getId(), event.getKeys()); } return; @@ -99,7 +127,7 @@ public void onApplicationEvent(EnvironmentChangeEvent event) { Set serverPropertyNames = filterServerPropertyNames(event); if (serverPropertyNames.isEmpty()) { - if(logger.isTraceEnabled()) { + if (logger.isTraceEnabled()) { logger.trace("Current context[id : '{}'] does not receive the property change of ServerProperties, keys : {}", context.getId(), event.getKeys()); } return; @@ -127,8 +155,8 @@ private boolean isServerPropertyName(String propertyName) { private void configureTomcatIfChanged(Set serverPropertyNames) { ServerProperties refreshableServerProperties = getRefreshableServerProperties(serverPropertyNames); - if(logger.isTraceEnabled()) { - logger.debug("The ServerProperties property is changed to: {}", getProperties(environment, serverPropertyNames)); + if (logger.isTraceEnabled()) { + logger.trace("The ServerProperties property is changed to: {}", getProperties(environment, serverPropertyNames)); } configureConnector(refreshableServerProperties); // Reset current ServerProperties @@ -155,7 +183,21 @@ private void configureConnector(ServerProperties refreshableServerProperties) { configureHttp11Protocol(refreshableServerProperties, connector, protocolHandler); } - private void configureProtocol(ServerProperties refreshableServerProperties, ProtocolHandler protocolHandler) { + /** + * Configure the Tomcat {@link AbstractProtocol} settings such as thread pool sizes, + * accept count, connection timeout, and max connections from the refreshed {@link ServerProperties}. + * + *

    Example Usage: + *

    {@code
    +     * ServerProperties refreshedProperties = ...;
    +     * ProtocolHandler protocolHandler = connector.getProtocolHandler();
    +     * listener.configureProtocol(refreshedProperties, protocolHandler);
    +     * }
    + * + * @param refreshableServerProperties the refreshed {@link ServerProperties} to apply + * @param protocolHandler the {@link ProtocolHandler} to configure + */ + void configureProtocol(ServerProperties refreshableServerProperties, ProtocolHandler protocolHandler) { if (protocolHandler instanceof AbstractProtocol) { ServerProperties.Tomcat refreshableTomcatProperties = refreshableServerProperties.getTomcat(); @@ -203,7 +245,23 @@ private void configureProtocol(ServerProperties refreshableServerProperties, Pro } } - private void configureHttp11Protocol(ServerProperties refreshableServerProperties, Connector connector, ProtocolHandler protocolHandler) { + /** + * Configure the Tomcat {@link AbstractHttp11Protocol} settings such as max HTTP header size, + * max swallow size, and max HTTP form POST size from the refreshed {@link ServerProperties}. + * + *

    Example Usage: + *

    {@code
    +     * ServerProperties refreshedProperties = ...;
    +     * Connector connector = tomcatWebServer.getTomcat().getConnector();
    +     * ProtocolHandler protocolHandler = connector.getProtocolHandler();
    +     * listener.configureHttp11Protocol(refreshedProperties, connector, protocolHandler);
    +     * }
    + * + * @param refreshableServerProperties the refreshed {@link ServerProperties} to apply + * @param connector the Tomcat {@link Connector} + * @param protocolHandler the {@link ProtocolHandler} to configure + */ + void configureHttp11Protocol(ServerProperties refreshableServerProperties, Connector connector, ProtocolHandler protocolHandler) { if (protocolHandler instanceof AbstractHttp11Protocol) { AbstractHttp11Protocol protocol = (AbstractHttp11Protocol) protocolHandler; @@ -213,8 +271,8 @@ private void configureHttp11Protocol(ServerProperties refreshableServerPropertie // Max HTTP Header Size configure("Tomcat HTTP Headers' max size(bytes)") - .value(refreshableServerProperties::getMaxHttpHeaderSize) - .compare(currentServerProperties::getMaxHttpHeaderSize) + .value(refreshableServerProperties::getMaxHttpRequestHeaderSize) + .compare(currentServerProperties::getMaxHttpRequestHeaderSize) .as(this::toIntBytes) .on(this::isPositive) .apply(protocol::setMaxHttpHeaderSize); @@ -245,9 +303,20 @@ private int toIntBytes(DataSize dataSize) { return (int) dataSize.toBytes(); } - private boolean isPositive(int value) { + /** + * Check whether the given integer value is positive (greater than zero). + * + *

    Example Usage: + *

    {@code
    +     * boolean result = listener.isPositive(10);  // true
    +     * boolean result2 = listener.isPositive(0);  // false
    +     * }
    + * + * @param value the value to check + * @return {@code true} if the value is greater than zero + */ + boolean isPositive(int value) { return value > 0; } - -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/loadbalancer/condition/ConditionalOnLoadBalancerEnabled.java b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/loadbalancer/condition/ConditionalOnLoadBalancerEnabled.java new file mode 100644 index 00000000..5b04c06e --- /dev/null +++ b/microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/loadbalancer/condition/ConditionalOnLoadBalancerEnabled.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.microsphere.spring.cloud.loadbalancer.condition; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static io.microsphere.spring.cloud.commons.constants.SpringCloudPropertyConstants.LOAD_BALANCER_ENABLED_PROPERTY_NAME; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * The conditional annotation meta-annotates {@link ConditionalOnProperty @ConditionalOnProperty} for + * LoadBalancer enabled. + * + * @author Mercy + * @see org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration + * @see ConditionalOnProperty + * @since 1.0.0 + */ +@Retention(RUNTIME) +@Target({TYPE, METHOD}) +@Documented +@ConditionalOnProperty(name = LOAD_BALANCER_ENABLED_PROPERTY_NAME, havingValue = "true", matchIfMissing = true) +public @interface ConditionalOnLoadBalancerEnabled { +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/resources/META-INF/spring.factories b/microsphere-spring-cloud-commons/src/main/resources/META-INF/spring.factories index a0de4297..7586f87c 100644 --- a/microsphere-spring-cloud-commons/src/main/resources/META-INF/spring.factories +++ b/microsphere-spring-cloud-commons/src/main/resources/META-INF/spring.factories @@ -1,10 +1 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -io.microsphere.spring.cloud.client.discovery.autoconfigure.DiscoveryClientAutoConfiguration,\ -io.microsphere.spring.cloud.client.service.registry.autoconfigure.ServiceRegistryAutoConfiguration,\ -io.microsphere.spring.cloud.client.service.registry.autoconfigure.WebMvcServiceRegistryAutoConfiguration,\ -io.microsphere.spring.cloud.client.service.registry.autoconfigure.WebFluxServiceRegistryAutoConfiguration,\ -io.microsphere.spring.cloud.client.service.registry.autoconfigure.SimpleAutoServiceRegistrationAutoConfiguration,\ -io.microsphere.spring.cloud.client.service.registry.actuate.autoconfigure.ServiceRegistrationEndpointAutoConfiguration,\ -io.microsphere.spring.cloud.fault.tolerance.tomcat.autoconfigure.TomcatFaultToleranceAutoConfiguration - com.alibaba.cloud.nacos.registry.NacosServiceRegistry = com.alibaba.cloud.nacos.registry.NacosRegistration \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/microsphere-spring-cloud-commons/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 7be8d06f..fd5ff8c4 100644 --- a/microsphere-spring-cloud-commons/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/microsphere-spring-cloud-commons/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,6 +1,8 @@ io.microsphere.spring.cloud.client.discovery.autoconfigure.DiscoveryClientAutoConfiguration +io.microsphere.spring.cloud.client.discovery.autoconfigure.ReactiveDiscoveryClientAutoConfiguration io.microsphere.spring.cloud.client.service.registry.autoconfigure.ServiceRegistryAutoConfiguration io.microsphere.spring.cloud.client.service.registry.autoconfigure.WebMvcServiceRegistryAutoConfiguration io.microsphere.spring.cloud.client.service.registry.autoconfigure.WebFluxServiceRegistryAutoConfiguration io.microsphere.spring.cloud.client.service.registry.autoconfigure.SimpleAutoServiceRegistrationAutoConfiguration -io.microsphere.spring.cloud.client.service.registry.actuate.autoconfigure.ServiceRegistrationEndpointAutoConfiguration \ No newline at end of file +io.microsphere.spring.cloud.client.service.registry.actuate.autoconfigure.ServiceRegistrationEndpointAutoConfiguration +io.microsphere.spring.cloud.fault.tolerance.tomcat.autoconfigure.TomcatFaultToleranceAutoConfiguration \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/condition/ConditionalOnFeaturesEnabledTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/condition/ConditionalOnFeaturesEnabledTest.java index eab52d40..80986d9b 100644 --- a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/condition/ConditionalOnFeaturesEnabledTest.java +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/condition/ConditionalOnFeaturesEnabledTest.java @@ -17,43 +17,15 @@ package io.microsphere.spring.cloud.client.condition; /** - * {@link ConditionalOnFeaturesEnabled} Test + * {@link io.microsphere.spring.cloud.client.condition.ConditionalOnFeaturesEnabled} Test * * @author Mercy - * @see ConditionalOnFeaturesEnabled + * @see io.microsphere.spring.cloud.client.condition.ConditionalOnFeaturesEnabled * @since 1.0.0 */ -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit.jupiter.SpringExtension; +import io.microsphere.spring.cloud.test.ConditionalOnPropertyEnabledTest; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -@ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = { - ConditionalOnFeaturesEnabledTest.FeaturesConfiguration.class -}) -@TestPropertySource( - properties = { - "spring.cloud.features.enabled=true" - } -) -public class ConditionalOnFeaturesEnabledTest { - - @ConditionalOnFeaturesEnabled - static class FeaturesConfiguration { - } - - @Autowired - private ObjectProvider featuresConfigurationProvider; - - @Test - public void test() { - assertNotNull(featuresConfigurationProvider.getIfAvailable()); - } +@ConditionalOnFeaturesEnabled +class ConditionalOnFeaturesEnabledTest extends ConditionalOnPropertyEnabledTest { } diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/ReactiveDiscoveryClientAdapterTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/ReactiveDiscoveryClientAdapterTest.java new file mode 100644 index 00000000..2f65a00d --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/ReactiveDiscoveryClientAdapterTest.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.discovery; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClient; +import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryProperties; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Scheduler; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +import static io.microsphere.collection.Lists.ofList; +import static io.microsphere.spring.cloud.client.discovery.ReactiveDiscoveryClientAdapter.toList; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtilsTest.createDefaultServiceInstance; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static reactor.core.scheduler.Schedulers.immediate; +import static reactor.core.scheduler.Schedulers.newSingle; + +/** + * {@link ReactiveDiscoveryClientAdapter} + * + * @author Mercy + * @see ReactiveDiscoveryClientAdapter + * @see ReactiveDiscoveryClient + * @see DiscoveryClient + * @since 1.0.0 + */ +class ReactiveDiscoveryClientAdapterTest { + + private DefaultServiceInstance serviceInstance; + + private String appName = "test-service"; + + private SimpleReactiveDiscoveryProperties properties; + + private ReactiveDiscoveryClient client; + + private ReactiveDiscoveryClientAdapter adapter; + + @BeforeEach + void setUp() { + Map> instances = new HashMap<>(); + this.serviceInstance = createDefaultServiceInstance(); + this.appName = this.serviceInstance.getServiceId(); + instances.put(appName, ofList(this.serviceInstance)); + this.properties = new SimpleReactiveDiscoveryProperties(); + this.properties.setInstances(instances); + this.client = new SimpleReactiveDiscoveryClient(properties); + this.adapter = new ReactiveDiscoveryClientAdapter(client); + } + + @Test + void testDescription() { + assertEquals("Simple Reactive Discovery Client", this.adapter.description()); + } + + @Test + void testGetInstances() { + List serviceInstances = this.adapter.getInstances(this.appName); + assertEquals(1, serviceInstances.size()); + assertSame(this.serviceInstance, serviceInstances.get(0)); + } + + @Test + void testGetServices() { + List services = this.adapter.getServices(); + assertEquals(1, services.size()); + assertSame(appName, services.get(0)); + } + + @Test + void testProbe() { + this.adapter.probe(); + } + + @Test + void testGetOrder() { + assertEquals(this.client.getOrder(), this.adapter.getOrder()); + } + + @Test + void testToList() throws Exception { + assertList(immediate(), "1,2,3"); + assertList(newSingle("test"), "1,2,3"); + } + + void assertList(Scheduler scheduler, T... values) throws Exception { + Flux flux = Flux.just(values); + Disposable disposable = scheduler.schedule(() -> { + List list = toList(flux); + assertEquals(ofList(values), list); + }); + if (disposable instanceof Callable) { + ((Callable) disposable).call(); + } + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/UnionDiscoveryClientIntegrationTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/UnionDiscoveryClientIntegrationTest.java new file mode 100644 index 00000000..be79b427 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/UnionDiscoveryClientIntegrationTest.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.microsphere.spring.cloud.client.discovery; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.containers.ComposeContainer; +import org.testcontainers.junit.jupiter.EnabledIfDockerAvailable; + +import java.io.File; +import java.net.URL; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.testcontainers.containers.wait.strategy.Wait.forLogMessage; + +/** + * {@link UnionDiscoveryClient} Integration Test + * + * @author Mercy + * @since 1.0.0 + */ +@EnabledIfSystemProperty(named = "testcontainers.enabled", matches = "true") +@EnabledIfDockerAvailable +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = { + UnionDiscoveryClientIntegrationTest.class +}) +@TestPropertySource(properties = { + "spring.application.name=test", + "spring.cloud.service-registry.auto-registration.enabled=true", + "microsphere.spring.cloud.client.discovery.mode=union", + "microsphere.spring.cloud.multiple-registration.enabled=true" +}) +@EnableAutoConfiguration +class UnionDiscoveryClientIntegrationTest { + + private static ComposeContainer composeContainer; + + @Autowired + private DiscoveryClient discoveryClient; + + @BeforeAll + static void beforeAll() throws Exception { + ClassLoader classLoader = UnionDiscoveryClientIntegrationTest.class.getClassLoader(); + URL resource = classLoader.getResource("META-INF/docker/service-registry-servers.yml"); + File dockerComposeFile = new File(resource.toURI()); + composeContainer = new ComposeContainer(dockerComposeFile); + composeContainer.waitingFor("nacos", forLogMessage(".*Nacos started successfully.*", 1)) + .waitingFor("eureka", forLogMessage(".*Started EurekaServerApplication.*", 1)) + .start(); + } + + @AfterAll + static void afterAll() { + composeContainer.stop(); + } + + @Test + void test() { + assertEquals(CompositeDiscoveryClient.class, discoveryClient.getClass()); + CompositeDiscoveryClient compositeDiscoveryClient = CompositeDiscoveryClient.class.cast(discoveryClient); + List discoveryClients = compositeDiscoveryClient.getDiscoveryClients(); + assertEquals(7, discoveryClients.size()); + assertEquals(UnionDiscoveryClient.class, discoveryClients.get(0).getClass()); + List services = compositeDiscoveryClient.getServices(); + assertTrue(services.size() > 1); + } +} diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/UnionDiscoveryClientTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/UnionDiscoveryClientTest.java new file mode 100644 index 00000000..60b49dc0 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/UnionDiscoveryClientTest.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.microsphere.spring.cloud.client.discovery; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient; +import org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClientAutoConfiguration; +import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClient; +import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration; +import org.springframework.cloud.commons.util.UtilAutoConfiguration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.List; + +import static io.microsphere.collection.Lists.ofList; +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link UnionDiscoveryClient} Test + * + * @author Mercy + * @since 1.0.0 + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = { + UtilAutoConfiguration.class, + SimpleDiscoveryClientAutoConfiguration.class, + CompositeDiscoveryClientAutoConfiguration.class, + UnionDiscoveryClient.class, + UnionDiscoveryClientTest.DummyDiscoveryClient.class, + UnionDiscoveryClientTest.class +}) +@TestPropertySource( + properties = { + "spring.cloud.discovery.client.simple.order=-1", + "spring.cloud.discovery.client.simple.instances.test[0].instanceId=1", + "spring.cloud.discovery.client.simple.instances.test[0].serviceId=test", + "spring.cloud.discovery.client.simple.instances.test[0].host=127.0.0.1", + "spring.cloud.discovery.client.simple.instances.test[0].port=8080", + "spring.cloud.discovery.client.simple.instances.test[0].metadata.key-1=value-1" + } +) +class UnionDiscoveryClientTest { + + @Autowired + private DiscoveryClient discoveryClient; + + @Autowired + private UnionDiscoveryClient unionDiscoveryClient; + + @Test + void test() { + assertEquals(CompositeDiscoveryClient.class, discoveryClient.getClass()); + CompositeDiscoveryClient compositeDiscoveryClient = CompositeDiscoveryClient.class.cast(discoveryClient); + List discoveryClients = compositeDiscoveryClient.getDiscoveryClients(); + assertEquals(3, discoveryClients.size()); + assertEquals(UnionDiscoveryClient.class, discoveryClients.get(0).getClass()); + assertEquals(SimpleDiscoveryClient.class, discoveryClients.get(1).getClass()); + assertEquals(DummyDiscoveryClient.class, discoveryClients.get(2).getClass()); + } + + @Test + void testDescription() { + assertEquals("Composite Discovery Client", this.discoveryClient.description()); + assertEquals("Union Discovery Client", this.unionDiscoveryClient.description()); + } + + @Test + void testGetInstances() { + assertServiceInstances(this.discoveryClient.getInstances("test")); + assertServiceInstances(this.unionDiscoveryClient.getInstances("test")); + + assertTrue(this.discoveryClient.getInstances("unknown").isEmpty()); + assertTrue(this.unionDiscoveryClient.getInstances("unknown").isEmpty()); + } + + @Test + void testGetServices() { + assertServices(this.discoveryClient.getServices()); + assertServices(this.unionDiscoveryClient.getServices()); + } + + void assertServiceInstances(List serviceInstances) { + assertEquals(1, serviceInstances.size()); + ServiceInstance serviceInstance = serviceInstances.get(0); + assertEquals("test", serviceInstance.getServiceId()); + assertEquals("1", serviceInstance.getInstanceId()); + assertEquals("127.0.0.1", serviceInstance.getHost()); + assertEquals(8080, serviceInstance.getPort()); + assertEquals("value-1", serviceInstance.getMetadata().get("key-1")); + } + + void assertServices(List services) { + assertEquals(ofList("test"), services); + } + + static class DummyDiscoveryClient implements DiscoveryClient { + + @Override + public String description() { + return "Dummy Discovery Client"; + } + + @Override + public List getInstances(String serviceId) { + return emptyList(); + } + + @Override + public List getServices() { + return emptyList(); + } + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/UnionDiscoveryClientTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/DiscoveryClientAutoConfigurationTest.java similarity index 77% rename from microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/UnionDiscoveryClientTest.java rename to microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/DiscoveryClientAutoConfigurationTest.java index 882ae70e..76fdc403 100644 --- a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/UnionDiscoveryClientTest.java +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/DiscoveryClientAutoConfigurationTest.java @@ -20,34 +20,37 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient; import org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClientAutoConfiguration; import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClient; import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration; import org.springframework.cloud.commons.util.UtilAutoConfiguration; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.Arrays; import java.util.List; +import static io.microsphere.spring.cloud.client.discovery.autoconfigure.DiscoveryClientAutoConfiguration.DISCOVERY_CLIENT_PROPERTY_PREFIX; +import static io.microsphere.spring.cloud.client.discovery.autoconfigure.DiscoveryClientAutoConfiguration.MODE_PROPERTY_NAME; +import static io.microsphere.spring.cloud.client.discovery.autoconfigure.DiscoveryClientAutoConfiguration.UNION_DISCOVERY_CLIENT_MODE; import static org.junit.jupiter.api.Assertions.assertEquals; /** - * {@link UnionDiscoveryClient} Test + * {@link DiscoveryClientAutoConfiguration} Test * - * @author Mercy + * @author Mercy + * @see DiscoveryClientAutoConfiguration * @since 1.0.0 */ @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = { +@SpringBootTest(classes = { UtilAutoConfiguration.class, SimpleDiscoveryClientAutoConfiguration.class, CompositeDiscoveryClientAutoConfiguration.class, - DiscoveryClientAutoConfiguration.class, - UnionDiscoveryClientTest.class + DiscoveryClientAutoConfiguration.class }) @TestPropertySource( properties = { @@ -59,13 +62,20 @@ "spring.cloud.discovery.client.simple.instances.test[0].metadata.key-1=value-1" } ) -public class UnionDiscoveryClientTest { +class DiscoveryClientAutoConfigurationTest { @Autowired private DiscoveryClient discoveryClient; @Test - public void test() { + void testConstants() { + assertEquals("microsphere.spring.cloud.client.discovery.", DISCOVERY_CLIENT_PROPERTY_PREFIX); + assertEquals("mode", MODE_PROPERTY_NAME); + assertEquals("union", UNION_DISCOVERY_CLIENT_MODE); + } + + @Test + void test() { assertEquals(CompositeDiscoveryClient.class, discoveryClient.getClass()); CompositeDiscoveryClient compositeDiscoveryClient = CompositeDiscoveryClient.class.cast(discoveryClient); List discoveryClients = compositeDiscoveryClient.getDiscoveryClients(); diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/ReactiveDiscoveryClientAutoConfigurationTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/ReactiveDiscoveryClientAutoConfigurationTest.java new file mode 100644 index 00000000..529b0941 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/autoconfigure/ReactiveDiscoveryClientAutoConfigurationTest.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.discovery.autoconfigure; + + +import io.microsphere.spring.cloud.client.discovery.ReactiveDiscoveryClientAdapter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient; +import org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClientAutoConfiguration; +import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration; +import org.springframework.cloud.commons.util.UtilAutoConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.List; + +import static io.microsphere.collection.Lists.ofList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * {@link ReactiveDiscoveryClientAutoConfiguration} Test + * + * @author Mercy + * @see ReactiveDiscoveryClientAutoConfiguration + * @since 1.0.0 + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(classes = { + UtilAutoConfiguration.class, + SimpleReactiveDiscoveryClientAutoConfiguration.class, + CompositeDiscoveryClientAutoConfiguration.class, + ReactiveDiscoveryClientAutoConfiguration.class +}) +@TestPropertySource( + properties = { + "spring.cloud.discovery.client.simple.instances.test[0].instanceId=1", + "spring.cloud.discovery.client.simple.instances.test[0].serviceId=test", + "spring.cloud.discovery.client.simple.instances.test[0].host=127.0.0.1", + "spring.cloud.discovery.client.simple.instances.test[0].port=8080", + "spring.cloud.discovery.client.simple.instances.test[0].metadata.key-1=value-1" + } +) +class ReactiveDiscoveryClientAutoConfigurationTest { + + @Autowired + private DiscoveryClient discoveryClient; + + @Autowired + private ReactiveDiscoveryClientAdapter adapter; + + @Test + void test() { + assertEquals(CompositeDiscoveryClient.class, this.discoveryClient.getClass()); + CompositeDiscoveryClient compositeDiscoveryClient = CompositeDiscoveryClient.class.cast(this.discoveryClient); + List discoveryClients = compositeDiscoveryClient.getDiscoveryClients(); + assertEquals(1, discoveryClients.size()); + assertSame(this.adapter, discoveryClients.get(0)); + List services = compositeDiscoveryClient.getServices(); + assertEquals(ofList("test"), services); + assertEquals(services, discoveryClients.get(0).getServices()); + assertEquals(services, this.adapter.getServices()); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/constants/DiscoveryClientConstantsTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/constants/DiscoveryClientConstantsTest.java new file mode 100644 index 00000000..131a2c1e --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/constants/DiscoveryClientConstantsTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.microsphere.spring.cloud.client.discovery.constants; + +import org.junit.jupiter.api.Test; + +import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.COMMONS_CLIENT_AUTO_CONFIGURATION_CLASS_NAME; +import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.COMPOSITE_DISCOVERY_CLIENT_CLASS_NAME; +import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.DISCOVERY_CLIENT_CLASS_NAME; +import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.REACTIVE_COMMONS_CLIENT_AUTO_CONFIGURATION_CLASS_NAME; +import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.REACTIVE_COMPOSITE_DISCOVERY_CLIENT_AUTO_CONFIGURATION_CLASS_NAME; +import static io.microsphere.spring.cloud.client.discovery.constants.DiscoveryClientConstants.SIMPLE_REACTIVE_DISCOVERY_CLIENT_AUTO_CONFIGURATION_CLASS_NAME; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link DiscoveryClientConstants} Test + * + * @author Mercy + * @see DiscoveryClientConstants + * @since 1.0.0 + */ +class DiscoveryClientConstantsTest { + + @Test + void testConstants() { + assertEquals("org.springframework.cloud.client.discovery.DiscoveryClient", DISCOVERY_CLIENT_CLASS_NAME); + assertEquals("org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient", COMPOSITE_DISCOVERY_CLIENT_CLASS_NAME); + assertEquals("org.springframework.cloud.client.CommonsClientAutoConfiguration", COMMONS_CLIENT_AUTO_CONFIGURATION_CLASS_NAME); + assertEquals("org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration", REACTIVE_COMMONS_CLIENT_AUTO_CONFIGURATION_CLASS_NAME); + assertEquals("org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration", SIMPLE_REACTIVE_DISCOVERY_CLIENT_AUTO_CONFIGURATION_CLASS_NAME); + assertEquals("org.springframework.cloud.client.discovery.composite.reactive.ReactiveCompositeDiscoveryClientAutoConfiguration", REACTIVE_COMPOSITE_DISCOVERY_CLIENT_AUTO_CONFIGURATION_CLASS_NAME); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/util/DiscoveryUtilsTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/util/DiscoveryUtilsTest.java new file mode 100644 index 00000000..c795655a --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/discovery/util/DiscoveryUtilsTest.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.discovery.util; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryProperties; +import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryProperties; + +import java.util.List; +import java.util.Map; + +import static io.microsphere.collection.Lists.ofList; +import static io.microsphere.spring.cloud.client.discovery.util.DiscoveryUtils.getInstancesMap; +import static io.microsphere.spring.cloud.client.discovery.util.DiscoveryUtils.simpleDiscoveryProperties; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtilsTest.createDefaultServiceInstance; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link DiscoveryUtils} Test + * + * @author Mercy + * @see DiscoveryUtils + * @since 1.0.0 + */ +class DiscoveryUtilsTest { + + private DefaultServiceInstance serviceInstance; + + private SimpleDiscoveryProperties properties; + + @BeforeEach + void setUp() { + this.serviceInstance = createDefaultServiceInstance(); + this.properties = new SimpleDiscoveryProperties(); + Map> instancesMap = this.properties.getInstances(); + instancesMap.put(this.serviceInstance.getInstanceId(), ofList(this.serviceInstance)); + } + + @Test + void testGetInstancesMap() { + Map> instancesMap = getInstancesMap(this.properties); + assertEquals(1, instancesMap.size()); + assertEquals(ofList(this.serviceInstance), instancesMap.get(this.serviceInstance.getInstanceId())); + } + + @Test + void testGetInstancesMapFromSimpleReactiveDiscoveryProperties() { + SimpleReactiveDiscoveryProperties properties = new SimpleReactiveDiscoveryProperties(); + Map> instancesMap = getInstancesMap(properties); + assertTrue(instancesMap.isEmpty()); + + properties.setInstances(this.properties.getInstances()); + instancesMap = getInstancesMap(properties); + assertEquals(1, instancesMap.size()); + assertEquals(ofList(this.serviceInstance), instancesMap.get(this.serviceInstance.getInstanceId())); + } + + @Test + void testSimpleDiscoveryProperties() { + SimpleReactiveDiscoveryProperties properties = new SimpleReactiveDiscoveryProperties(); + properties.setInstances(this.properties.getInstances()); + + SimpleDiscoveryProperties simpleDiscoveryProperties = simpleDiscoveryProperties(properties); + assertEquals(this.properties.getInstances(), simpleDiscoveryProperties.getInstances()); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/event/ServiceInstancesChangedEventTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/event/ServiceInstancesChangedEventTest.java new file mode 100644 index 00000000..da3a7ca9 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/event/ServiceInstancesChangedEventTest.java @@ -0,0 +1,67 @@ +package io.microsphere.spring.cloud.client.event; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; + +import java.net.URI; +import java.util.Arrays; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link ServiceInstancesChangedEvent} Test + * + * @author Mercy + * @see ServiceInstancesChangedEvent + * @since 1.0.0 + */ +class ServiceInstancesChangedEventTest { + + private String serviceName = "testService"; + + private ServiceInstancesChangedEvent event; + + private ServiceInstance instance; + + @BeforeEach + void setUp() { + this.instance = createInstance(serviceName); + this.event = new ServiceInstancesChangedEvent(serviceName, Arrays.asList(instance)); + } + + private ServiceInstance createInstance(String serviceName) { + DefaultServiceInstance instance = new DefaultServiceInstance(); + instance.setServiceId(serviceName); + instance.setServiceId(UUID.randomUUID().toString()); + instance.setHost("127.0.0.1"); + instance.setPort(8080); + instance.setUri(URI.create("http://127.0.0.1:8080/info")); + return instance; + } + + @Test + void testGetServiceName() { + assertEquals(this.serviceName, this.event.getServiceName()); + assertEquals(this.serviceName, this.event.getSource()); + } + + @Test + void testGetServiceInstances() { + assertEquals(Arrays.asList(this.instance), this.event.getServiceInstances()); + assertEquals(this.instance, this.event.getServiceInstances().get(0)); + assertSame(this.instance, this.event.getServiceInstances().get(0)); + } + + @Test + void testProcessed() { + assertFalse(this.event.isProcessed()); + this.event.processed(); + assertTrue(this.event.isProcessed()); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/DefaultRegistrationTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/DefaultRegistrationTest.java new file mode 100644 index 00000000..ffa974e5 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/DefaultRegistrationTest.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.registry; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.URI; + +import static java.lang.System.currentTimeMillis; +import static java.net.URI.create; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link DefaultRegistration} Test + * + * @author Mercy + * @see DefaultRegistration + * @since 1.0.0 + */ +public class DefaultRegistrationTest { + + private DefaultRegistration registration; + + @BeforeEach + void setUp() { + this.registration = createDefaultRegistration(); + } + + @Test + void testGetUri() { + assertNotNull(this.registration.getUri()); + } + + @Test + void testGetMetadata() { + assertNotNull(this.registration.getMetadata()); + } + + @Test + void testGetInstanceId() { + assertNotNull(this.registration.getInstanceId()); + } + + @Test + void testGetServiceId() { + assertEquals("test-service", this.registration.getServiceId()); + } + + @Test + void testGetHost() { + assertEquals("localhost", this.registration.getHost()); + } + + @Test + void testGetPort() { + assertEquals(8080, this.registration.getPort()); + } + + @Test + void testIsSecure() { + assertTrue(this.registration.isSecure()); + } + + @Test + void testSetInstanceId() { + this.registration.setInstanceId("test-instance-id"); + assertEquals("test-instance-id", this.registration.getInstanceId()); + } + + @Test + void testSetServiceId() { + this.registration.setServiceId("test-service-id"); + assertEquals("test-service-id", this.registration.getServiceId()); + } + + @Test + void testSetHost() { + this.registration.setHost("test-host"); + assertEquals("test-host", this.registration.getHost()); + } + + @Test + void testSetPort() { + this.registration.setPort(9090); + assertEquals(9090, this.registration.getPort()); + } + + @Test + void testSetUri() { + URI uri = create("https://localhost:9090"); + this.registration.setUri(uri); + assertEquals(uri, this.registration.getUri()); + } + + @Test + void testToString() { + assertNotNull(this.registration.toString()); + } + + @Test + void testEquals() { + DefaultRegistration registration = createDefaultRegistration(); + registration.setInstanceId(this.registration.getInstanceId()); + assertEquals(this.registration, registration); + } + + @Test + void testHashCode() { + DefaultRegistration registration = createDefaultRegistration(); + registration.setInstanceId(this.registration.getInstanceId()); + assertEquals(this.registration.hashCode(), registration.hashCode()); + + } + + @Test + void testGetScheme() { + assertNull(this.registration.getScheme()); + } + + public static DefaultRegistration createDefaultRegistration() { + DefaultRegistration defaultRegistration = new DefaultRegistration(); + defaultRegistration.setInstanceId("ServiceInstance-" + currentTimeMillis()); + defaultRegistration.setServiceId("test-service"); + defaultRegistration.setHost("localhost"); + defaultRegistration.setPort(8080); + defaultRegistration.setSecure(true); + return defaultRegistration; + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/MultipleAutoServiceRegistrationTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/MultipleAutoServiceRegistrationTest.java new file mode 100644 index 00000000..ac5f1520 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/MultipleAutoServiceRegistrationTest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.registry; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationProperties; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; + +import static io.microsphere.collection.Lists.ofList; +import static io.microsphere.spring.cloud.client.service.registry.DefaultRegistrationTest.createDefaultRegistration; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * {@link MultipleAutoServiceRegistration} Test + * + * @author Mercy + * @see MultipleAutoServiceRegistration + * @since 1.0.0 + */ +class MultipleAutoServiceRegistrationTest { + + private DefaultRegistration defaultRegistration; + + private MultipleRegistration registration; + + private ServiceRegistry serviceRegistry; + + private AutoServiceRegistrationProperties properties; + + private MultipleAutoServiceRegistration autoServiceRegistration; + + @BeforeEach + void setUp() { + this.defaultRegistration = createDefaultRegistration(); + this.registration = new MultipleRegistration(ofList(defaultRegistration)); + this.serviceRegistry = new InMemoryServiceRegistry(); + this.properties = new AutoServiceRegistrationProperties(); + this.autoServiceRegistration = new MultipleAutoServiceRegistration(registration, serviceRegistry, properties); + } + + @Test + void testGetConfiguration() { + assertNull(this.autoServiceRegistration.getConfiguration()); + } + + @Test + void testIsEnabled() { + assertEquals(this.properties.isEnabled(), this.autoServiceRegistration.isEnabled()); + } + + @Test + void testGetRegistration() { + assertSame(this.registration, this.autoServiceRegistration.getRegistration()); + } + + @Test + void testGetManagementRegistration() { + assertSame(this.registration, this.autoServiceRegistration.getManagementRegistration()); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/MultipleRegistrationTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/MultipleRegistrationTest.java new file mode 100644 index 00000000..5f8d08d5 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/MultipleRegistrationTest.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.registry; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.client.serviceregistry.Registration; + +import static io.microsphere.collection.Lists.ofList; +import static io.microsphere.spring.cloud.client.service.registry.DefaultRegistrationTest.createDefaultRegistration; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * {@link MultipleRegistration} Test + * + * @author Mercy + * @see MultipleRegistration + * @since 1.0.0 + */ +class MultipleRegistrationTest { + + private DefaultRegistration defaultRegistration; + + private MultipleRegistration registration; + + @BeforeEach + void setUp() { + this.defaultRegistration = createDefaultRegistration(); + this.registration = new MultipleRegistration(ofList(defaultRegistration)); + } + + @Test + void testGetInstanceId() { + assertEquals(defaultRegistration.getInstanceId(), registration.getInstanceId()); + } + + @Test + void testGetServiceId() { + assertEquals(defaultRegistration.getServiceId(), registration.getServiceId()); + } + + @Test + void testGetHost() { + assertEquals(defaultRegistration.getHost(), registration.getHost()); + } + + @Test + void testGetPort() { + assertEquals(defaultRegistration.getPort(), registration.getPort()); + } + + @Test + void testIsSecure() { + assertEquals(defaultRegistration.isSecure(), registration.isSecure()); + } + + @Test + void testGetUri() { + assertEquals(defaultRegistration.getUri(), registration.getUri()); + } + + @Test + void testGetMetadata() { + assertEquals(defaultRegistration.getMetadata(), registration.getMetadata()); + } + + @Test + void testGetDefaultRegistration() { + assertEquals(defaultRegistration, registration.getDefaultRegistration()); + } + + @Test + void testSpecial() { + assertSame(registration, registration.special(Registration.class)); + assertSame(defaultRegistration, registration.special(DefaultRegistration.class)); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/MultipleServiceRegistryIntegrationTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/MultipleServiceRegistryIntegrationTest.java new file mode 100644 index 00000000..3c00c890 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/MultipleServiceRegistryIntegrationTest.java @@ -0,0 +1,129 @@ +package io.microsphere.spring.cloud.client.service.registry; + +import com.netflix.appinfo.ApplicationInfoManager; +import com.netflix.appinfo.InstanceInfo; +import io.microsphere.spring.cloud.client.service.registry.event.RegistrationPreRegisteredEvent; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationProperties; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaRegistration; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.containers.ComposeContainer; +import org.testcontainers.junit.jupiter.EnabledIfDockerAvailable; + +import java.io.File; +import java.net.URL; +import java.util.Map; + +import static io.microsphere.lang.function.ThrowableAction.execute; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.testcontainers.containers.wait.strategy.Wait.forLogMessage; + +/** + * {@link MultipleServiceRegistry} Integration Test + * + * @author 韩超 + * @author Mercy + * @see MultipleServiceRegistry + * @since 1.0.0 + */ +@EnabledIfSystemProperty(named = "testcontainers.enabled", matches = "true") +@EnabledIfDockerAvailable +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = { + MultipleServiceRegistryIntegrationTest.class, +}) +@TestPropertySource( + properties = { + "spring.application.name=test", + + "spring.cloud.service-registry.auto-registration.enabled=true", + "spring.cloud.nacos.discovery.namespace=f7ad23e0-f581-4516-9420-8c50aa6a7b89", + "spring.cloud.nacos.discovery.metadata.key=value", + + "microsphere.spring.cloud.multiple-registration.enabled=true", + "microsphere.spring.cloud.default-registration.type=com.alibaba.cloud.nacos.registry.NacosRegistration", + "microsphere.spring.cloud.default-service-registry.type=com.alibaba.cloud.nacos.registry.NacosServiceRegistry", + } +) +@EnableAutoConfiguration +class MultipleServiceRegistryIntegrationTest implements ApplicationListener { + + private static ComposeContainer composeContainer; + + @Autowired + private ServiceRegistry serviceRegistry; + + @Autowired + private AutoServiceRegistrationProperties properties; + + @Autowired + private Registration registration; + + @Autowired + private MultipleAutoServiceRegistration autoServiceRegistration; + + @Autowired + private ConfigurableApplicationContext context; + + @BeforeAll + static void beforeAll() throws Exception { + ClassLoader classLoader = MultipleServiceRegistryIntegrationTest.class.getClassLoader(); + URL resource = classLoader.getResource("META-INF/docker/service-registry-servers.yml"); + File dockerComposeFile = new File(resource.toURI()); + composeContainer = new ComposeContainer(dockerComposeFile); + composeContainer.waitingFor("nacos", forLogMessage(".*Nacos started successfully.*", 1)) + .waitingFor("eureka", forLogMessage(".*Started EurekaServerApplication.*", 1)) + .start(); + } + + @AfterAll + static void afterAll() { + composeContainer.stop(); + } + + @BeforeEach + void setUp() { + context.addApplicationListener(this); + } + + @Override + public void onApplicationEvent(RegistrationPreRegisteredEvent event) { + onPreRegisteredEvent(event.getRegistration()); + } + + void onPreRegisteredEvent(Registration registration) { + this.registration.getMetadata().put("my-key", "my-value"); + if (registration instanceof EurekaRegistration) { + EurekaRegistration eurekaRegistration = (EurekaRegistration) registration; + ApplicationInfoManager applicationInfoManager = eurekaRegistration.getApplicationInfoManager(); + InstanceInfo instanceInfo = applicationInfoManager.getInfo(); + Map metadata = registration.getMetadata(); + // Sync metadata from Registration to InstanceInfo + instanceInfo.getMetadata().putAll(metadata); + } + } + + @Test + void test() throws Exception { + assertNotNull(registration); + execute(autoServiceRegistration::start, e -> { + + }); + assertEquals(registration.getMetadata().get("my-key"), "my-value"); + autoServiceRegistration.stop(); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/MultipleServiceRegistryTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/MultipleServiceRegistryTest.java index ee918731..0bef8aaa 100644 --- a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/MultipleServiceRegistryTest.java +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/MultipleServiceRegistryTest.java @@ -1,113 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.microsphere.spring.cloud.client.service.registry; -import com.alibaba.cloud.nacos.NacosServiceAutoConfiguration; -import com.alibaba.cloud.nacos.discovery.NacosDiscoveryAutoConfiguration; -import com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration; -import com.netflix.appinfo.ApplicationInfoManager; -import com.netflix.appinfo.InstanceInfo; -import io.microsphere.spring.cloud.client.service.registry.autoconfigure.ServiceRegistryAutoConfiguration; -import io.microsphere.spring.cloud.client.service.registry.event.RegistrationPreRegisteredEvent; -import org.junit.jupiter.api.Disabled; + +import com.alibaba.cloud.nacos.registry.NacosRegistration; +import com.alibaba.cloud.nacos.registry.NacosServiceRegistry; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cloud.client.CommonsClientAutoConfiguration; -import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration; -import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationProperties; import org.springframework.cloud.client.serviceregistry.Registration; import org.springframework.cloud.client.serviceregistry.ServiceRegistry; -import org.springframework.cloud.commons.util.UtilAutoConfiguration; -import org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration; -import org.springframework.cloud.netflix.eureka.config.DiscoveryClientOptionalArgsConfiguration; -import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaRegistration; -import org.springframework.context.ApplicationListener; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import java.util.Map; +import static io.microsphere.collection.Lists.ofList; +import static io.microsphere.collection.Maps.ofMap; +import static io.microsphere.spring.cloud.client.service.registry.DefaultRegistrationTest.createDefaultRegistration; +import static io.microsphere.spring.cloud.client.service.registry.MultipleServiceRegistry.getRegistrationClass; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -@Disabled -@ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = { - AutoServiceRegistrationAutoConfiguration.class, - CommonsClientAutoConfiguration.class, - EurekaClientAutoConfiguration.class, - DiscoveryClientOptionalArgsConfiguration.class, - NacosServiceRegistryAutoConfiguration.class, - NacosServiceAutoConfiguration.class, - NacosDiscoveryAutoConfiguration.class, - UtilAutoConfiguration.class, - MultipleServiceRegistryTest.class, - ServiceRegistryAutoConfiguration.class, -}) -@TestPropertySource( - properties = { - "spring.application.name=test", - "microsphere.spring.cloud.multiple-registration.enabled=true", - "microsphere.spring.cloud.default-registration.type=com.alibaba.cloud.nacos.registry.NacosRegistration", - "microsphere.spring.cloud.default-service-registry.type=com.alibaba.cloud.nacos.registry.NacosServiceRegistry", - "spring.cloud.service-registry.auto-registration.enabled=true", - "spring.cloud.nacos.discovery.namespace=f7ad23e0-f581-4516-9420-8c50aa6a7b89", - "spring.cloud.nacos.discovery.metadata.key=value", - "eureka.client.service-url.defaultZone=http://127.0.0.1:8080/eureka", - } -) -@EnableAutoConfiguration -class MultipleServiceRegistryTest implements ApplicationListener { - - @Autowired - private ServiceRegistry serviceRegistry; - - @Autowired - private AutoServiceRegistrationProperties properties; - - @Autowired - private Registration registration; - - @Autowired - private MultipleAutoServiceRegistration autoServiceRegistration; - - @Override - public void onApplicationEvent(RegistrationPreRegisteredEvent event) { - this.registration.getMetadata().put("my-key", "my-value"); - if (event.getRegistration() instanceof EurekaRegistration) { - EurekaRegistration eurekaRegistration = (EurekaRegistration) event.getRegistration(); - - - ApplicationInfoManager applicationInfoManager = eurekaRegistration.getApplicationInfoManager(); - InstanceInfo instanceInfo = applicationInfoManager.getInfo(); - Map metadata = registration.getMetadata(); - // Sync metadata from Registration to InstanceInfo - instanceInfo.getMetadata().putAll(metadata); - } - } +import static org.junit.jupiter.api.Assertions.assertNull; - @Test - public void test() throws Exception { - assertNotNull(serviceRegistry); - assertNotNull(registration); - autoServiceRegistration.start(); - Thread.sleep(60 * 1000); +/** + * {@link MultipleServiceRegistry} Test + * + * @author Mercy + * @see MultipleServiceRegistry + * @since 1.0.0 + */ +class MultipleServiceRegistryTest { - autoServiceRegistration.stop(); + private DefaultRegistration defaultRegistration; + + private MultipleRegistration registration; + + private ServiceRegistry serviceRegistry; + + private MultipleServiceRegistry multipleServiceRegistry; + + @BeforeEach + void setUp() { + this.defaultRegistration = createDefaultRegistration(); + this.registration = new MultipleRegistration(ofList(defaultRegistration)); + this.serviceRegistry = new InMemoryServiceRegistry(); + this.multipleServiceRegistry = new MultipleServiceRegistry(ofMap("default", serviceRegistry)); } @Test - public void testMetaData() throws Exception { - assertNotNull(registration); + void testRegister() { + this.multipleServiceRegistry.register(this.registration); + } - autoServiceRegistration.start(); + @Test + void testDeregister() { + this.multipleServiceRegistry.deregister(this.registration); + } - assertEquals(registration.getMetadata().get("my-key"), "my-value"); - Thread.sleep(60 * 1000); + @Test + void testClose() { + this.multipleServiceRegistry.close(); + } - autoServiceRegistration.stop(); + @Test + void testStatus() { + testRegister(); + this.multipleServiceRegistry.setStatus(this.registration, "UP"); + assertEquals("UP", this.multipleServiceRegistry.getStatus(this.registration)); + testDeregister(); + this.multipleServiceRegistry.setStatus(this.registration, "UP"); + assertNull(this.multipleServiceRegistry.getStatus(this.registration)); } + @Test + void testGetRegistrationClass() { + assertEquals(MultipleRegistration.class, getRegistrationClass(this.multipleServiceRegistry.getClass())); + assertEquals(NacosRegistration.class, getRegistrationClass(NacosServiceRegistry.class)); + } + @Test + void testGetRegistrationClassWithNull() { + MultipleRegistration multipleRegistration = new MultipleRegistration(ofList(this.defaultRegistration)) { + @Override + public T special(Class specialClass) { + return null; + } + }; + this.multipleServiceRegistry.register(multipleRegistration); + } } \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/RegistrationMetaDataTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/RegistrationMetaDataTest.java new file mode 100644 index 00000000..7239edad --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/RegistrationMetaDataTest.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.registry; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import static io.microsphere.collection.Lists.ofList; +import static io.microsphere.collection.MapUtils.ofEntry; +import static io.microsphere.collection.Maps.ofMap; +import static io.microsphere.spring.cloud.client.service.registry.DefaultRegistrationTest.createDefaultRegistration; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link RegistrationMetaData} Test + * + * @author Mercy + * @see RegistrationMetaData + * @since 1.0.0 + */ +class RegistrationMetaDataTest { + + private DefaultRegistration defaultRegistration; + + private RegistrationMetaData metaData; + + @BeforeEach + void setUp() { + this.defaultRegistration = createDefaultRegistration(); + Map metadata = this.defaultRegistration.getMetadata(); + metadata.put("key1", "value1"); + metadata.put("key2", "value2"); + metadata.put("key3", "value3"); + this.metaData = new RegistrationMetaData(ofList(defaultRegistration)); + } + + @Test + void testConstructor() { + Map metadata = this.defaultRegistration.getMetadata(); + metadata.put(null, "null"); + metadata.put("null", null); + this.metaData = new RegistrationMetaData(ofList(defaultRegistration)); + testGet(); + assertNull(this.metaData.get("null")); + } + + @Test + void testSize() { + assertEquals(3, metaData.size()); + } + + @Test + void testIsEmpty() { + assertEquals(false, metaData.isEmpty()); + } + + @Test + void testContainsKey() { + assertTrue(metaData.containsKey("key1")); + assertFalse(metaData.containsKey("key4")); + } + + @Test + void testContainsValue() { + assertTrue(metaData.containsValue("value1")); + assertFalse(metaData.containsValue("value4")); + } + + @Test + void testGet() { + assertEquals("value1", metaData.get("key1")); + assertEquals("value2", metaData.get("key2")); + assertEquals("value3", metaData.get("key3")); + assertNull(metaData.get("key4")); + } + + @Test + void testPut() { + metaData.put("key4", "value4"); + assertEquals("value4", metaData.get("key4")); + } + + @Test + void testRemove() { + metaData.remove("key1"); + assertNull(metaData.get("key1")); + } + + @Test + void testPutAll() { + metaData.putAll(ofMap("key4", "value4", "key5", "value5")); + assertEquals("value1", metaData.get("key1")); + assertEquals("value2", metaData.get("key2")); + assertEquals("value3", metaData.get("key3")); + assertEquals("value4", metaData.get("key4")); + assertEquals("value5", metaData.get("key5")); + } + + @Test + void testClear() { + metaData.clear(); + assertEquals(0, metaData.size()); + } + + @Test + void testKeySet() { + Set keys = metaData.keySet(); + assertTrue(keys.contains("key1")); + assertTrue(keys.contains("key2")); + assertTrue(keys.contains("key3")); + assertFalse(keys.contains("key4")); + } + + @Test + void testValues() { + Collection values = metaData.values(); + assertTrue(values.contains("value1")); + assertTrue(values.contains("value2")); + assertTrue(values.contains("value3")); + assertFalse(values.contains("value4")); + } + + @Test + void testEntrySet() { + Set> entries = metaData.entrySet(); + assertTrue(entries.contains(ofEntry("key1", "value1"))); + assertTrue(entries.contains(ofEntry("key2", "value2"))); + assertTrue(entries.contains(ofEntry("key3", "value3"))); + assertFalse(entries.contains(ofEntry("key4", "value4"))); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/SimpleAutoServiceRegistrationTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/SimpleAutoServiceRegistrationTest.java new file mode 100644 index 00000000..218a2f90 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/SimpleAutoServiceRegistrationTest.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.registry; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationProperties; + +import static io.microsphere.spring.cloud.client.service.registry.DefaultRegistrationTest.createDefaultRegistration; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * {@link SimpleAutoServiceRegistration} Test + * + * @author Mercy + * @see SimpleAutoServiceRegistration + * @since 1.0.0 + */ +class SimpleAutoServiceRegistrationTest { + + private InMemoryServiceRegistry serviceRegistry; + + private AutoServiceRegistrationProperties properties; + + private DefaultRegistration registration; + + private SimpleAutoServiceRegistration autoServiceRegistration; + + @BeforeEach + void setUp() { + this.serviceRegistry = new InMemoryServiceRegistry(); + this.properties = new AutoServiceRegistrationProperties(); + this.registration = createDefaultRegistration(); + this.autoServiceRegistration = new SimpleAutoServiceRegistration(serviceRegistry, properties, registration); + } + + @Test + void testGetConfiguration() { + assertSame(this.properties, autoServiceRegistration.getConfiguration()); + } + + @Test + void testIsEnabled() { + assertSame(this.properties.isEnabled(), autoServiceRegistration.isEnabled()); + } + + @Test + void testGetRegistration() { + assertSame(this.registration, autoServiceRegistration.getRegistration()); + } + + @Test + void testGetManagementRegistration() { + assertSame(this.registration, autoServiceRegistration.getManagementRegistration()); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/SimpleServiceRegistryTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/SimpleServiceRegistryTest.java new file mode 100644 index 00000000..1b11e19e --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/SimpleServiceRegistryTest.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.registry; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryProperties; +import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryProperties; + +import java.util.List; +import java.util.Map; + +import static io.microsphere.spring.cloud.client.discovery.util.DiscoveryUtils.simpleReactiveDiscoveryProperties; +import static io.microsphere.spring.cloud.client.service.registry.DefaultRegistrationTest.createDefaultRegistration; +import static io.microsphere.spring.cloud.client.service.registry.SimpleServiceRegistry.STATUS_KEY; +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link SimpleServiceRegistry} Test + * + * @author Mercy + * @see SimpleServiceRegistry + * @since 1.0.0 + */ +class SimpleServiceRegistryTest { + + private DefaultRegistration registration; + + private SimpleDiscoveryProperties properties; + + private SimpleServiceRegistry registry; + + @BeforeEach + void setUp() { + this.registration = createDefaultRegistration(); + this.properties = new SimpleDiscoveryProperties(); + this.registry = new SimpleServiceRegistry(this.properties); + } + + @Test + void testConstructor() { + SimpleReactiveDiscoveryProperties properties = simpleReactiveDiscoveryProperties(this.properties); + this.registry = new SimpleServiceRegistry(properties); + testDeregister(); + } + + @Test + void testRegister() { + Map> instancesMap = this.properties.getInstances(); + assertTrue(instancesMap.isEmpty()); + this.registry.register(this.registration); + + List instances = instancesMap.get(this.registration.getServiceId()); + assertEquals(1, instances.size()); + DefaultServiceInstance instance = instances.get(0); + assertSame(instance, this.registration); + } + + @Test + void testDeregister() { + testRegister(); + this.registry.deregister(this.registration); + List instances = getInstances(this.registration.getServiceId()); + assertTrue(instances.isEmpty()); + } + + @Test + void testClose() { + Map> instancesMap = this.properties.getInstances(); + assertTrue(instancesMap.isEmpty()); + this.registry.close(); + assertTrue(instancesMap.isEmpty()); + } + + @Test + void testSetStatus() { + testRegister(); + DefaultServiceInstance instance = getInstance(this.registration.getServiceId(), this.registration.getInstanceId()); + String status = "UP"; + this.registry.setStatus(this.registration, status); + assertEquals(status, instance.getMetadata().get(STATUS_KEY)); + } + + @Test + void testGetStatus() { + testSetStatus(); + assertEquals(this.registration.getMetadata().get(STATUS_KEY), this.registry.getStatus(this.registration)); + } + + List getInstances(String serviceId) { + return this.properties.getInstances().getOrDefault(serviceId, emptyList()); + } + + DefaultServiceInstance getInstance(String serviceId, String instanceId) { + List instances = getInstances(serviceId); + return instances.stream() + .filter(instance -> instance.getInstanceId().equals(instanceId)) + .findFirst().orElse(null); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/actuate/autoconfigure/ServiceRegistrationEndpointAutoConfigurationTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/actuate/autoconfigure/ServiceRegistrationEndpointAutoConfigurationTest.java new file mode 100644 index 00000000..6bc1a536 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/actuate/autoconfigure/ServiceRegistrationEndpointAutoConfigurationTest.java @@ -0,0 +1,46 @@ +package io.microsphere.spring.cloud.client.service.registry.actuate.autoconfigure; + +import io.microsphere.spring.cloud.client.service.registry.endpoint.ServiceDeregistrationEndpoint; +import io.microsphere.spring.cloud.client.service.registry.endpoint.ServiceRegistrationEndpoint; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * {@link ServiceRegistrationEndpointAutoConfiguration} Test + * + * @author Mercy + * @see ServiceRegistrationEndpointAutoConfiguration + * @since 1.0.0 + */ +@SpringBootTest( + classes = { + ServiceRegistrationEndpointAutoConfigurationTest.class + }, + webEnvironment = RANDOM_PORT, + properties = { + "microsphere.spring.cloud.service-registry.auto-registration.simple.enabled=true", + "management.endpoint.serviceRegistration.enabled=true", + "management.endpoint.serviceDeregistration.enabled=true", + } +) +@EnableAutoConfiguration +class ServiceRegistrationEndpointAutoConfigurationTest { + + @Autowired + private ObjectProvider serviceRegistrationEndpoint; + + @Autowired + private ObjectProvider serviceDeregistrationEndpoint; + + @Test + void testEndpoints() { + assertNotNull(serviceRegistrationEndpoint.getIfAvailable()); + assertNotNull(serviceDeregistrationEndpoint.getIfAvailable()); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/aspect/EventPublishingRegistrationAspectTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/aspect/EventPublishingRegistrationAspectTest.java new file mode 100644 index 00000000..cbdfc790 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/aspect/EventPublishingRegistrationAspectTest.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.registry.aspect; + + +import io.microsphere.spring.cloud.client.service.registry.DefaultRegistration; +import io.microsphere.spring.cloud.client.service.registry.InMemoryServiceRegistry; +import io.microsphere.spring.cloud.client.service.registry.MultipleRegistration; +import io.microsphere.spring.cloud.client.service.registry.MultipleServiceRegistry; +import io.microsphere.spring.cloud.client.service.registry.RegistrationCustomizer; +import io.microsphere.spring.cloud.client.service.registry.event.RegistrationDeregisteredEvent; +import io.microsphere.spring.cloud.client.service.registry.event.RegistrationEvent; +import io.microsphere.spring.cloud.client.service.registry.event.RegistrationPreDeregisteredEvent; +import io.microsphere.spring.cloud.client.service.registry.event.RegistrationPreRegisteredEvent; +import io.microsphere.spring.cloud.client.service.registry.event.RegistrationRegisteredEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.Map; + +import static io.microsphere.collection.Lists.ofList; +import static io.microsphere.spring.cloud.client.service.registry.DefaultRegistrationTest.createDefaultRegistration; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * {@link EventPublishingRegistrationAspect} Test + * + * @author Mercy + * @see EventPublishingRegistrationAspect + * @since 1.0.0 + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = { + EventPublishingRegistrationAspect.class, + EventPublishingRegistrationAspectTest.Config.class +}) +@DirtiesContext +class EventPublishingRegistrationAspectTest { + + @Autowired + private ServiceRegistry serviceRegistry; + + @Autowired + private ConfigurableApplicationContext context; + + private MultipleRegistration multipleRegistration; + + @BeforeEach + void setUp() { + DefaultRegistration defaultRegistration = createDefaultRegistration(); + this.multipleRegistration = new MultipleRegistration(ofList(defaultRegistration)); + } + + @Test + void testBeforeAndAfterRegister() { + this.context.addApplicationListener((ApplicationListener) event -> { + assertRegistration(event); + }); + this.context.addApplicationListener((ApplicationListener) event -> { + assertRegistration(event); + }); + this.serviceRegistry.register(multipleRegistration); + } + + @Test + void testBeforeAndAfterDeregister() { + this.context.addApplicationListener((ApplicationListener) event -> { + assertRegistration(event); + }); + this.context.addApplicationListener((ApplicationListener) event -> { + assertRegistration(event); + }); + this.serviceRegistry.deregister(multipleRegistration); + } + + void assertRegistration(RegistrationEvent event) { + assertSame(this.multipleRegistration, event.getRegistration()); + } + + @Import(InMemoryServiceRegistry.class) + @EnableAspectJAutoProxy + static class Config implements RegistrationCustomizer { + + @Override + public void customize(Registration registration) { + } + + @Bean + @Primary + public MultipleServiceRegistry multipleServiceRegistry(Map registriesMap) { + return new MultipleServiceRegistry(registriesMap); + } + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/ServiceRegistryAutoConfigurationTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/ServiceRegistryAutoConfigurationTest.java new file mode 100644 index 00000000..9e1a0091 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/ServiceRegistryAutoConfigurationTest.java @@ -0,0 +1,134 @@ +package io.microsphere.spring.cloud.client.service.registry.autoconfigure; + +import io.microsphere.spring.cloud.client.service.registry.InMemoryServiceRegistry; +import io.microsphere.spring.cloud.client.service.registry.event.RegistrationDeregisteredEvent; +import io.microsphere.spring.cloud.client.service.registry.event.RegistrationEvent; +import io.microsphere.spring.cloud.client.service.registry.event.RegistrationPreDeregisteredEvent; +import io.microsphere.spring.cloud.client.service.registry.event.RegistrationPreRegisteredEvent; +import io.microsphere.spring.cloud.client.service.registry.event.RegistrationRegisteredEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +import static io.microsphere.spring.cloud.client.service.registry.DefaultRegistrationTest.createDefaultRegistration; +import static io.microsphere.spring.cloud.client.service.registry.event.RegistrationEvent.Type.DEREGISTERED; +import static io.microsphere.spring.cloud.client.service.registry.event.RegistrationEvent.Type.PRE_DEREGISTERED; +import static io.microsphere.spring.cloud.client.service.registry.event.RegistrationEvent.Type.PRE_REGISTERED; +import static io.microsphere.spring.cloud.client.service.registry.event.RegistrationEvent.Type.REGISTERED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.aop.support.AopUtils.getTargetClass; + +/** + * {@link ServiceRegistryAutoConfiguration} Test + * + * @author Mercy + * @see ServiceRegistryAutoConfiguration + * @since 1.0.0 + */ +@SpringBootTest( + classes = { + InMemoryServiceRegistry.class, + ServiceRegistryAutoConfiguration.class, + ServiceRegistryAutoConfigurationTest.class + } +) +@EnableAspectJAutoProxy +class ServiceRegistryAutoConfigurationTest { + + @Autowired + private ConfigurableApplicationContext context; + + @Autowired + private ServiceRegistry serviceRegistry; + + private Registration registration; + + private int count; + + @BeforeEach + void setUp() { + this.registration = createDefaultRegistration(); + } + + @Test + void testEventPublishingRegistrationAspect() { + + context.addApplicationListener(this::onApplicationEvent); + + serviceRegistry.register(registration); + + assertEquals(2, count); + + serviceRegistry.deregister(registration); + + assertEquals(4, count); + } + + private void onApplicationEvent(ApplicationEvent event) { + if (event instanceof RegistrationPreRegisteredEvent) { + onRegistrationPreRegisteredEvent((RegistrationPreRegisteredEvent) event); + } else if (event instanceof RegistrationRegisteredEvent) { + onRegistrationRegisteredEvent((RegistrationRegisteredEvent) event); + } else if (event instanceof RegistrationPreDeregisteredEvent) { + onRegistrationPreDeregisteredEvent((RegistrationPreDeregisteredEvent) event); + } else if (event instanceof RegistrationDeregisteredEvent) { + onRegistrationDeregisteredEvent((RegistrationDeregisteredEvent) event); + } + } + + private void onRegistrationPreRegisteredEvent(RegistrationPreRegisteredEvent event) { + assertRegistrationEvent(event); + assertTrue(event.isPreRegistered()); + assertFalse(event.isRegistered()); + assertFalse(event.isPreDeregistered()); + assertFalse(event.isDeregistered()); + assertEquals(PRE_REGISTERED, event.getType()); + } + + private void onRegistrationRegisteredEvent(RegistrationRegisteredEvent event) { + assertRegistrationEvent(event); + assertFalse(event.isPreRegistered()); + assertTrue(event.isRegistered()); + assertFalse(event.isPreDeregistered()); + assertFalse(event.isDeregistered()); + assertEquals(REGISTERED, event.getType()); + } + + private void onRegistrationPreDeregisteredEvent(RegistrationPreDeregisteredEvent event) { + assertRegistrationEvent(event); + assertFalse(event.isPreRegistered()); + assertFalse(event.isRegistered()); + assertTrue(event.isPreDeregistered()); + assertFalse(event.isDeregistered()); + assertEquals(PRE_DEREGISTERED, event.getType()); + } + + private void onRegistrationDeregisteredEvent(RegistrationDeregisteredEvent event) { + assertRegistrationEvent(event); + assertFalse(event.isPreRegistered()); + assertFalse(event.isRegistered()); + assertFalse(event.isPreDeregistered()); + assertTrue(event.isDeregistered()); + assertEquals(DEREGISTERED, event.getType()); + } + + private void assertRegistrationEvent(RegistrationEvent event) { + Registration registration = event.getRegistration(); + assertEquals(this.registration, registration); + assertSame(this.registration, registration); + assertSame(getTargetClass(this.serviceRegistry), getTargetClass(event.getRegistry())); + assertNotNull(event.getSource()); + assertNotNull(event.getType()); + count++; + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/SimpleAutoServiceRegistrationAutoConfigurationTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/SimpleAutoServiceRegistrationAutoConfigurationTest.java new file mode 100644 index 00000000..8fa8f2e0 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/SimpleAutoServiceRegistrationAutoConfigurationTest.java @@ -0,0 +1,54 @@ +package io.microsphere.spring.cloud.client.service.registry.autoconfigure; + +import io.microsphere.spring.cloud.client.service.registry.SimpleAutoServiceRegistration; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * {@link SimpleAutoServiceRegistrationAutoConfiguration} Test + * + * @author Mercy + * @see SimpleAutoServiceRegistrationAutoConfiguration + * @since 1.0.0 + */ +@SpringBootTest( + classes = { + SimpleAutoServiceRegistrationAutoConfigurationTest.class + }, + webEnvironment = RANDOM_PORT, + properties = { + "microsphere.spring.cloud.service-registry.auto-registration.simple.enabled=true", + "spring.application.name=test-service" + } +) +@EnableAutoConfiguration +class SimpleAutoServiceRegistrationAutoConfigurationTest { + + @Autowired + private Registration registration; + + @Autowired + private ServiceRegistry serviceRegistry; + + @Autowired + private SimpleAutoServiceRegistration simpleAutoServiceRegistration; + + @Test + void test() { + assertEquals("test-service", registration.getServiceId()); + assertNotNull(registration.getHost()); + assertNotNull(registration.getPort()); + assertNotNull(registration.getUri()); + assertNotNull(registration.getInstanceId()); + assertNotNull(registration.getMetadata()); + } + +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebFluxServiceRegistryAutoConfigurationTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebFluxServiceRegistryAutoConfigurationTest.java new file mode 100644 index 00000000..a3fdb1e5 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebFluxServiceRegistryAutoConfigurationTest.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.registry.autoconfigure; + + +import io.microsphere.spring.webflux.annotation.EnableWebFluxExtension; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.test.context.TestPropertySource; + +/** + * {@link WebFluxServiceRegistryAutoConfiguration} Test + * + * @author Mercy + * @see WebFluxServiceRegistryAutoConfiguration + * @since 1.0.0 + */ +@TestPropertySource( + properties = { + "spring.main.web-application-type=reactive" + } +) +@EnableWebFluxExtension +@EnableAutoConfiguration +class WebFluxServiceRegistryAutoConfigurationTest extends WebServiceRegistryAutoConfigurationTest { +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebMvcServiceRegistryAutoConfigurationTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebMvcServiceRegistryAutoConfigurationTest.java index f0693bf7..cf751340 100644 --- a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebMvcServiceRegistryAutoConfigurationTest.java +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebMvcServiceRegistryAutoConfigurationTest.java @@ -17,17 +17,7 @@ package io.microsphere.spring.cloud.client.service.registry.autoconfigure; import io.microsphere.spring.webmvc.annotation.EnableWebMvcExtension; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.client.serviceregistry.Registration; - -import java.util.Map; - -import static io.microsphere.spring.cloud.client.service.registry.constants.InstanceConstants.WEB_MAPPINGS_METADATA_NAME; -import static org.junit.jupiter.api.Assertions.assertNotNull; /** * {@link WebMvcServiceRegistryAutoConfiguration} Test @@ -35,28 +25,7 @@ * @author Mercy * @since 1.0.0 */ -@SpringBootTest( - classes = {WebMvcServiceRegistryAutoConfigurationTest.class}, - properties = { - "microsphere.spring.cloud.simple.enabled=true", - "spring.cloud.service-registry.auto-registration.enabled=true", - "spring.cloud.kubernetes.enabled=false", - "kubernetes.informer.enabled=false", - "kubernetes.manifests.enabled=false", - "kubernetes.reconciler.enabled=false" - }, - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT -) -@EnableAutoConfiguration @EnableWebMvcExtension -public class WebMvcServiceRegistryAutoConfigurationTest { - - @Autowired - private Registration registration; - - @Test - public void test() { - Map metadata = registration.getMetadata(); - assertNotNull(metadata.get(WEB_MAPPINGS_METADATA_NAME)); - } -} +@EnableAutoConfiguration +class WebMvcServiceRegistryAutoConfigurationTest extends WebServiceRegistryAutoConfigurationTest { +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebServiceRegistryAutoConfigurationTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebServiceRegistryAutoConfigurationTest.java new file mode 100644 index 00000000..2dd85f88 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/autoconfigure/WebServiceRegistryAutoConfigurationTest.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.registry.autoconfigure; + + +import io.microsphere.logging.test.jupiter.LoggingLevelsTest; +import io.microsphere.spring.test.web.controller.TestController; +import io.microsphere.spring.web.metadata.WebEndpointMapping; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.client.serviceregistry.Registration; + +import java.util.Collection; +import java.util.Map; + +import static io.microsphere.collection.ListUtils.newLinkedList; +import static io.microsphere.collection.Lists.ofList; +import static io.microsphere.spring.cloud.client.service.registry.constants.InstanceConstants.WEB_CONTEXT_PATH_METADATA_NAME; +import static io.microsphere.spring.cloud.client.service.registry.constants.InstanceConstants.WEB_MAPPINGS_METADATA_NAME; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.getWebEndpointMappings; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import static org.springframework.http.HttpMethod.GET; + +/** + * {@link WebServiceRegistryAutoConfiguration} Test + * + * @author Mercy + * @see WebServiceRegistryAutoConfiguration + * @since 1.0.0 + */ +@SpringBootTest( + classes = TestController.class, + properties = { + "microsphere.spring.cloud.service-registry.auto-registration.simple.enabled=true", + "spring.cloud.service-registry.auto-registration.enabled=true" + }, + webEnvironment = RANDOM_PORT +) +abstract class WebServiceRegistryAutoConfigurationTest { + + @Autowired + private Registration registration; + + @Autowired + private WebServiceRegistryAutoConfiguration autoConfiguration; + + @Test + void test() { + Map metadata = this.registration.getMetadata(); + assertEquals("", metadata.get(WEB_CONTEXT_PATH_METADATA_NAME)); + assertNotNull(metadata.get(WEB_MAPPINGS_METADATA_NAME)); + + Collection webEndpointMappings = getWebEndpointMappings(this.registration); + assertTrue(webEndpointMappings.size() >= 6); + } + + @Test + @LoggingLevelsTest(levels = "ERROR") + void testExcludeMappings() { + WebEndpointMapping webEndpointMapping = WebEndpointMapping + .webmvc() + .endpoint(this) + .methods(GET) + .pattern("/actuator/test") + .build(); + + Collection webEndpointMappings = newLinkedList(ofList(webEndpointMapping)); + + this.autoConfiguration.excludeMappings(webEndpointMappings); + assertTrue(webEndpointMappings.isEmpty()); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/constants/InstanceConstantsTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/constants/InstanceConstantsTest.java new file mode 100644 index 00000000..42598211 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/constants/InstanceConstantsTest.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.registry.constants; + + +import org.junit.jupiter.api.Test; + +import static io.microsphere.spring.cloud.client.service.registry.constants.InstanceConstants.MANAGEMENT_PORT_METADATA_NAME; +import static io.microsphere.spring.cloud.client.service.registry.constants.InstanceConstants.START_TIME_METADATA_NAME; +import static io.microsphere.spring.cloud.client.service.registry.constants.InstanceConstants.WEB_CONTEXT_PATH_METADATA_NAME; +import static io.microsphere.spring.cloud.client.service.registry.constants.InstanceConstants.WEB_MAPPINGS_METADATA_NAME; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link InstanceConstants} Test + * + * @author Mercy + * @see InstanceConstants + * @since 1.0.0 + */ +class InstanceConstantsTest { + + @Test + void testConstants() { + assertEquals("web.mappings", WEB_MAPPINGS_METADATA_NAME); + assertEquals("web.context-path", WEB_CONTEXT_PATH_METADATA_NAME); + assertEquals("management-port", MANAGEMENT_PORT_METADATA_NAME); + assertEquals("start-time", START_TIME_METADATA_NAME); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/endpoint/AbstractServiceRegistrationEndpointTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/endpoint/AbstractServiceRegistrationEndpointTest.java new file mode 100644 index 00000000..e0264f87 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/endpoint/AbstractServiceRegistrationEndpointTest.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.registry.endpoint; + +import io.microsphere.spring.cloud.client.service.registry.InMemoryServiceRegistry; +import io.microsphere.spring.cloud.client.service.registry.SimpleAutoServiceRegistration; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.boot.web.server.WebServer; +import org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationProperties; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.cloud.client.serviceregistry.ServiceRegistry; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.context.ContextConfiguration; + +import static io.microsphere.spring.cloud.client.service.registry.DefaultRegistrationTest.createDefaultRegistration; +import static io.microsphere.spring.cloud.client.service.registry.endpoint.AbstractServiceRegistrationEndpoint.detectRunning; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * {@link AbstractServiceRegistrationEndpoint} Test + * + * @author Mercy + * @see BaseServiceRegistrationEndpointTest + * @since 1.0.0 + */ +@ContextConfiguration(classes = { + AbstractServiceRegistrationEndpointTest.class +}) +@EnableAutoConfiguration +class AbstractServiceRegistrationEndpointTest extends BaseServiceRegistrationEndpointTest { + + @Test + void testOnApplicationEvent() { + AbstractServiceRegistrationEndpoint endpoint = new AbstractServiceRegistrationEndpoint() { + }; + WebServer webServer = mock(WebServer.class); + when(webServer.getPort()).thenReturn(this.port); + WebServerInitializedEvent event = new ServletWebServerInitializedEvent(webServer, null); + endpoint.onApplicationEvent(event); + assertFalse(endpoint.isRunning()); + } + + @Test + void testDetectRunning() { + assertFalse(detectRunning(null)); + ServiceRegistry serviceRegistry = new InMemoryServiceRegistry(); + AutoServiceRegistrationProperties properties = new AutoServiceRegistrationProperties(); + Registration registration = createDefaultRegistration(); + SimpleAutoServiceRegistration simpleAutoServiceRegistration = new SimpleAutoServiceRegistration(serviceRegistry, properties, registration); + GenericApplicationContext context = new GenericApplicationContext(); + context.refresh(); + simpleAutoServiceRegistration.setApplicationContext(context); + assertFalse(detectRunning(simpleAutoServiceRegistration)); + simpleAutoServiceRegistration.start(); + assertTrue(detectRunning(simpleAutoServiceRegistration)); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/endpoint/BaseServiceRegistrationEndpointTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/endpoint/BaseServiceRegistrationEndpointTest.java new file mode 100644 index 00000000..cbd55d63 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/endpoint/BaseServiceRegistrationEndpointTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.registry.endpoint; + + +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationProperties; +import org.springframework.cloud.client.serviceregistry.Registration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * {@link AbstractServiceRegistrationEndpoint} Base Test + * + * @author Mercy + * @see AbstractServiceRegistrationEndpoint + * @since 1.0.0 + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest( + properties = { + "spring.application.name=test-app", + "spring.cloud.discovery.client.simple.instances.test[0].instanceId=1", + "spring.cloud.discovery.client.simple.instances.test[0].serviceId=test", + "spring.cloud.discovery.client.simple.instances.test[0].host=127.0.0.1", + "spring.cloud.discovery.client.simple.instances.test[0].port=8080", + "spring.cloud.discovery.client.simple.instances.test[0].metadata.key-1=value-1", + "microsphere.spring.cloud.service-registry.auto-registration.simple.enabled=true" + }, + webEnvironment = RANDOM_PORT +) +class BaseServiceRegistrationEndpointTest { + + @Autowired + protected Registration registration; + + @LocalServerPort + protected Integer port; + + @Autowired + protected AutoServiceRegistrationProperties autoServiceRegistrationProperties; +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/endpoint/ServiceDeregistrationEndpointTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/endpoint/ServiceDeregistrationEndpointTest.java new file mode 100644 index 00000000..c1ea2d7e --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/endpoint/ServiceDeregistrationEndpointTest.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.registry.endpoint; + + +import io.microsphere.logging.test.jupiter.LoggingLevelsTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.test.context.ContextConfiguration; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link ServiceDeregistrationEndpoint} Test + * + * @author Mercy + * @see ServiceDeregistrationEndpoint + * @since 1.0.0 + */ +@ContextConfiguration(classes = { + ServiceRegistrationEndpoint.class, + ServiceDeregistrationEndpoint.class +}) +@EnableAutoConfiguration +class ServiceDeregistrationEndpointTest extends BaseServiceRegistrationEndpointTest { + + @Autowired + private ServiceRegistrationEndpoint serviceRegistrationEndpoint; + + @Autowired + private ServiceDeregistrationEndpoint serviceDeregistrationEndpoint; + + @Test + @LoggingLevelsTest(levels = "ERROR") + void testStop() { + assertEquals(this.serviceDeregistrationEndpoint.isRunning(), this.serviceDeregistrationEndpoint.stop()); + assertEquals(this.serviceRegistrationEndpoint.isRunning(), this.serviceRegistrationEndpoint.start()); + assertEquals(this.serviceDeregistrationEndpoint.isRunning(), this.serviceDeregistrationEndpoint.stop()); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/endpoint/ServiceRegistrationEndpointTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/endpoint/ServiceRegistrationEndpointTest.java new file mode 100644 index 00000000..77e9aff3 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/registry/endpoint/ServiceRegistrationEndpointTest.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.registry.endpoint; + + +import io.microsphere.logging.test.jupiter.LoggingLevelsTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.test.context.ContextConfiguration; + +import java.util.Map; + +import static java.lang.Boolean.TRUE; +import static java.lang.Integer.valueOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * {@link ServiceRegistrationEndpoint} Test + * + * @author Mercy + * @see ServiceRegistrationEndpoint + * @since 1.0.0 + */ +@ContextConfiguration(classes = { + ServiceRegistrationEndpoint.class +}) +@EnableAutoConfiguration +class ServiceRegistrationEndpointTest extends BaseServiceRegistrationEndpointTest { + + @Autowired + private ServiceRegistrationEndpoint endpoint; + + @Test + void testMetadata() { + Map metadata = this.endpoint.metadata(); + assertEquals("test-app", metadata.get("application-name")); + assertSame(this.registration, metadata.get("registration")); + assertEquals(this.port, metadata.get("port")); + assertNull(metadata.get("status")); + assertEquals(TRUE, metadata.get("running")); + assertEquals(TRUE, metadata.get("enabled")); + assertEquals(valueOf(0), metadata.get("phase")); + assertEquals(valueOf(0), metadata.get("order")); + assertSame(autoServiceRegistrationProperties, metadata.get("config")); + } + + @Test + @LoggingLevelsTest(levels = "ERROR") + void testStart() { + assertEquals(this.endpoint.isRunning(), this.endpoint.start()); + assertEquals(this.endpoint.isRunning(), this.endpoint.start()); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/util/ServiceInstanceUtilsTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/util/ServiceInstanceUtilsTest.java new file mode 100644 index 00000000..55bc457f --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/client/service/util/ServiceInstanceUtilsTest.java @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.client.service.util; + + +import io.microsphere.json.JSONObject; +import io.microsphere.logging.test.jupiter.LoggingLevelsTest; +import io.microsphere.spring.web.metadata.WebEndpointMapping; +import io.microsphere.spring.web.metadata.WebEndpointMapping.Builder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.client.DefaultServiceInstance; + +import java.net.URI; +import java.util.Collection; + +import static io.microsphere.collection.Lists.ofList; +import static io.microsphere.json.JSONUtils.jsonObject; +import static io.microsphere.spring.cloud.client.service.registry.constants.InstanceConstants.WEB_CONTEXT_PATH_METADATA_NAME; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.attachMetadata; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.getMetadata; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.getUri; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.getUriString; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.getWebEndpointMappings; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.parseWebEndpointMapping; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.parseWebEndpointMappings; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.removeMetadata; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.setMetadata; +import static io.microsphere.spring.cloud.client.service.util.ServiceInstanceUtils.setProperties; +import static io.microsphere.spring.web.metadata.WebEndpointMapping.Kind.SERVLET; +import static io.microsphere.spring.web.metadata.WebEndpointMapping.servlet; +import static io.microsphere.util.StringUtils.EMPTY_STRING; +import static io.microsphere.util.StringUtils.EMPTY_STRING_ARRAY; +import static java.lang.System.currentTimeMillis; +import static java.net.URI.create; +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link ServiceInstanceUtils} Test + * + * @author Mercy + * @see ServiceInstanceUtils + * @since 1.0.0 + */ +public class ServiceInstanceUtilsTest { + + private static final Integer WEB_ENDPOINT_MAPPING_ID = Integer.valueOf(12345); + + private static final String WEB_ENDPOINT_MAPPING_URL_PATTERN = "/test"; + + private static final String WEB_ENDPOINT_MAPPING_METHOD = "GET"; + + private String context = "/"; + + private DefaultServiceInstance serviceInstance; + + private Collection webEndpointMappings; + + @BeforeEach + void setUp() { + this.serviceInstance = createDefaultServiceInstance(); + this.webEndpointMappings = createWebEndpointMappings(); + } + + @Test + @LoggingLevelsTest(levels = "ERROR") + void testAttachMetadata() { + attachMetadata(this.context, this.serviceInstance, this.webEndpointMappings); + assertEquals(this.context, getMetadata(this.serviceInstance, WEB_CONTEXT_PATH_METADATA_NAME)); + } + + @Test + void testGetWebEndpointMappings() { + attachMetadata(this.context, this.serviceInstance, this.webEndpointMappings); + Collection webEndpointMappings = getWebEndpointMappings(this.serviceInstance); + assertEquals(1, webEndpointMappings.size()); + WebEndpointMapping webEndpointMapping = webEndpointMappings.iterator().next(); + assertEquals(WEB_ENDPOINT_MAPPING_ID, webEndpointMapping.getId()); + assertTrue(webEndpointMapping.isNegated()); + assertEquals(WEB_ENDPOINT_MAPPING_URL_PATTERN, webEndpointMapping.getPatterns()[0]); + assertEquals(WEB_ENDPOINT_MAPPING_METHOD, webEndpointMapping.getMethods()[0]); + assertEquals(SERVLET, webEndpointMapping.getKind()); + assertArrayEquals(EMPTY_STRING_ARRAY, webEndpointMapping.getParams()); + assertArrayEquals(EMPTY_STRING_ARRAY, webEndpointMapping.getHeaders()); + assertArrayEquals(EMPTY_STRING_ARRAY, webEndpointMapping.getProduces()); + assertArrayEquals(EMPTY_STRING_ARRAY, webEndpointMapping.getConsumes()); + } + + @Test + void testParseWebEndpointMapping() { + WebEndpointMapping webEndpointMapping = buildWebEndpointMapping(false); + String json = webEndpointMapping.toJSON(); + JSONObject jsonObject = jsonObject(json); + WebEndpointMapping webEndpointMapping1 = parseWebEndpointMapping(jsonObject); + assertEquals(webEndpointMapping, webEndpointMapping1); + } + + @Test + void testParseWebEndpointMappings() { + assertSame(emptyList(), parseWebEndpointMappings(null)); + assertSame(emptyList(), parseWebEndpointMappings(EMPTY_STRING)); + assertSame(emptyList(), parseWebEndpointMappings(" ")); + } + + @Test + void testGetUriString() { + assertEquals("http://localhost:8080", getUriString(this.serviceInstance)); + + String uriString = "https://localhost:8080"; + this.serviceInstance.setUri(create(uriString)); + assertEquals(uriString, getUriString(this.serviceInstance)); + } + + @Test + void testGetUriStringWithoutPort() { + String uriString = "http://localhost"; + this.serviceInstance.setUri(create(uriString)); + assertEquals(uriString + ":80", getUriString(this.serviceInstance)); + + uriString = "https://localhost"; + this.serviceInstance.setUri(create(uriString)); + assertEquals(uriString + ":443", getUriString(this.serviceInstance)); + } + + @Test + void testGetUri() { + URI uri = getUri(this.serviceInstance); + assertEquals(create("http://localhost:8080"), uri); + assertEquals(DefaultServiceInstance.getUri(this.serviceInstance), uri); + + uri = create("https://localhost"); + this.serviceInstance.setUri(uri); + uri = getUri(this.serviceInstance); + assertEquals(create("https://localhost:443"), uri); + assertEquals(DefaultServiceInstance.getUri(this.serviceInstance), uri); + } + + @Test + void testMetadataOps() { + assertNull(getMetadata(this.serviceInstance, WEB_CONTEXT_PATH_METADATA_NAME)); + assertNull(setMetadata(this.serviceInstance, WEB_CONTEXT_PATH_METADATA_NAME, this.context)); + assertEquals(this.context, getMetadata(this.serviceInstance, WEB_CONTEXT_PATH_METADATA_NAME)); + assertEquals(this.context, setMetadata(this.serviceInstance, WEB_CONTEXT_PATH_METADATA_NAME, EMPTY_STRING)); + assertEquals(EMPTY_STRING, getMetadata(this.serviceInstance, WEB_CONTEXT_PATH_METADATA_NAME)); + + assertEquals(EMPTY_STRING, removeMetadata(this.serviceInstance, WEB_CONTEXT_PATH_METADATA_NAME)); + assertNull(getMetadata(this.serviceInstance, WEB_CONTEXT_PATH_METADATA_NAME)); + } + + @Test + void testSetProperties() { + DefaultServiceInstance target = new DefaultServiceInstance(); + setProperties(this.serviceInstance, target); + assertEquals(this.serviceInstance, target); + } + + private Collection createWebEndpointMappings() { + return ofList(buildWebEndpointMapping(true)); + } + + private WebEndpointMapping buildWebEndpointMapping(boolean nagated) { + Builder builder = servlet() + .endpoint(WEB_ENDPOINT_MAPPING_ID) + .method(WEB_ENDPOINT_MAPPING_METHOD) + .pattern(WEB_ENDPOINT_MAPPING_URL_PATTERN); + if (nagated) { + builder.negate(); + } + return builder.build(); + } + + public static DefaultServiceInstance createDefaultServiceInstance() { + DefaultServiceInstance serviceInstance = new DefaultServiceInstance(); + serviceInstance.setInstanceId("ServiceInstance-" + currentTimeMillis()); + serviceInstance.setServiceId("test-service"); + serviceInstance.setHost("localhost"); + serviceInstance.setPort(8080); + serviceInstance.setSecure(false); + return serviceInstance; + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/commons/condition/ConditionalOnUtilEnabledTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/commons/condition/ConditionalOnUtilEnabledTest.java new file mode 100644 index 00000000..ce62854f --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/commons/condition/ConditionalOnUtilEnabledTest.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.commons.condition; + +import io.microsphere.spring.cloud.test.ConditionalOnPropertyEnabledTest; + +/** + * {@link ConditionalOnUtilEnabled} Test + * + * @author Mercy + * @see ConditionalOnUtilEnabled + * @since 1.0.0 + */ +@ConditionalOnUtilEnabled +public class ConditionalOnUtilEnabledTest extends ConditionalOnPropertyEnabledTest { +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/commons/constants/CommonsPropertyConstantsTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/commons/constants/CommonsPropertyConstantsTest.java new file mode 100644 index 00000000..979ad63e --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/commons/constants/CommonsPropertyConstantsTest.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.microsphere.spring.cloud.commons.constants; + +import org.junit.jupiter.api.Test; + +import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.COMPOSITE_REGISTRATION_ENABLED_PROPERTY_NAME; +import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.FEATURES_ENABLED_PROPERTY_NAME; +import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.MICROSPHERE_SPRING_CLOUD_PROPERTY_NAME_PREFIX; +import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.MICROSPHERE_SPRING_CLOUD_WEB_MVC_PROPERTY_NAME_PREFIX; +import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.MULTIPLE_REGISTRATION_DEFAULT_REGISTRATION_PROPERTY_NAME; +import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.MULTIPLE_REGISTRATION_DEFAULT_REGISTRY_PROPERTY_NAME; +import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.MULTIPLE_REGISTRATION_ENABLED_PROPERTY_NAME; +import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.SERVICE_REGISTRY_AUTO_REGISTRATION_ENABLED_PROPERTY_NAME; +import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.SERVICE_REGISTRY_PROPERTY_PREFIX; +import static io.microsphere.spring.cloud.commons.constants.CommonsPropertyConstants.SPRING_CLOUD_PROPERTY_PREFIX; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link CommonsPropertyConstants} Test + * + * @author Mercy + * @see CommonsPropertyConstants + * @since 1.0.0 + */ +class CommonsPropertyConstantsTest { + + @Test + void testConstants() { + assertEquals("spring.cloud.", SPRING_CLOUD_PROPERTY_PREFIX); + assertEquals("spring.cloud.service-registry.", SERVICE_REGISTRY_PROPERTY_PREFIX); + assertEquals("spring.cloud.service-registry.auto-registration.enabled", SERVICE_REGISTRY_AUTO_REGISTRATION_ENABLED_PROPERTY_NAME); + assertEquals("spring.cloud.features.enabled", FEATURES_ENABLED_PROPERTY_NAME); + assertEquals("microsphere.spring.cloud.", MICROSPHERE_SPRING_CLOUD_PROPERTY_NAME_PREFIX); + assertEquals("microsphere.spring.cloud.web.mvc.", MICROSPHERE_SPRING_CLOUD_WEB_MVC_PROPERTY_NAME_PREFIX); + assertEquals("microsphere.spring.cloud.multiple-registration.enabled", MULTIPLE_REGISTRATION_ENABLED_PROPERTY_NAME); + assertEquals("microsphere.spring.cloud.default-registration.type", MULTIPLE_REGISTRATION_DEFAULT_REGISTRATION_PROPERTY_NAME); + assertEquals("microsphere.spring.cloud.default-service-registry.type", MULTIPLE_REGISTRATION_DEFAULT_REGISTRY_PROPERTY_NAME); + assertEquals("microsphere.spring.cloud.composite-registration.enabled", COMPOSITE_REGISTRATION_ENABLED_PROPERTY_NAME); + } +} diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/commons/constants/SpringCloudPropertyConstantsTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/commons/constants/SpringCloudPropertyConstantsTest.java new file mode 100644 index 00000000..41b9a193 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/commons/constants/SpringCloudPropertyConstantsTest.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.commons.constants; + + +import org.junit.jupiter.api.Test; + +import static io.microsphere.spring.cloud.commons.constants.SpringCloudPropertyConstants.FEATURES_ENABLED_PROPERTY_NAME; +import static io.microsphere.spring.cloud.commons.constants.SpringCloudPropertyConstants.LOAD_BALANCER_ENABLED_PROPERTY_NAME; +import static io.microsphere.spring.cloud.commons.constants.SpringCloudPropertyConstants.SERVICE_REGISTRY_AUTO_REGISTRATION_ENABLED_PROPERTY_NAME; +import static io.microsphere.spring.cloud.commons.constants.SpringCloudPropertyConstants.SERVICE_REGISTRY_PROPERTY_PREFIX; +import static io.microsphere.spring.cloud.commons.constants.SpringCloudPropertyConstants.SPRING_CLOUD_PROPERTY_PREFIX; +import static io.microsphere.spring.cloud.commons.constants.SpringCloudPropertyConstants.UTIL_ENABLED_PROPERTY_NAME; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link SpringCloudPropertyConstants} Test + * + * @author Mercy + * @see SpringCloudPropertyConstants + * @since 1.0.0 + */ +class SpringCloudPropertyConstantsTest { + + @Test + void testConstants() { + assertEquals("spring.cloud.", SPRING_CLOUD_PROPERTY_PREFIX); + assertEquals("spring.cloud.service-registry.", SERVICE_REGISTRY_PROPERTY_PREFIX); + assertEquals("spring.cloud.service-registry.auto-registration.enabled", SERVICE_REGISTRY_AUTO_REGISTRATION_ENABLED_PROPERTY_NAME); + assertEquals("spring.cloud.features.enabled", FEATURES_ENABLED_PROPERTY_NAME); + assertEquals("spring.cloud.loadbalancer.enabled", LOAD_BALANCER_ENABLED_PROPERTY_NAME); + assertEquals("spring.cloud.util.enabled", UTIL_ENABLED_PROPERTY_NAME); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/constants/FaultTolerancePropertyConstantsTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/constants/FaultTolerancePropertyConstantsTest.java new file mode 100644 index 00000000..89c83ca7 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/constants/FaultTolerancePropertyConstantsTest.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.fault.tolerance.constants; + + +import org.junit.jupiter.api.Test; + +import static io.microsphere.spring.cloud.fault.tolerance.constants.FaultTolerancePropertyConstants.DEFAULT_WARMUP_TIME_PROPERTY_VALUE; +import static io.microsphere.spring.cloud.fault.tolerance.constants.FaultTolerancePropertyConstants.DEFAULT_WEIGHT_PROPERTY_VALUE; +import static io.microsphere.spring.cloud.fault.tolerance.constants.FaultTolerancePropertyConstants.FAULT_TOLERANCE_PROPERTY_NAME_PREFIX; +import static io.microsphere.spring.cloud.fault.tolerance.constants.FaultTolerancePropertyConstants.LOAD_BALANCER_PROPERTY_PREFIX; +import static io.microsphere.spring.cloud.fault.tolerance.constants.FaultTolerancePropertyConstants.WARMUP_TIME_PROPERTY_NAME; +import static io.microsphere.spring.cloud.fault.tolerance.constants.FaultTolerancePropertyConstants.WEIGHT_PROPERTY_NAME; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link FaultTolerancePropertyConstants} Test + * + * @author Mercy + * @see FaultTolerancePropertyConstants + * @since 1.0.0 + */ +class FaultTolerancePropertyConstantsTest { + + @Test + void testConstants() { + assertEquals("microsphere.spring.cloud.fault-tolerance.", FAULT_TOLERANCE_PROPERTY_NAME_PREFIX); + assertEquals("microsphere.spring.cloud.fault-tolerance.load-balancer.", LOAD_BALANCER_PROPERTY_PREFIX); + assertEquals("microsphere.spring.cloud.fault-tolerance.warmup-time", WARMUP_TIME_PROPERTY_NAME); + assertEquals("microsphere.spring.cloud.fault-tolerance.weight", WEIGHT_PROPERTY_NAME); + assertEquals(600000, DEFAULT_WARMUP_TIME_PROPERTY_VALUE); + assertEquals(100, DEFAULT_WEIGHT_PROPERTY_VALUE); + } + +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/loadbalancer/WeightedRoundRobinTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/loadbalancer/WeightedRoundRobinTest.java new file mode 100644 index 00000000..1710392f --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/loadbalancer/WeightedRoundRobinTest.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.fault.tolerance.loadbalancer; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static io.microsphere.text.FormatUtils.format; +import static java.lang.System.currentTimeMillis; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link WeightedRoundRobin} Test + * + * @author Mercy + * @see WeightedRoundRobin + * @since 1.0.0 + */ +class WeightedRoundRobinTest { + + private WeightedRoundRobin weightedRoundRobin; + + @BeforeEach + void setUp() { + this.weightedRoundRobin = new WeightedRoundRobin("test-id"); + } + + @Test + void testGetId() { + assertEquals("test-id", weightedRoundRobin.getId()); + } + + @Test + void testWeight() { + this.weightedRoundRobin.setWeight(10); + assertEquals(10, this.weightedRoundRobin.getWeight()); + } + + @Test + void testIncreaseCurrent() { + this.weightedRoundRobin.setWeight(10); + assertEquals(10L, this.weightedRoundRobin.increaseCurrent()); + assertEquals(20L, this.weightedRoundRobin.increaseCurrent()); + assertEquals(30L, this.weightedRoundRobin.increaseCurrent()); + } + + @Test + void testSel() { + this.weightedRoundRobin.sel(10); + assertEquals(-10L, this.weightedRoundRobin.current.longValue()); + } + + @Test + void testLastUpdate() { + assertEquals(0L, this.weightedRoundRobin.getLastUpdate()); + long now = currentTimeMillis(); + this.weightedRoundRobin.setLastUpdate(now); + assertEquals(now, this.weightedRoundRobin.getLastUpdate()); + } + + @Test + void testToString() { + this.weightedRoundRobin.setWeight(10); + this.weightedRoundRobin.setLastUpdate(currentTimeMillis()); + assertEquals(format("WeightedRoundRobin[id='{}', weight={}, current={}, lastUpdate={}]", + this.weightedRoundRobin.getId(), + this.weightedRoundRobin.getWeight(), + this.weightedRoundRobin.current, + this.weightedRoundRobin.getLastUpdate()), + this.weightedRoundRobin.toString()); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/loadbalancer/util/LoadBalancerUtilsTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/loadbalancer/util/LoadBalancerUtilsTest.java new file mode 100644 index 00000000..8c32d4e8 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/loadbalancer/util/LoadBalancerUtilsTest.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.fault.tolerance.loadbalancer.util; + + +import org.junit.jupiter.api.Test; + +import static io.microsphere.spring.cloud.fault.tolerance.loadbalancer.util.LoadBalancerUtils.calculateWarmupWeight; +import static java.lang.System.currentTimeMillis; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link LoadBalancerUtils} Test + * + * @author Mercy + * @see LoadBalancerUtils + * @since 1.0.0 + */ +class LoadBalancerUtilsTest { + + @Test + void testCalculateWarmupWeight() throws InterruptedException { + long uptime = currentTimeMillis(); + int weight = 10; + assertTrue(calculateWarmupWeight(uptime, uptime, weight) > 1); + assertTrue(calculateWarmupWeight(uptime, 1, weight) == 1); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/tomcat/autoconfigure/TomcatFaultToleranceAutoConfigurationTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/tomcat/autoconfigure/TomcatFaultToleranceAutoConfigurationTest.java index 320338a0..a5ab57d9 100644 --- a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/tomcat/autoconfigure/TomcatFaultToleranceAutoConfigurationTest.java +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/tomcat/autoconfigure/TomcatFaultToleranceAutoConfigurationTest.java @@ -25,10 +25,16 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.context.WebServerApplicationContext; +import org.springframework.boot.web.context.WebServerInitializedEvent; import org.springframework.boot.web.embedded.tomcat.TomcatWebServer; +import org.springframework.boot.web.server.WebServer; +import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; +import org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent; import org.springframework.cloud.context.environment.EnvironmentManager; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; /** * {@link TomcatFaultToleranceAutoConfiguration} Test @@ -36,11 +42,9 @@ * @author Mercy * @since 1.0.0 */ -@SpringBootTest(classes = { - TomcatFaultToleranceAutoConfigurationTest.class}, - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@SpringBootTest(classes = TomcatFaultToleranceAutoConfigurationTest.class, webEnvironment = RANDOM_PORT) @EnableAutoConfiguration -public class TomcatFaultToleranceAutoConfigurationTest { +class TomcatFaultToleranceAutoConfigurationTest { @Autowired private EnvironmentManager environmentManager; @@ -48,6 +52,11 @@ public class TomcatFaultToleranceAutoConfigurationTest { @Autowired private WebServerApplicationContext context; + @Autowired + private TomcatFaultToleranceAutoConfiguration configuration; + + private TomcatWebServer tomcatWebServer; + private Tomcat tomcat; private Connector connector; @@ -55,15 +64,15 @@ public class TomcatFaultToleranceAutoConfigurationTest { private AbstractHttp11Protocol protocol; @BeforeEach - public void before() { - TomcatWebServer tomcatWebServer = (TomcatWebServer) context.getWebServer(); - this.tomcat = tomcatWebServer.getTomcat(); + void before() { + this.tomcatWebServer = (TomcatWebServer) context.getWebServer(); + this.tomcat = this.tomcatWebServer.getTomcat(); this.connector = tomcat.getConnector(); this.protocol = (AbstractHttp11Protocol) connector.getProtocolHandler(); } @Test - public void testMinSpareThreads() { + void testMinSpareThreads() { // default assertEquals(10, protocol.getMinSpareThreads()); // changed @@ -72,7 +81,7 @@ public void testMinSpareThreads() { } @Test - public void testMaxThreads() { + void testMaxThreads() { // default assertEquals(200, protocol.getMaxThreads()); // changed @@ -81,7 +90,7 @@ public void testMaxThreads() { } @Test - public void testAcceptCount() { + void testAcceptCount() { // default assertEquals(100, protocol.getAcceptCount()); // changed @@ -90,7 +99,7 @@ public void testAcceptCount() { } @Test - public void testConnectionTimeout() { + void testConnectionTimeout() { // default assertEquals(60000, protocol.getConnectionTimeout()); // changed @@ -99,7 +108,7 @@ public void testConnectionTimeout() { } @Test - public void testMaxConnections() { + void testMaxConnections() { // default assertEquals(8192, protocol.getMaxConnections()); // changed @@ -108,16 +117,16 @@ public void testMaxConnections() { } @Test - public void testMaxHttpHeaderSize() { + void testMaxHttpHeaderSize() { // default assertEquals(8192, protocol.getMaxHttpHeaderSize()); // changed - environmentManager.setProperty("server.max-http-header-size", "5120"); + environmentManager.setProperty("server.max-http-request-header-size", "5120"); assertEquals(5120, protocol.getMaxHttpHeaderSize()); } @Test - public void testMaxSwallowSize() { + void testMaxSwallowSize() { // default assertEquals(1024 * 1024 * 2, protocol.getMaxSwallowSize()); // changed @@ -126,11 +135,30 @@ public void testMaxSwallowSize() { } @Test - public void testMaxHttpFormPostSize() { + void testMaxHttpFormPostSize() { // default assertEquals(1024 * 1024 * 2, connector.getMaxPostSize()); // changed environmentManager.setProperty("server.tomcat.max-http-form-post-size", "10240"); assertEquals(10240, connector.getMaxPostSize()); } + + @Test + void testOnWebServerInitializedEventWithWebServerInitializedEvent() { + WebServerInitializedEvent event = new WebServerInitializedEvent(this.tomcatWebServer) { + @Override + public WebServerApplicationContext getApplicationContext() { + return null; + } + }; + configuration.onWebServerInitializedEvent(event); + } + + @Test + void testOnWebServerInitializedEventWithNonTomcatWebServer() { + ServletWebServerApplicationContext context = (ServletWebServerApplicationContext) this.context; + WebServer webServer = mock(WebServer.class); + WebServerInitializedEvent event = new ServletWebServerInitializedEvent(webServer, context); + configuration.onWebServerInitializedEvent(event); + } } diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/tomcat/event/TomcatDynamicConfigurationListenerTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/tomcat/event/TomcatDynamicConfigurationListenerTest.java new file mode 100644 index 00000000..f2e67d85 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/fault/tolerance/tomcat/event/TomcatDynamicConfigurationListenerTest.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.fault.tolerance.tomcat.event; + + +import io.microsphere.spring.test.junit.jupiter.SpringLoggingTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.embedded.tomcat.TomcatWebServer; +import org.springframework.cloud.context.environment.EnvironmentChangeEvent; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.mock.env.MockEnvironment; + +import static io.microsphere.collection.Sets.ofSet; + +/** + * {@link TomcatDynamicConfigurationListener} Testt + * + * @author Mercy + * @see TomcatDynamicConfigurationListener + * @since 1.0.0 + */ +@SpringLoggingTest +class TomcatDynamicConfigurationListenerTest { + + private ServerProperties serverProperties; + + private MockEnvironment environment; + + private ConfigurableApplicationContext context; + + private TomcatDynamicConfigurationListener listener; + + @BeforeEach + void setUp() { + TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); + TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer(); + this.serverProperties = new ServerProperties(); + this.environment = new MockEnvironment(); + this.environment.setProperty("server.port", "8080"); + this.context = new GenericApplicationContext(); + this.context.setEnvironment(this.environment); + this.context.refresh(); + this.listener = new TomcatDynamicConfigurationListener(tomcatWebServer, this.serverProperties, this.context); + } + + @Test + void testOnApplicationEventOnDifferentEventSource() { + EnvironmentChangeEvent event = new EnvironmentChangeEvent(ofSet("test-key")); + this.listener.onApplicationEvent(event); + } + + @Test + void testOnApplicationEventOnEmptyKeys() { + EnvironmentChangeEvent event = new EnvironmentChangeEvent(this.context, ofSet()); + this.listener.onApplicationEvent(event); + } + + @Test + void testOnApplicationEvent() { + String key = "server.tomcat.threads.max"; + this.environment.setProperty(key, "100"); + EnvironmentChangeEvent event = new EnvironmentChangeEvent(this.context, ofSet(key)); + this.listener.onApplicationEvent(event); + } + + @Test + void testConfigureProtocolWithNull() { + this.listener.configureProtocol(this.serverProperties, null); + } + + @Test + void testConfigureHttp11ProtocolWithNull() { + this.listener.configureHttp11Protocol(this.serverProperties, null, null); + } + + @Test + void testIsPositiveWithNegative() { + this.listener.isPositive(-1); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/loadbalancer/condition/ConditionalOnLoadBalancerEnabledTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/loadbalancer/condition/ConditionalOnLoadBalancerEnabledTest.java new file mode 100644 index 00000000..d251003f --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/loadbalancer/condition/ConditionalOnLoadBalancerEnabledTest.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.loadbalancer.condition; + +import io.microsphere.spring.cloud.test.ConditionalOnPropertyEnabledTest; + +/** + * {@link ConditionalOnLoadBalancerEnabled @ConditionalOnLoadBalancerEnabled} Test + * + * @author Mercy + * @see ConditionalOnLoadBalancerEnabled + * @since 1.0.0 + */ +@ConditionalOnLoadBalancerEnabled +public class ConditionalOnLoadBalancerEnabledTest extends ConditionalOnPropertyEnabledTest { +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/test/ConditionalOnPropertyEnabledTest.java b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/test/ConditionalOnPropertyEnabledTest.java new file mode 100644 index 00000000..b5b1aa7c --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/java/io/microsphere/spring/cloud/test/ConditionalOnPropertyEnabledTest.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.test; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Conditional; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; + +import java.util.Properties; +import java.util.Set; + +import static io.microsphere.collection.SetUtils.newLinkedHashSet; +import static io.microsphere.spring.beans.BeanUtils.isBeanPresent; +import static io.microsphere.spring.core.annotation.AnnotationUtils.getAnnotationAttributes; +import static io.microsphere.spring.test.util.SpringTestUtils.testInSpringContainer; +import static io.microsphere.util.ArrayUtils.isEmpty; +import static io.microsphere.util.ArrayUtils.length; +import static java.lang.String.valueOf; +import static java.lang.System.getProperties; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.springframework.core.type.AnnotationMetadata.introspect; +import static org.springframework.util.StringUtils.hasText; + +/** + * Abstract test class for {@link ConditionalOnProperty @ConditionalOnProperty} On Enabled + * + * @author Mercy + * @see ConditionalOnProperty + * @since 1.0.0 + */ +public abstract class ConditionalOnPropertyEnabledTest { + + private AnnotationAttributes annotationAttributes; + + @BeforeEach + void setUp() { + AnnotationMetadata annotationMetadata = introspect(getClass()); + this.annotationAttributes = getAnnotationAttributes(annotationMetadata, ConditionalOnProperty.class); + } + + @Test + void testConditionalOnPropertyEnabled() { + if (matchIfMissing()) { + testBean(true); + } else { + testConditionalOnPropertyEnabled(true); + } + } + + @Test + void testConditionalOnPropertyDisabled() { + testConditionalOnPropertyEnabled(false); + } + + /** + * Whether match if missing + * + * @return {@code true} if match if missing + */ + protected boolean matchIfMissing() { + return this.annotationAttributes.getBoolean("matchIfMissing"); + } + + /** + * Get the property names of the {@link Conditional @Conditional} + * + * @return property names + */ + protected Set getPropertyNames() { + String prefix = this.annotationAttributes.getString("prefix"); + String[] names = this.annotationAttributes.getStringArray("name"); + if (isEmpty(names)) { + names = this.annotationAttributes.getStringArray("value"); + } + boolean hasPrefix = hasText(prefix); + Set propertyNames = newLinkedHashSet(length(names)); + for (String name : names) { + String propertyName = hasPrefix ? prefix + name : name; + propertyNames.add(propertyName); + } + return propertyNames; + } + + protected void testConditionalOnPropertyEnabled(boolean enabled) { + Set propertyNames = getPropertyNames(); + Properties properties = getProperties(); + try { + for (String propertyName : propertyNames) { + properties.setProperty(propertyName, valueOf(enabled)); + } + testBean(enabled); + } finally { + for (String propertyName : propertyNames) { + properties.remove(propertyName); + } + } + } + + protected void testBean(boolean present) { + testInSpringContainer(context -> { + assertEquals(present, isBeanPresent(context, getClass())); + }, getClass()); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/resources/application.yaml b/microsphere-spring-cloud-commons/src/test/resources/META-INF/config/default/test.yaml similarity index 80% rename from microsphere-spring-cloud-commons/src/test/resources/application.yaml rename to microsphere-spring-cloud-commons/src/test/resources/META-INF/config/default/test.yaml index db6965c0..943d64ad 100644 --- a/microsphere-spring-cloud-commons/src/test/resources/application.yaml +++ b/microsphere-spring-cloud-commons/src/test/resources/META-INF/config/default/test.yaml @@ -29,7 +29,9 @@ eureka: --- spring: - profiles: nacos + config: + activate: + on-profile: nacos cloud: nacos: @@ -44,7 +46,9 @@ spring: --- spring: - profiles: eureka + config: + activate: + on-profile: eureka eureka: client: @@ -55,7 +59,9 @@ eureka: --- spring: - profiles: zookeeper + config: + activate: + on-profile: zookeeper cloud: zookeeper: enabled: true @@ -64,7 +70,9 @@ spring: --- spring: - profiles: consul + config: + activate: + on-profile: consul cloud: consul: @@ -75,7 +83,9 @@ spring: --- spring: - profiles: kubernetes + config: + activate: + on-profile: kubernetes cloud: kubernetes: diff --git a/microsphere-spring-cloud-commons/src/test/resources/META-INF/docker/service-registry-servers.yml b/microsphere-spring-cloud-commons/src/test/resources/META-INF/docker/service-registry-servers.yml new file mode 100644 index 00000000..8f34de59 --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/resources/META-INF/docker/service-registry-servers.yml @@ -0,0 +1,25 @@ +services: + zookeeper: + image: zookeeper + restart: always + ports: + - "2181:2181" + + consul: + image: hashicorp/consul + ports: + - "8500:8500" + + nacos: + image: nacos/nacos-server:v2.5.1 + ports: + - "8848:8848" + environment: + - MODE=standalone + + eureka: + image: aliaksandrvoitko/eureka-server + ports: + - "8761:8761" + environment: + - SERVER_PORT=8761 \ No newline at end of file diff --git a/microsphere-spring-cloud-commons/src/test/resources/logback-test.xml b/microsphere-spring-cloud-commons/src/test/resources/logback-test.xml new file mode 100644 index 00000000..0d11a9ed --- /dev/null +++ b/microsphere-spring-cloud-commons/src/test/resources/logback-test.xml @@ -0,0 +1,27 @@ + + + + + + + ${ENCODER_PATTERN} + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/pom.xml b/microsphere-spring-cloud-openfeign/pom.xml index be05c2cc..556c28bf 100644 --- a/microsphere-spring-cloud-openfeign/pom.xml +++ b/microsphere-spring-cloud-openfeign/pom.xml @@ -50,16 +50,18 @@ true + org.springframework.boot spring-boot-starter-test test + - org.springframework.cloud - spring-cloud-starter-loadbalancer - true + io.github.microsphere-projects + microsphere-spring-test + test diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autoconfigure/EnableFeignAutoRefresh.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autoconfigure/EnableFeignAutoRefresh.java index 1587b120..efe2cc14 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autoconfigure/EnableFeignAutoRefresh.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autoconfigure/EnableFeignAutoRefresh.java @@ -3,11 +3,13 @@ import org.springframework.context.annotation.Import; import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** * Enable Feign Auto Refresh * @@ -16,10 +18,11 @@ * @see FeignClientAutoRefreshAutoConfiguration * @since 0.0.1 */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE}) +@Retention(RUNTIME) +@Target(TYPE) @Documented -@Import(FeignClientAutoRefreshAutoConfiguration.class) +@Inherited +@Import(EnableFeignAutoRefresh.Marker.class) public @interface EnableFeignAutoRefresh { class Marker { diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autoconfigure/FeignClientAutoRefreshAutoConfiguration.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autoconfigure/FeignClientAutoRefreshAutoConfiguration.java index 93c5e366..8d1fe2c5 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autoconfigure/FeignClientAutoRefreshAutoConfiguration.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autoconfigure/FeignClientAutoRefreshAutoConfiguration.java @@ -1,15 +1,19 @@ package io.microsphere.spring.cloud.openfeign.autoconfigure; +import io.microsphere.spring.cloud.openfeign.autoconfigure.EnableFeignAutoRefresh.Marker; import io.microsphere.spring.cloud.openfeign.autorefresh.FeignClientConfigurationChangedListener; import io.microsphere.spring.cloud.openfeign.autorefresh.FeignComponentRegistry; import io.microsphere.spring.cloud.openfeign.components.NoOpRequestInterceptor; import org.springframework.beans.factory.BeanFactory; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.cloud.openfeign.FeignBuilderCustomizer; import org.springframework.cloud.openfeign.FeignClientProperties; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.event.EventListener; + +import static io.microsphere.spring.cloud.openfeign.components.NoOpRequestInterceptor.INSTANCE; /** * The Auto-Configuration class for {@link EnableFeignAutoRefresh} @@ -19,30 +23,89 @@ * @see EnableFeignAutoRefresh * @since 0.0.1 */ -@ConditionalOnBean(EnableFeignAutoRefresh.Marker.class) -@AutoConfigureAfter(ConfigurationPropertiesRebinderAutoConfiguration.class) +@ConditionalOnBean(Marker.class) public class FeignClientAutoRefreshAutoConfiguration { + /** + * Creates a {@link FeignBuilderCustomizer} that adds the {@link NoOpRequestInterceptor} + * as a default request interceptor to every Feign client builder. + * + *

    Example Usage: + *

    {@code
    +     * // Automatically registered as a Spring bean; customizes every Feign builder
    +     * FeignBuilderCustomizer customizer = addDefaultRequestInterceptorCustomizer();
    +     * }
    + * + * @return a {@link FeignBuilderCustomizer} that adds the {@link NoOpRequestInterceptor} + */ @Bean public FeignBuilderCustomizer addDefaultRequestInterceptorCustomizer() { return builder -> { - builder.requestInterceptor(NoOpRequestInterceptor.INSTANCE); + builder.requestInterceptor(INSTANCE); }; } - @Bean - public FeignClientConfigurationChangedListener feignClientConfigurationChangedListener(FeignComponentRegistry registry) { - return new FeignClientConfigurationChangedListener(registry); + /** + * Handles the {@link ApplicationReadyEvent} to register the + * {@link FeignClientConfigurationChangedListener} after the application is fully initialized. + * + *

    Example Usage: + *

    {@code
    +     * // Invoked automatically by the Spring event system on application ready
    +     * onApplicationReadyEvent(applicationReadyEvent);
    +     * }
    + * + * @param event the {@link ApplicationReadyEvent} fired when the application is ready + */ + @EventListener(ApplicationReadyEvent.class) + public void onApplicationReadyEvent(ApplicationReadyEvent event) { + /** + * Make sure the FeignClientConfigurationChangedListener is registered after the ConfigurationPropertiesRebinder + */ + registerFeignClientConfigurationChangedListener(event); } + /** + * Creates the {@link FeignComponentRegistry} bean that tracks decorated Feign components + * and supports auto-refresh when configuration properties change. + * + *

    Example Usage: + *

    {@code
    +     * // Automatically registered as a Spring bean
    +     * FeignComponentRegistry registry = feignClientRegistry(clientProperties, beanFactory);
    +     * }
    + * + * @param clientProperties the {@link FeignClientProperties} providing the default config name + * @param beanFactory the {@link BeanFactory} used for component instantiation + * @return a new {@link FeignComponentRegistry} instance + */ @Bean public FeignComponentRegistry feignClientRegistry(FeignClientProperties clientProperties, BeanFactory beanFactory) { return new FeignComponentRegistry(clientProperties.getDefaultConfig(), beanFactory); } + /** + * Creates the {@link FeignClientSpecificationPostProcessor} bean that injects + * the {@link io.microsphere.spring.cloud.openfeign.autorefresh.AutoRefreshCapability} + * into default Feign client specifications. + * + *

    Example Usage: + *

    {@code
    +     * // Automatically registered as a Spring bean
    +     * FeignClientSpecificationPostProcessor processor = feignClientSpecificationPostProcessor();
    +     * }
    + * + * @return a new {@link FeignClientSpecificationPostProcessor} instance + */ @Bean public FeignClientSpecificationPostProcessor feignClientSpecificationPostProcessor() { return new FeignClientSpecificationPostProcessor(); } + private void registerFeignClientConfigurationChangedListener(ApplicationReadyEvent event) { + ConfigurableApplicationContext context = event.getApplicationContext(); + FeignComponentRegistry feignComponentRegistry = context.getBean(FeignComponentRegistry.class); + context.addApplicationListener(new FeignClientConfigurationChangedListener(feignComponentRegistry)); + } + } diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autoconfigure/FeignClientSpecificationPostProcessor.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autoconfigure/FeignClientSpecificationPostProcessor.java index 52eb071b..1ae0ec5e 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autoconfigure/FeignClientSpecificationPostProcessor.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autoconfigure/FeignClientSpecificationPostProcessor.java @@ -4,18 +4,16 @@ import io.microsphere.spring.cloud.openfeign.autorefresh.AutoRefreshCapability; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.cloud.context.named.NamedContextFactory; - -import java.lang.reflect.Method; -import java.util.Arrays; +import org.springframework.cloud.openfeign.FeignClientSpecification; import static io.microsphere.logging.LoggerFactory.getLogger; -import static io.microsphere.reflect.MethodUtils.findMethod; +import static io.microsphere.util.ArrayUtils.arrayToString; import static io.microsphere.util.ArrayUtils.combine; import static org.springframework.aop.support.AopUtils.getTargetClass; -import static org.springframework.util.ClassUtils.resolveClassName; /** + * {@link BeanPostProcessor} for {@link FeignClientSpecification} + * * @author 韩超 * @author Mercy * @see org.springframework.cloud.openfeign.FeignClientSpecification @@ -25,37 +23,41 @@ public class FeignClientSpecificationPostProcessor implements BeanPostProcessor private static final Logger logger = getLogger(FeignClientSpecificationPostProcessor.class); - private static final Class AUTO_REFRESH_CAPABILITY_CLASS = AutoRefreshCapability.class; - - private static final String FEIGN_CLIENT_SPECIFICATION_CLASS_NAME = "org.springframework.cloud.openfeign.FeignClientSpecification"; - - private static final Class FEIGN_CLIENT_SPECIFICATION_CLASS = resolveClassName(FEIGN_CLIENT_SPECIFICATION_CLASS_NAME, null); - - private static final Method setConfigurationMethod = findMethod(FEIGN_CLIENT_SPECIFICATION_CLASS, "setConfiguration", Class[].class); - + static final Class AUTO_REFRESH_CAPABILITY_CLASS = AutoRefreshCapability.class; + + static final Class FEIGN_CLIENT_SPECIFICATION_CLASS = FeignClientSpecification.class; + + /** + * Injects the {@link AutoRefreshCapability} into default {@link FeignClientSpecification} + * beans after initialization. + * + *

    Example Usage: + *

    {@code
    +     * // Invoked automatically by the Spring container during bean post-processing
    +     * Object processed = postProcessAfterInitialization(bean, "default.my-client");
    +     * }
    + * + * @param bean the bean instance that has been initialized + * @param beanName the name of the bean in the Spring context + * @return the (possibly modified) bean instance + * @throws BeansException if post-processing fails + */ @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { Class beanType = getTargetClass(bean); if (FEIGN_CLIENT_SPECIFICATION_CLASS.isAssignableFrom(beanType) && beanName.startsWith("default")) { - injectAutoRefreshCapability((NamedContextFactory.Specification) bean); + injectAutoRefreshCapability((FeignClientSpecification) bean); } return bean; } - private void injectAutoRefreshCapability(NamedContextFactory.Specification defaultSpecification) { - if (setConfigurationMethod != null) { - Class[] originConfigurationClasses = defaultSpecification.getConfiguration(); - Class[] newConfigurationClasses = combine(AUTO_REFRESH_CAPABILITY_CLASS, originConfigurationClasses); - Object arg = newConfigurationClasses; - try { - setConfigurationMethod.setAccessible(true); - setConfigurationMethod.invoke(defaultSpecification, arg); - } catch (Throwable e) { - if (logger.isWarnEnabled()) { - logger.warn("FeignClientSpecification#setConfiguration(Class[]) can't be invoked , instance : {} , args : {}", - defaultSpecification, Arrays.toString(newConfigurationClasses)); - } - } + void injectAutoRefreshCapability(FeignClientSpecification specification) { + Class[] originConfigurationClasses = specification.getConfiguration(); + Class[] newConfigurationClasses = combine(AUTO_REFRESH_CAPABILITY_CLASS, originConfigurationClasses); + specification.setConfiguration(newConfigurationClasses); + if (logger.isTraceEnabled()) { + logger.trace("The Configuration classes: before - {} , after - {}", arrayToString(originConfigurationClasses), + arrayToString(newConfigurationClasses)); } } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autorefresh/AutoRefreshCapability.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autorefresh/AutoRefreshCapability.java index 0e29d0b2..d9dc7ddd 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autorefresh/AutoRefreshCapability.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autorefresh/AutoRefreshCapability.java @@ -12,111 +12,239 @@ import io.microsphere.spring.cloud.openfeign.components.DecoratedDecoder; import io.microsphere.spring.cloud.openfeign.components.DecoratedEncoder; import io.microsphere.spring.cloud.openfeign.components.DecoratedErrorDecoder; -import io.microsphere.spring.cloud.openfeign.components.DecoratedFeignComponent; import io.microsphere.spring.cloud.openfeign.components.DecoratedQueryMapEncoder; import io.microsphere.spring.cloud.openfeign.components.DecoratedRetryer; import org.springframework.beans.BeansException; +import org.springframework.cloud.context.named.NamedContextFactory; import org.springframework.cloud.openfeign.FeignClientProperties; -import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.cloud.openfeign.FeignClientSpecification; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import static io.microsphere.spring.cloud.openfeign.components.DecoratedFeignComponent.instantiate; + /** * @author 韩超 + * @author Mercy * @since 0.0.1 */ public class AutoRefreshCapability implements Capability, ApplicationContextAware { private final FeignComponentRegistry componentRegistry; - private final FeignContext feignContext; + + private final NamedContextFactory contextFactory; + private final FeignClientProperties clientProperties; private String contextId; - private static final String CONTEXT_ID_PROPERTY_NAME = "feign.client.name"; - - public AutoRefreshCapability(FeignClientProperties clientProperties, FeignContext feignContext, FeignComponentRegistry componentRegistry) { + /** + * Constructs an {@link AutoRefreshCapability} with the required dependencies. + * + *

    Example Usage: + *

    {@code
    +     * AutoRefreshCapability capability = new AutoRefreshCapability(
    +     *     clientProperties, contextFactory, componentRegistry);
    +     * }
    + * + * @param clientProperties the {@link FeignClientProperties} providing Feign client configuration + * @param contextFactory the {@link NamedContextFactory} for resolving per-client contexts + * @param componentRegistry the {@link FeignComponentRegistry} to register decorated components + */ + public AutoRefreshCapability(FeignClientProperties clientProperties, + NamedContextFactory contextFactory, + FeignComponentRegistry componentRegistry) { this.clientProperties = clientProperties; - this.feignContext = feignContext; + this.contextFactory = contextFactory; this.componentRegistry = componentRegistry; } + /** + * Sets the {@link ApplicationContext} and extracts the Feign client context ID + * from the {@code spring.cloud.openfeign.client.name} property. + * + *

    Example Usage: + *

    {@code
    +     * AutoRefreshCapability capability = new AutoRefreshCapability(props, factory, registry);
    +     * capability.setApplicationContext(applicationContext);
    +     * }
    + * + * @param applicationContext the {@link ApplicationContext} for this Feign client + * @throws BeansException if the context cannot be set + */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.contextId = applicationContext.getEnvironment().getProperty(CONTEXT_ID_PROPERTY_NAME); + this.contextId = applicationContext.getEnvironment().getProperty("spring.cloud.openfeign.client.name"); } - + /** + * Enriches the given {@link Retryer} by wrapping it in a {@link DecoratedRetryer} + * that supports auto-refresh on configuration changes. + * + *

    Example Usage: + *

    {@code
    +     * Retryer original = new Retryer.Default();
    +     * Retryer enriched = capability.enrich(original);
    +     * }
    + * + * @param retryer the original {@link Retryer} to enrich, or {@code null} + * @return the decorated {@link Retryer}, or {@code null} if the input is {@code null} + */ @Override public Retryer enrich(Retryer retryer) { - if (retryer == null) + if (retryer == null) { return null; + } - DecoratedRetryer decoratedRetryer = DecoratedFeignComponent.instantiate(DecoratedRetryer.class, Retryer.class, - contextId, feignContext, clientProperties, retryer); - + DecoratedRetryer decoratedRetryer = instantiate(DecoratedRetryer.class, Retryer.class, + contextId, contextFactory, clientProperties, retryer); this.componentRegistry.register(contextId, decoratedRetryer); return decoratedRetryer; } + /** + * Enriches the given {@link Contract} by wrapping it in a {@link DecoratedContract} + * that supports auto-refresh on configuration changes. + * + *

    Example Usage: + *

    {@code
    +     * Contract original = new Contract.Default();
    +     * Contract enriched = capability.enrich(original);
    +     * }
    + * + * @param contract the original {@link Contract} to enrich, or {@code null} + * @return the decorated {@link Contract}, or {@code null} if the input is {@code null} + */ @Override public Contract enrich(Contract contract) { - if (contract == null) + if (contract == null) { return null; + } - DecoratedContract decoratedContract = DecoratedFeignComponent.instantiate(DecoratedContract.class, Contract.class, - contextId, feignContext, clientProperties, contract); + DecoratedContract decoratedContract = instantiate(DecoratedContract.class, Contract.class, + contextId, contextFactory, clientProperties, contract); this.componentRegistry.register(contextId, decoratedContract); return decoratedContract; } + /** + * Enriches the given {@link Decoder} by wrapping it in a {@link DecoratedDecoder} + * that supports auto-refresh on configuration changes. + * + *

    Example Usage: + *

    {@code
    +     * Decoder original = new Decoder.Default();
    +     * Decoder enriched = capability.enrich(original);
    +     * }
    + * + * @param decoder the original {@link Decoder} to enrich, or {@code null} + * @return the decorated {@link Decoder}, or {@code null} if the input is {@code null} + */ @Override public Decoder enrich(Decoder decoder) { - if (decoder == null) + if (decoder == null) { return null; + } - DecoratedDecoder decoratedDecoder = DecoratedFeignComponent.instantiate(DecoratedDecoder.class, Decoder.class, - contextId, feignContext, clientProperties, decoder); + DecoratedDecoder decoratedDecoder = instantiate(DecoratedDecoder.class, Decoder.class, + contextId, contextFactory, clientProperties, decoder); this.componentRegistry.register(contextId, decoratedDecoder); return decoratedDecoder; } + /** + * Enriches the given {@link Encoder} by wrapping it in a {@link DecoratedEncoder} + * that supports auto-refresh on configuration changes. + * + *

    Example Usage: + *

    {@code
    +     * Encoder original = new Encoder.Default();
    +     * Encoder enriched = capability.enrich(original);
    +     * }
    + * + * @param encoder the original {@link Encoder} to enrich, or {@code null} + * @return the decorated {@link Encoder}, or {@code null} if the input is {@code null} + */ @Override public Encoder enrich(Encoder encoder) { - if (encoder == null) + if (encoder == null) { return null; + } - DecoratedEncoder decoratedEncoder = DecoratedFeignComponent.instantiate(DecoratedEncoder.class, Encoder.class, - contextId, feignContext, clientProperties, encoder); + DecoratedEncoder decoratedEncoder = instantiate(DecoratedEncoder.class, Encoder.class, + contextId, contextFactory, clientProperties, encoder); this.componentRegistry.register(contextId, decoratedEncoder); return decoratedEncoder; } + /** + * Enriches the given {@link ErrorDecoder} by wrapping it in a {@link DecoratedErrorDecoder} + * that supports auto-refresh on configuration changes. + * + *

    Example Usage: + *

    {@code
    +     * ErrorDecoder original = new ErrorDecoder.Default();
    +     * ErrorDecoder enriched = capability.enrich(original);
    +     * }
    + * + * @param decoder the original {@link ErrorDecoder} to enrich, or {@code null} + * @return the decorated {@link ErrorDecoder}, or {@code null} if the input is {@code null} + */ public ErrorDecoder enrich(ErrorDecoder decoder) { - if (decoder == null) + if (decoder == null) { return null; + } - DecoratedErrorDecoder decoratedErrorDecoder = DecoratedFeignComponent.instantiate(DecoratedErrorDecoder.class, ErrorDecoder.class, - contextId, feignContext, clientProperties, decoder); + DecoratedErrorDecoder decoratedErrorDecoder = instantiate(DecoratedErrorDecoder.class, ErrorDecoder.class, + contextId, contextFactory, clientProperties, decoder); this.componentRegistry.register(contextId, decoratedErrorDecoder); return decoratedErrorDecoder; } + /** + * Enriches the given {@link RequestInterceptor} by registering it in the + * {@link FeignComponentRegistry} as part of a {@link io.microsphere.spring.cloud.openfeign.components.CompositedRequestInterceptor}. + * + *

    Example Usage: + *

    {@code
    +     * RequestInterceptor original = template -> template.header("X-Custom", "value");
    +     * RequestInterceptor enriched = capability.enrich(original);
    +     * }
    + * + * @param requestInterceptor the original {@link RequestInterceptor} to enrich, or {@code null} + * @return the composited {@link RequestInterceptor}, or {@code null} if the input is {@code null} + */ @Override public RequestInterceptor enrich(RequestInterceptor requestInterceptor) { + if (requestInterceptor == null) { + return null; + } return this.componentRegistry.registerRequestInterceptor(contextId, requestInterceptor); } + /** + * Enriches the given {@link QueryMapEncoder} by wrapping it in a {@link DecoratedQueryMapEncoder} + * that supports auto-refresh on configuration changes. + * + *

    Example Usage: + *

    {@code
    +     * QueryMapEncoder original = new QueryMapEncoder.Default();
    +     * QueryMapEncoder enriched = capability.enrich(original);
    +     * }
    + * + * @param queryMapEncoder the original {@link QueryMapEncoder} to enrich, or {@code null} + * @return the decorated {@link QueryMapEncoder}, or {@code null} if the input is {@code null} + */ @Override public QueryMapEncoder enrich(QueryMapEncoder queryMapEncoder) { - if (queryMapEncoder == null) + if (queryMapEncoder == null) { return null; + } - DecoratedQueryMapEncoder decoratedQueryMapEncoder = DecoratedFeignComponent.instantiate(DecoratedQueryMapEncoder.class, QueryMapEncoder.class, - contextId, feignContext, clientProperties, queryMapEncoder); + DecoratedQueryMapEncoder decoratedQueryMapEncoder = instantiate(DecoratedQueryMapEncoder.class, QueryMapEncoder.class, + contextId, contextFactory, clientProperties, queryMapEncoder); this.componentRegistry.register(contextId, decoratedQueryMapEncoder); return decoratedQueryMapEncoder; } - -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autorefresh/FeignClientConfigurationChangedListener.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autorefresh/FeignClientConfigurationChangedListener.java index c4cede8c..7fae5c63 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autorefresh/FeignClientConfigurationChangedListener.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autorefresh/FeignClientConfigurationChangedListener.java @@ -15,19 +15,57 @@ public class FeignClientConfigurationChangedListener implements ApplicationListe private final FeignComponentRegistry registry; + /** + * Constructs a listener that refreshes Feign components when the environment changes. + * + *

    Example Usage: + *

    {@code
    +     * FeignComponentRegistry registry = ...;
    +     * FeignClientConfigurationChangedListener listener =
    +     *     new FeignClientConfigurationChangedListener(registry);
    +     * }
    + * + * @param registry the {@link FeignComponentRegistry} used to refresh affected Feign components + */ public FeignClientConfigurationChangedListener(FeignComponentRegistry registry) { this.registry = registry; } - private final String PREFIX = "feign.client.config."; + private final String PREFIX = "spring.cloud.openfeign.client.config."; + /** + * Handles an {@link EnvironmentChangeEvent} by resolving which Feign clients are affected + * and triggering a refresh on the corresponding components in the registry. + * + *

    Example Usage: + *

    {@code
    +     * // Invoked automatically by the Spring event system
    +     * listener.onApplicationEvent(environmentChangeEvent);
    +     * }
    + * + * @param event the {@link EnvironmentChangeEvent} containing the changed property keys + */ @Override public void onApplicationEvent(EnvironmentChangeEvent event) { Map> effectiveClients = resolveChangedClient(event); - effectiveClients.forEach(registry::refresh); - + if (!effectiveClients.isEmpty()) { + effectiveClients.forEach(registry::refresh); + } } + /** + * Resolves which Feign client names and their changed configuration keys are affected + * by the given {@link EnvironmentChangeEvent}. + * + *

    Example Usage: + *

    {@code
    +     * Map> changed = listener.resolveChangedClient(event);
    +     * // e.g. {"my-client" -> {"retryer", "decoder"}}
    +     * }
    + * + * @param event the {@link EnvironmentChangeEvent} containing the changed property keys + * @return a map of client names to their changed configuration sub-keys + */ protected Map> resolveChangedClient(EnvironmentChangeEvent event) { Set keys = event.getKeys(); return keys.stream() diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autorefresh/FeignComponentRegistry.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autorefresh/FeignComponentRegistry.java index ec97a928..4c20fae0 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autorefresh/FeignComponentRegistry.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/autorefresh/FeignComponentRegistry.java @@ -8,139 +8,279 @@ import feign.codec.Encoder; import feign.codec.ErrorDecoder; import io.microsphere.spring.cloud.openfeign.components.CompositedRequestInterceptor; -import io.microsphere.spring.cloud.openfeign.components.NoOpRequestInterceptor; import io.microsphere.spring.cloud.openfeign.components.Refreshable; import org.springframework.beans.factory.BeanFactory; -import org.springframework.util.ObjectUtils; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import static io.microsphere.collection.Lists.ofList; +import static io.microsphere.collection.Maps.ofMap; +import static io.microsphere.collection.Sets.ofSet; +import static io.microsphere.constants.SymbolConstants.LEFT_SQUARE_BRACKET; +import static io.microsphere.spring.boot.context.properties.source.util.ConfigurationPropertyUtils.toDashedForm; +import static io.microsphere.spring.cloud.openfeign.components.NoOpRequestInterceptor.INSTANCE; +import static io.microsphere.util.Assert.assertNoNullElements; +import static io.microsphere.util.Assert.assertNotBlank; +import static io.microsphere.util.Assert.assertNotEmpty; +import static io.microsphere.util.Assert.assertNotNull; +import static io.microsphere.util.StringUtils.isBlank; +import static io.microsphere.util.StringUtils.substringBefore; + /** + * Feign Component Registry + * * @author 韩超 + * @author Mercy * @since 0.0.1 */ public class FeignComponentRegistry { - private static final Map> configComponentMappings = new HashMap<>(16); + private static final Map> configComponentMappings = ofMap( + "retryer", Retryer.class, + "error-decoder", ErrorDecoder.class, + "request-interceptors", RequestInterceptor.class, + "default-request-headers", RequestInterceptor.class, + "default-query-parameters", RequestInterceptor.class, + "decoder", Decoder.class, + "encoder", Encoder.class, + "contract", Contract.class, + "query-map-encoder", QueryMapEncoder.class + ); private final Map> refreshableComponents = new ConcurrentHashMap<>(32); - private final Map interceptorMap = new ConcurrentHashMap<>(32); - private final String DEFAULT_CLIENT_NAME; - private final BeanFactory beanFactory; + private final Map interceptorsMap = new ConcurrentHashMap<>(32); - static { - configComponentMappings.put("retryer", Retryer.class); - configComponentMappings.put("errorDecoder", ErrorDecoder.class); - configComponentMappings.put("error-decoder", ErrorDecoder.class); - configComponentMappings.put("requestInterceptors", RequestInterceptor.class); - configComponentMappings.put("request-interceptors", RequestInterceptor.class); - configComponentMappings.put("defaultRequestHeaders", RequestInterceptor.class); - configComponentMappings.put("default-request-headers", RequestInterceptor.class); - configComponentMappings.put("defaultQueryParameters", RequestInterceptor.class); - configComponentMappings.put("default-query-parameters", RequestInterceptor.class); - configComponentMappings.put("decoder", Decoder.class); - configComponentMappings.put("encoder", Encoder.class); - configComponentMappings.put("contract", Contract.class); - configComponentMappings.put("queryMapEncoder", QueryMapEncoder.class); - configComponentMappings.put("query-map-encoder", QueryMapEncoder.class); - } + private final String defaultClientName; + + private final BeanFactory beanFactory; + /** + * Returns the Feign component class corresponding to the given configuration key. + * + *

    Example Usage: + *

    {@code
    +     * Class componentClass = FeignComponentRegistry.getComponentClass("retryer");
    +     * // returns Retryer.class
    +     * }
    + * + * @param config the configuration property key (e.g. {@code "retryer"}, {@code "decoder"}) + * @return the mapped Feign component {@link Class}, or {@code null} if not found + */ protected static Class getComponentClass(String config) { - if (ObjectUtils.isEmpty(config)) + if (isBlank(config)) { return null; - //组合 - if (config.endsWith("]")) { - for (Map.Entry> next : configComponentMappings.entrySet()) { - if (config.startsWith(next.getKey())) - return next.getValue(); - } - } else { - for (Map.Entry> next : configComponentMappings.entrySet()) { - if (config.equals(next.getKey())) - return next.getValue(); - } } - return null; + String normalizedConfig = normalizeConfig(config); + return configComponentMappings.get(normalizedConfig); } + /** + * Normalizes a configuration key by stripping array index suffixes and converting + * to dashed form. + * + *

    Example Usage: + *

    {@code
    +     * String normalized = FeignComponentRegistry.normalizeConfig("requestInterceptors[0]");
    +     * // returns "request-interceptors"
    +     * }
    + * + * @param config the raw configuration property key + * @return the normalized, dashed-form configuration key + */ + static String normalizeConfig(String config) { + String normalizedConfig = substringBefore(config, LEFT_SQUARE_BRACKET); + return toDashedForm(normalizedConfig); + } + + /** + * Constructs a {@link FeignComponentRegistry} with the given default client name + * and {@link BeanFactory}. + * + *

    Example Usage: + *

    {@code
    +     * FeignComponentRegistry registry = new FeignComponentRegistry("default", beanFactory);
    +     * }
    + * + * @param defaultClientName the name of the default Feign client configuration + * @param beanFactory the {@link BeanFactory} used for component resolution + */ public FeignComponentRegistry(String defaultClientName, BeanFactory beanFactory) { - this.DEFAULT_CLIENT_NAME = defaultClientName; + this.defaultClientName = defaultClientName; this.beanFactory = beanFactory; } + /** + * Registers a list of {@link Refreshable} components for the specified Feign client. + * + *

    Example Usage: + *

    {@code
    +     * List components = List.of(decoratedContract, decoratedDecoder);
    +     * registry.register("my-client", components);
    +     * }
    + * + * @param clientName the Feign client name + * @param components the list of {@link Refreshable} components to register + */ public void register(String clientName, List components) { + assertNotBlank(clientName, () -> "The 'clientName' must not be blank!"); + assertNotEmpty(components, () -> "The 'components' must not be empty!"); + assertNoNullElements(components, () -> "The 'components' must not contain the null element!"); List componentList = this.refreshableComponents.computeIfAbsent(clientName, name -> new ArrayList<>()); - componentList.addAll(componentList); + componentList.addAll(components); } + /** + * Registers a single {@link Refreshable} component for the specified Feign client. + * + *

    Example Usage: + *

    {@code
    +     * registry.register("my-client", decoratedContract);
    +     * }
    + * + * @param clientName the Feign client name + * @param component the {@link Refreshable} component to register + */ public void register(String clientName, Refreshable component) { - List componentList = this.refreshableComponents.computeIfAbsent(clientName, name -> new ArrayList<>()); - componentList.add(component); + register(clientName, ofList(component)); } + /** + * Registers a {@link RequestInterceptor} for the specified Feign client. Interceptors + * are collected into a {@link CompositedRequestInterceptor} per client. + * + *

    Example Usage: + *

    {@code
    +     * RequestInterceptor interceptor = template -> template.header("X-Custom", "value");
    +     * RequestInterceptor result = registry.registerRequestInterceptor("my-client", interceptor);
    +     * }
    + * + * @param clientName the Feign client name + * @param requestInterceptor the {@link RequestInterceptor} to register + * @return the {@link CompositedRequestInterceptor} if this is the first interceptor + * for the client, or {@link io.microsphere.spring.cloud.openfeign.components.NoOpRequestInterceptor#INSTANCE} otherwise + */ public RequestInterceptor registerRequestInterceptor(String clientName, RequestInterceptor requestInterceptor) { - CompositedRequestInterceptor compositedRequestInterceptor = this.interceptorMap.computeIfAbsent(clientName, (name) -> new CompositedRequestInterceptor(clientName, beanFactory)); + assertNotBlank(clientName, () -> "The 'clientName' must not be blank!"); + assertNotNull(requestInterceptor, () -> "The 'requestInterceptor' must not be null!"); + CompositedRequestInterceptor compositedRequestInterceptor = this.interceptorsMap.computeIfAbsent(clientName, (name) -> new CompositedRequestInterceptor(clientName, beanFactory)); if (compositedRequestInterceptor.addRequestInterceptor(requestInterceptor)) { return compositedRequestInterceptor; - } else return NoOpRequestInterceptor.INSTANCE; + } + return INSTANCE; } - public synchronized void refresh(String clientName, Set changedConfig) { + /** + * Refreshes the Feign components for the specified client whose configurations have changed. + * + *

    Example Usage: + *

    {@code
    +     * registry.refresh("my-client", "retryer", "decoder");
    +     * }
    + * + * @param clientName the Feign client name + * @param changedConfigs the configuration keys that have changed + */ + public void refresh(String clientName, String... changedConfigs) { + refresh(clientName, ofSet(changedConfigs)); + } + + /** + * Refreshes the Feign components for the specified client based on a set of changed + * configuration keys. If the default client configuration changed, all registered + * components are refreshed. + * + *

    Example Usage: + *

    {@code
    +     * Set changed = Set.of("my-client.retryer", "my-client.decoder");
    +     * registry.refresh("my-client", changed);
    +     * }
    + * + * @param clientName the Feign client name + * @param changedConfigs the set of changed configuration sub-keys + */ + public synchronized void refresh(String clientName, Set changedConfigs) { + Set> effectiveComponents = new HashSet<>(changedConfigs.size()); - Set> effectiveComponents = new HashSet<>(); boolean hasInterceptor = false; - for (String value : changedConfig) { - value = value.replace(clientName + ".", ""); - Class clazz = getComponentClass(value); + for (String changedConfig : changedConfigs) { + changedConfig = changedConfig.replace(clientName + ".", ""); + Class clazz = getComponentClass(changedConfig); if (clazz != null) { effectiveComponents.add(clazz); - hasInterceptor = clazz.equals(RequestInterceptor.class); + hasInterceptor = RequestInterceptor.class.equals(clazz); } } - if (DEFAULT_CLIENT_NAME.equals(clientName)) { + if (defaultClientName.equals(clientName)) { //default configs changed, need refresh all refreshableComponents.values().stream() .flatMap(List::stream) - .filter(component -> { - Class componentsClass = component.getClass(); - for (Class actualComponent : effectiveComponents) - if (actualComponent.isAssignableFrom(componentsClass)) - return true; - return false; - }) + .filter(component -> isComponentPresent(component, effectiveComponents)) .forEach(Refreshable::refresh); - if (hasInterceptor) - this.interceptorMap.values() - .forEach(CompositedRequestInterceptor::refresh); + if (hasInterceptor) { + this.interceptorsMap.values().forEach(CompositedRequestInterceptor::refresh); + } return; } + List components = this.refreshableComponents.get(clientName); - if (components != null) + if (components != null) { components.stream() - .filter(component -> { - Class componentsClass = component.getClass(); - for (Class actualComponent : effectiveComponents) - if (actualComponent.isAssignableFrom(componentsClass)) - return true; - return false; - }) + .filter(component -> isComponentPresent(component, effectiveComponents)) .forEach(Refreshable::refresh); + } if (hasInterceptor) { - CompositedRequestInterceptor requestInterceptor = this.interceptorMap.get(clientName); + CompositedRequestInterceptor requestInterceptor = this.interceptorsMap.get(clientName); if (requestInterceptor != null) requestInterceptor.refresh(); } + } + /** + * Checks whether the given {@link Refreshable} component's class is assignable from + * any of the effective component classes. + * + *

    Example Usage: + *

    {@code
    +     * boolean present = FeignComponentRegistry.isComponentPresent(
    +     *     refreshableComponent, List.of(Retryer.class, Decoder.class));
    +     * }
    + * + * @param component the {@link Refreshable} component to check + * @param effectiveComponents the component classes to match against + * @return {@code true} if the component matches any of the effective classes + */ + static boolean isComponentPresent(Refreshable component, Iterable> effectiveComponents) { + return isComponentClassPresent(component.getClass(), effectiveComponents); } + /** + * Checks whether the given class is assignable from any of the effective component classes. + * + *

    Example Usage: + *

    {@code
    +     * boolean present = FeignComponentRegistry.isComponentClassPresent(
    +     *     DecoratedRetryer.class, List.of(Retryer.class));
    +     * }
    + * + * @param componentsClass the class to check + * @param effectiveComponents the component classes to match against + * @return {@code true} if the class is assignable from any effective class + */ + static boolean isComponentClassPresent(Class componentsClass, Iterable> effectiveComponents) { + for (Class actualComponent : effectiveComponents) { + if (actualComponent.isAssignableFrom(componentsClass)) { + return true; + } + } + return false; + } } diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/CompositedRequestInterceptor.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/CompositedRequestInterceptor.java index 4cb3cf9f..4f051ee3 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/CompositedRequestInterceptor.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/CompositedRequestInterceptor.java @@ -2,97 +2,150 @@ import feign.RequestInterceptor; import feign.RequestTemplate; -import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.openfeign.FeignClientProperties; -import org.springframework.util.CollectionUtils; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; + +import static io.microsphere.collection.CollectionUtils.isNotEmpty; +import static io.microsphere.collection.MapUtils.isNotEmpty; +import static java.util.Collections.unmodifiableSet; +import static org.springframework.beans.BeanUtils.instantiateClass; /** * @author 韩超 + * @author Mercy * @since 0.0.1 */ public class CompositedRequestInterceptor implements RequestInterceptor, Refreshable { private final BeanFactory beanFactory; - private final String contextId; - private final Set set = new HashSet<>(); + private final String contextId; + private final Set set = new LinkedHashSet<>(); + + /** + * Constructs a {@link CompositedRequestInterceptor} for the specified Feign client context. + * + *

    Example Usage: + *

    {@code
    +     * CompositedRequestInterceptor interceptor =
    +     *     new CompositedRequestInterceptor("my-client", beanFactory);
    +     * }
    + * + * @param contextId the Feign client context ID + * @param beanFactory the {@link BeanFactory} for resolving interceptor instances + */ public CompositedRequestInterceptor(String contextId, BeanFactory beanFactory) { this.beanFactory = beanFactory; this.contextId = contextId; } + /** + * Returns an unmodifiable view of the registered {@link RequestInterceptor} instances. + * + *

    Example Usage: + *

    {@code
    +     * Set interceptors = compositedInterceptor.getRequestInterceptors();
    +     * }
    + * + * @return an unmodifiable {@link Set} of registered request interceptors + */ + public Set getRequestInterceptors() { + return unmodifiableSet(set); + } + /** + * Applies all registered {@link RequestInterceptor} instances to the given + * {@link RequestTemplate} in order. + * + *

    Example Usage: + *

    {@code
    +     * RequestTemplate template = new RequestTemplate();
    +     * compositedInterceptor.apply(template);
    +     * }
    + * + * @param template the {@link RequestTemplate} to apply interceptors to + */ @Override public void apply(RequestTemplate template) { synchronized (this.set) { - if (!this.set.isEmpty()) - set.forEach(requestInterceptor -> requestInterceptor.apply(template)); + set.forEach(requestInterceptor -> requestInterceptor.apply(template)); } - } + /** + * Adds a {@link RequestInterceptor} to this composite. Returns {@code true} if this + * is the first interceptor added (i.e., the composite was previously empty). + * + *

    Example Usage: + *

    {@code
    +     * boolean wasFirst = compositedInterceptor.addRequestInterceptor(
    +     *     template -> template.header("Authorization", "Bearer token"));
    +     * }
    + * + * @param requestInterceptor the {@link RequestInterceptor} to add + * @return {@code true} if this was the first interceptor added, {@code false} otherwise + */ public boolean addRequestInterceptor(RequestInterceptor requestInterceptor) { synchronized (this.set) { boolean isFirst = this.set.isEmpty(); this.set.add(requestInterceptor); return isFirst; } - } private RequestInterceptor getInterceptorOrInstantiate(Class clazz) { - try { - return this.beanFactory.getBean(clazz); - } catch (Exception e) { - return BeanUtils.instantiateClass(clazz); - } + return getOrInstantiate(clazz); } + /** + * Refreshes the set of {@link RequestInterceptor} instances by re-reading the + * {@link FeignClientProperties} configuration for request interceptors, default + * headers, and default query parameters. + * + *

    Example Usage: + *

    {@code
    +     * compositedInterceptor.refresh();
    +     * }
    + */ @Override public void refresh() { - FeignClientProperties properties = this.beanFactory.getBean(FeignClientProperties.class); + FeignClientProperties properties = getOrInstantiate(FeignClientProperties.class); Set> interceptors = new HashSet<>(); //headers Map> headers = new HashMap<>(); Map> params = new HashMap<>(); - if (properties != null) { - FeignClientProperties.FeignClientConfiguration defaultConfiguration = properties.getConfig().get(properties.getDefaultConfig()); - FeignClientProperties.FeignClientConfiguration current = properties.getConfig().get(contextId); - if (defaultConfiguration != null && defaultConfiguration.getRequestInterceptors() != null) - interceptors.addAll(defaultConfiguration.getRequestInterceptors()); - if (current != null && current.getRequestInterceptors() != null) - interceptors.addAll(current.getRequestInterceptors()); - - if (defaultConfiguration != null && defaultConfiguration.getDefaultRequestHeaders() != null) - headers.putAll(defaultConfiguration.getDefaultRequestHeaders()); - - if (current != null && current.getDefaultRequestHeaders() != null) { - current.getDefaultRequestHeaders().forEach(headers::putIfAbsent); - } - if (defaultConfiguration != null && defaultConfiguration.getDefaultQueryParameters() != null) - params.putAll(defaultConfiguration.getDefaultRequestHeaders()); + Map config = properties.getConfig(); + FeignClientConfiguration defaultConfiguration = config.get(properties.getDefaultConfig()); + FeignClientConfiguration currentConfiguration = config.get(contextId); - if (current != null && current.getDefaultQueryParameters() != null) { - current.getDefaultQueryParameters().forEach(params::putIfAbsent); - } + addAll(defaultConfiguration::getRequestInterceptors, interceptors); + addAll(currentConfiguration::getRequestInterceptors, interceptors); - } + putIfAbsent(defaultConfiguration::getDefaultRequestHeaders, headers); + putIfAbsent(currentConfiguration::getDefaultRequestHeaders, headers); + + putIfAbsent(defaultConfiguration::getDefaultQueryParameters, params); + putIfAbsent(currentConfiguration::getDefaultQueryParameters, params); synchronized (this.set) { this.set.clear(); - for (Class interceptorClass : interceptors) + for (Class interceptorClass : interceptors) { set.add(getInterceptorOrInstantiate(interceptorClass)); + } - if (!CollectionUtils.isEmpty(headers)) + if (isNotEmpty(headers)) set.add(requestTemplate -> { Map> requestHeader = requestTemplate.headers(); headers.keySet().forEach(key -> { @@ -102,7 +155,7 @@ public void refresh() { }); }); - if (!CollectionUtils.isEmpty(params)) + if (isNotEmpty(params)) set.add(requestTemplate -> { Map> requestQueries = requestTemplate.queries(); params.keySet().forEach(key -> { @@ -112,8 +165,66 @@ public void refresh() { }); }); } + } + /** + * Adds all elements from the source collection (obtained via the supplier) into the + * target collection if the source is not empty. + * + *

    Example Usage: + *

    {@code
    +     * Collection target = new ArrayList<>();
    +     * addAll(() -> List.of("a", "b"), target);
    +     * }
    + * + * @param the element type + * @param sourceSupplier the supplier providing the source collection + * @param target the target collection to add elements to + */ + static void addAll(Supplier> sourceSupplier, Collection target) { + Collection source = sourceSupplier.get(); + if (isNotEmpty(source)) { + source.forEach(target::add); + } + } + /** + * Retrieves a bean of the given type from the {@link BeanFactory}, falling back to + * instantiation via the default constructor if the bean is not available. + * + *

    Example Usage: + *

    {@code
    +     * FeignClientProperties properties = getOrInstantiate(FeignClientProperties.class);
    +     * }
    + * + * @param the type of the bean + * @param clazz the class of the bean to retrieve or instantiate + * @return an instance of the requested type + */ + T getOrInstantiate(Class clazz) { + ObjectProvider beanProvider = this.beanFactory.getBeanProvider(clazz); + return beanProvider.getIfAvailable(() -> instantiateClass(clazz)); + } + /** + * Puts all entries from the source map (obtained via the supplier) into the target + * map if the source is not empty, without overwriting existing keys. + * + *

    Example Usage: + *

    {@code
    +     * Map target = new HashMap<>();
    +     * putIfAbsent(() -> Map.of("key", "value"), target);
    +     * }
    + * + * @param the key type + * @param the value type + * @param sourceSupplier the supplier providing the source map + * @param target the target map to add entries to + */ + static void putIfAbsent(Supplier> sourceSupplier, Map target) { + Map source = sourceSupplier.get(); + if (isNotEmpty(source)) { + source.forEach(target::putIfAbsent); + } } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedContract.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedContract.java index a49185c5..38ff43c8 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedContract.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedContract.java @@ -2,37 +2,69 @@ import feign.Contract; import feign.MethodMetadata; +import org.springframework.cloud.context.named.NamedContextFactory; import org.springframework.cloud.openfeign.FeignClientProperties; -import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; +import org.springframework.cloud.openfeign.FeignClientSpecification; import java.util.List; /** * @author 韩超 + * @author Mercy * @since 0.0.1 */ public class DecoratedContract extends DecoratedFeignComponent implements Contract { - public DecoratedContract(String contextId, FeignContext feignContext, FeignClientProperties clientProperties, Contract delegate) { - super(contextId, feignContext, clientProperties, delegate); + /** + * Constructs a {@link DecoratedContract} wrapping the given {@link Contract} delegate. + * + *

    Example Usage: + *

    {@code
    +     * DecoratedContract contract = new DecoratedContract(
    +     *     "my-client", contextFactory, clientProperties, new Contract.Default());
    +     * }
    + * + * @param contextId the Feign client context ID + * @param contextFactory the {@link NamedContextFactory} for resolving per-client contexts + * @param clientProperties the {@link FeignClientProperties} for configuration lookup + * @param delegate the original {@link Contract} to delegate to + */ + public DecoratedContract(String contextId, NamedContextFactory contextFactory, FeignClientProperties clientProperties, Contract delegate) { + super(contextId, contextFactory, clientProperties, delegate); } + /** + * Returns the configured {@link Contract} class from {@link FeignClientConfiguration}, + * falling back to {@link Contract} if not configured. + * + *

    Example Usage: + *

    {@code
    +     * Class type = decoratedContract.componentType();
    +     * }
    + * + * @return the {@link Contract} component type class + */ @Override - protected Class componentType() { - Class contractClass = null; - if (getDefaultConfiguration() != null && getDefaultConfiguration().getContract() != null) - contractClass = getDefaultConfiguration().getContract(); - - if (getCurrentConfiguration() != null && getCurrentConfiguration().getContract() != null) - contractClass = getCurrentConfiguration().getContract(); - - if (contractClass != null) - return contractClass; - return Contract.class; + protected Class componentType() { + Class contractClass = get(FeignClientConfiguration::getContract); + return contractClass == null ? Contract.class : contractClass; } + /** + * Parses and validates metadata for the given target type by delegating to the + * underlying {@link Contract}. + * + *

    Example Usage: + *

    {@code
    +     * List metadata = decoratedContract.parseAndValidateMetadata(MyFeignClient.class);
    +     * }
    + * + * @param targetType the Feign client interface class to parse + * @return the list of parsed {@link MethodMetadata} + */ @Override public List parseAndValidateMetadata(Class targetType) { return delegate().parseAndValidateMetadata(targetType); } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedDecoder.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedDecoder.java index 0734aaa4..21fb832e 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedDecoder.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedDecoder.java @@ -4,36 +4,72 @@ import feign.Response; import feign.codec.DecodeException; import feign.codec.Decoder; +import org.springframework.cloud.context.named.NamedContextFactory; import org.springframework.cloud.openfeign.FeignClientProperties; -import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; +import org.springframework.cloud.openfeign.FeignClientSpecification; import java.io.IOException; import java.lang.reflect.Type; /** * @author 韩超 + * @author Mercy * @since 0.0.1 */ public class DecoratedDecoder extends DecoratedFeignComponent implements Decoder { - public DecoratedDecoder(String contextId, FeignContext feignContext, FeignClientProperties clientProperties, Decoder delegate) { - super(contextId, feignContext, clientProperties, delegate); + /** + * Constructs a {@link DecoratedDecoder} wrapping the given {@link Decoder} delegate. + * + *

    Example Usage: + *

    {@code
    +     * DecoratedDecoder decoder = new DecoratedDecoder(
    +     *     "my-client", contextFactory, clientProperties, new Decoder.Default());
    +     * }
    + * + * @param contextId the Feign client context ID + * @param contextFactory the {@link NamedContextFactory} for resolving per-client contexts + * @param clientProperties the {@link FeignClientProperties} for configuration lookup + * @param delegate the original {@link Decoder} to delegate to + */ + public DecoratedDecoder(String contextId, NamedContextFactory contextFactory, FeignClientProperties clientProperties, Decoder delegate) { + super(contextId, contextFactory, clientProperties, delegate); } + /** + * Returns the configured {@link Decoder} class from {@link FeignClientConfiguration}, + * falling back to {@link Decoder} if not configured. + * + *

    Example Usage: + *

    {@code
    +     * Class type = decoratedDecoder.componentType();
    +     * }
    + * + * @return the {@link Decoder} component type class + */ @Override - protected Class componentType() { - Class decoderClass = null; - if (getDefaultConfiguration() != null && getDefaultConfiguration().getDecoder() != null) - decoderClass = getDefaultConfiguration().getDecoder(); - - if (getCurrentConfiguration() != null && getCurrentConfiguration().getDecoder() != null) - decoderClass = getCurrentConfiguration().getDecoder(); - - if (decoderClass != null) - return decoderClass; - return Decoder.class; + protected Class componentType() { + Class decoderClass = get(FeignClientConfiguration::getDecoder); + return decoderClass == null ? Decoder.class : decoderClass; } + /** + * Decodes a Feign {@link Response} into an object of the given type by delegating + * to the underlying {@link Decoder}. + * + *

    Example Usage: + *

    {@code
    +     * Object result = decoratedDecoder.decode(response, String.class);
    +     * }
    + * + * @param response the Feign {@link Response} to decode + * @param type the target type to decode into + * @return the decoded object + * @throws IOException if an I/O error occurs + * @throws DecodeException if decoding fails + * @throws FeignException if a Feign-specific error occurs + */ @Override public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException { return delegate().decode(response, type); diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedEncoder.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedEncoder.java index dfd37050..2ede1a4f 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedEncoder.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedEncoder.java @@ -3,37 +3,72 @@ import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; +import org.springframework.cloud.context.named.NamedContextFactory; import org.springframework.cloud.openfeign.FeignClientProperties; -import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; +import org.springframework.cloud.openfeign.FeignClientSpecification; import java.lang.reflect.Type; /** * @author 韩超 + * @author Mercy * @since 0.0.1 */ public class DecoratedEncoder extends DecoratedFeignComponent implements Encoder { - public DecoratedEncoder(String contextId, FeignContext feignContext, FeignClientProperties clientProperties, Encoder delegate) { - super(contextId, feignContext, clientProperties, delegate); + /** + * Constructs a {@link DecoratedEncoder} wrapping the given {@link Encoder} delegate. + * + *

    Example Usage: + *

    {@code
    +     * DecoratedEncoder encoder = new DecoratedEncoder(
    +     *     "my-client", contextFactory, clientProperties, new Encoder.Default());
    +     * }
    + * + * @param contextId the Feign client context ID + * @param contextFactory the {@link NamedContextFactory} for resolving per-client contexts + * @param clientProperties the {@link FeignClientProperties} for configuration lookup + * @param delegate the original {@link Encoder} to delegate to + */ + public DecoratedEncoder(String contextId, NamedContextFactory contextFactory, + FeignClientProperties clientProperties, Encoder delegate) { + super(contextId, contextFactory, clientProperties, delegate); } + /** + * Returns the configured {@link Encoder} class from {@link FeignClientConfiguration}, + * falling back to {@link Encoder} if not configured. + * + *

    Example Usage: + *

    {@code
    +     * Class type = decoratedEncoder.componentType();
    +     * }
    + * + * @return the {@link Encoder} component type class + */ @Override - protected Class componentType() { - Class encoderClass = null; - if (getDefaultConfiguration() != null && getDefaultConfiguration().getEncoder() != null) - encoderClass = getDefaultConfiguration().getEncoder(); - - if (getCurrentConfiguration() != null && getCurrentConfiguration().getEncoder() != null) - encoderClass = getCurrentConfiguration().getEncoder(); - - if (encoderClass != null) - return encoderClass; - return Encoder.class; + protected Class componentType() { + Class encoderClass = get(FeignClientConfiguration::getEncoder); + return encoderClass == null ? Encoder.class : encoderClass; } + /** + * Encodes the given object into the {@link RequestTemplate} by delegating to the + * underlying {@link Encoder}. + * + *

    Example Usage: + *

    {@code
    +     * decoratedEncoder.encode(myObject, MyObject.class, requestTemplate);
    +     * }
    + * + * @param object the object to encode + * @param bodyType the body type + * @param template the {@link RequestTemplate} to encode into + * @throws EncodeException if encoding fails + */ @Override public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { delegate().encode(object, bodyType, template); } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedErrorDecoder.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedErrorDecoder.java index 5173bbb7..7a7c55d3 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedErrorDecoder.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedErrorDecoder.java @@ -2,35 +2,67 @@ import feign.Response; import feign.codec.ErrorDecoder; +import org.springframework.cloud.context.named.NamedContextFactory; import org.springframework.cloud.openfeign.FeignClientProperties; -import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; +import org.springframework.cloud.openfeign.FeignClientSpecification; /** * @author 韩超 + * @author Mercy * @since 0.0.1 */ public class DecoratedErrorDecoder extends DecoratedFeignComponent implements ErrorDecoder { - public DecoratedErrorDecoder(String contextId, FeignContext feignContext, FeignClientProperties clientProperties, ErrorDecoder delegate) { - super(contextId, feignContext, clientProperties, delegate); + /** + * Constructs a {@link DecoratedErrorDecoder} wrapping the given {@link ErrorDecoder} delegate. + * + *

    Example Usage: + *

    {@code
    +     * DecoratedErrorDecoder decoder = new DecoratedErrorDecoder(
    +     *     "my-client", contextFactory, clientProperties, new ErrorDecoder.Default());
    +     * }
    + * + * @param contextId the Feign client context ID + * @param contextFactory the {@link NamedContextFactory} for resolving per-client contexts + * @param clientProperties the {@link FeignClientProperties} for configuration lookup + * @param delegate the original {@link ErrorDecoder} to delegate to + */ + public DecoratedErrorDecoder(String contextId, NamedContextFactory contextFactory, FeignClientProperties clientProperties, ErrorDecoder delegate) { + super(contextId, contextFactory, clientProperties, delegate); } + /** + * Returns the configured {@link ErrorDecoder} class from {@link FeignClientConfiguration}, + * falling back to {@link ErrorDecoder.Default} if not configured. + * + *

    Example Usage: + *

    {@code
    +     * Class type = decoratedErrorDecoder.componentType();
    +     * }
    + * + * @return the {@link ErrorDecoder} component type class + */ @Override - protected Class componentType() { - Class errorDecoderClass = null; - if (getDefaultConfiguration() != null && getDefaultConfiguration().getErrorDecoder() != null) - errorDecoderClass = getDefaultConfiguration().getErrorDecoder(); - - if (getCurrentConfiguration() != null && getCurrentConfiguration().getErrorDecoder() != null) - errorDecoderClass = getCurrentConfiguration().getErrorDecoder(); - - if (errorDecoderClass != null) - return errorDecoderClass; - return ErrorDecoder.class; + protected Class componentType() { + Class errorDecoderClass = get(FeignClientConfiguration::getErrorDecoder); + return errorDecoderClass == null ? Default.class : errorDecoderClass; } + /** + * Decodes an error response by delegating to the underlying {@link ErrorDecoder}. + * + *

    Example Usage: + *

    {@code
    +     * Exception ex = decoratedErrorDecoder.decode("MyClient#myMethod()", response);
    +     * }
    + * + * @param methodKey the Feign method key (e.g. {@code "MyClient#myMethod()"}) + * @param response the Feign {@link Response} containing the error + * @return the decoded {@link Exception} + */ @Override public Exception decode(String methodKey, Response response) { return delegate().decode(methodKey, response); } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedFeignComponent.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedFeignComponent.java index 79884de3..7c32bc92 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedFeignComponent.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedFeignComponent.java @@ -1,124 +1,265 @@ package io.microsphere.spring.cloud.openfeign.components; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeanInstantiationException; -import org.springframework.beans.BeanUtils; +import io.microsphere.logging.Logger; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.context.named.NamedContextFactory; import org.springframework.cloud.openfeign.FeignClientProperties; -import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; +import org.springframework.cloud.openfeign.FeignClientSpecification; import org.springframework.lang.NonNull; import java.lang.reflect.Constructor; -import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; + +import static io.microsphere.logging.LoggerFactory.getLogger; +import static io.microsphere.reflect.ConstructorUtils.findConstructor; +import static org.springframework.beans.BeanUtils.instantiateClass; /** * @author 韩超 + * @author Mercy * @since 0.0.1 */ public abstract class DecoratedFeignComponent implements Refreshable { - private final Logger log = LoggerFactory.getLogger(getClass()); + protected final Logger logger = getLogger(getClass()); + + private final NamedContextFactory contextFactory; - private final FeignContext feignContext; private final String contextId; private final FeignClientProperties clientProperties; - protected volatile T delegate; - private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - - private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); - private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); - - public DecoratedFeignComponent(String contextId, FeignContext feignContext, FeignClientProperties clientProperties, T delegate) { + /** + * Constructs a {@link DecoratedFeignComponent} wrapping the given delegate. + * + *

    Example Usage: + *

    {@code
    +     * // Typically invoked via a subclass constructor
    +     * super(contextId, contextFactory, clientProperties, delegate);
    +     * }
    + * + * @param contextId the Feign client context ID + * @param contextFactory the {@link NamedContextFactory} for resolving per-client contexts + * @param clientProperties the {@link FeignClientProperties} for configuration lookup + * @param delegate the original component to delegate to + */ + public DecoratedFeignComponent(String contextId, NamedContextFactory contextFactory, FeignClientProperties clientProperties, T delegate) { this.contextId = contextId; - this.feignContext = feignContext; + this.contextFactory = contextFactory; this.clientProperties = clientProperties; this.delegate = delegate; } + /** + * Returns the current delegate instance, lazily loading it from the context factory + * if it was previously cleared by a {@link #refresh()} call. + * + *

    Example Usage: + *

    {@code
    +     * T component = decoratedFeignComponent.delegate();
    +     * }
    + * + * @return the current delegate instance + */ public T delegate() { - readLock.lock(); + T delegate = this.delegate; if (delegate == null) { - log.trace("the component {} - Creating delegate instance for contextId: {}", componentType().getSimpleName(), contextId); - readLock.unlock(); - return loadInstance(); + delegate = loadInstance(); + if (logger.isTraceEnabled()) { + logger.trace("the component[{}] - Creating delegate instance[{}] for contextId: '{}'", componentType(), delegate, contextId); + } + this.delegate = delegate; } - - readLock.unlock(); - return this.delegate; + return delegate; } + /** + * Loads a component instance of the given type from the {@link NamedContextFactory}, + * falling back to direct instantiation if the bean is not available. + * + *

    Example Usage: + *

    {@code
    +     * Decoder decoder = decoratedFeignComponent.loadInstanceFromContextFactory("my-client", Decoder.class);
    +     * }
    + * + * @param the component type + * @param contextId the Feign client context ID + * @param componentType the class of the component to load + * @return the loaded component instance + */ @NonNull - public FeignContext getFeignContext() { - return this.feignContext; + public T loadInstanceFromContextFactory(String contextId, Class componentType) { + ObjectProvider beanProvider = this.contextFactory.getProvider(contextId, componentType); + return beanProvider.getIfAvailable(() -> instantiateClass(componentType)); } + /** + * Returns the Feign client context ID associated with this decorated component. + * + *

    Example Usage: + *

    {@code
    +     * String id = decoratedFeignComponent.contextId();
    +     * }
    + * + * @return the context ID string + */ @NonNull public String contextId() { return this.contextId; } + /** + * Refreshes this component by clearing the delegate, causing the next call to + * {@link #delegate()} to reload the instance from the context factory. + * + *

    Example Usage: + *

    {@code
    +     * decoratedFeignComponent.refresh();
    +     * }
    + */ public void refresh() { - writeLock.lock(); - log.debug("the component {} - Refreshing delegate instance for contextId: {}", componentType().getSimpleName(), contextId); + if (logger.isTraceEnabled()) { + logger.trace("the component[{}] - Refreshing delegate instance[{}] for contextId : '{}'", componentType(), this.delegate, contextId); + } this.delegate = null; - writeLock.unlock(); } - protected abstract Class componentType(); + /** + * Returns the Feign component type class used to resolve the delegate implementation. + * Subclasses must implement this to return the appropriate configuration class. + * + *

    Example Usage: + *

    {@code
    +     * Class type = decoratedFeignComponent.componentType();
    +     * }
    + * + * @return the component type class + */ + protected abstract Class componentType(); - public FeignClientProperties.FeignClientConfiguration getDefaultConfiguration() { + /** + * Returns the default {@link FeignClientConfiguration} as defined by the + * {@link FeignClientProperties#getDefaultConfig()} key. + * + *

    Example Usage: + *

    {@code
    +     * FeignClientConfiguration defaultConfig = decoratedFeignComponent.getDefaultConfiguration();
    +     * }
    + * + * @return the default {@link FeignClientConfiguration}, or {@code null} if not present + */ + public FeignClientConfiguration getDefaultConfiguration() { return this.clientProperties.getConfig().get(this.clientProperties.getDefaultConfig()); } - public FeignClientProperties.FeignClientConfiguration getCurrentConfiguration() { + /** + * Returns the {@link FeignClientConfiguration} for the current Feign client context ID. + * + *

    Example Usage: + *

    {@code
    +     * FeignClientConfiguration currentConfig = decoratedFeignComponent.getCurrentConfiguration();
    +     * }
    + * + * @return the current {@link FeignClientConfiguration}, or {@code null} if not present + */ + public FeignClientConfiguration getCurrentConfiguration() { return this.clientProperties.getConfig().get(contextId); } + /** + * Retrieves a value from the {@link FeignClientConfiguration} using the provided function, + * checking the default configuration first and then the current context configuration. + * + *

    Example Usage: + *

    {@code
    +     * Class decoderClass = get(FeignClientConfiguration::getDecoder);
    +     * }
    + * + * @param the value type + * @param configurationFunction the function to extract a value from the configuration + * @return the extracted value, or {@code null} if not found in either configuration + */ + protected T get(Function configurationFunction) { + FeignClientConfiguration config = getDefaultConfiguration(); + T value = null; + if (config != null) { + value = configurationFunction.apply(config); + } + if (value == null) { + config = getCurrentConfiguration(); + if (config != null) { + value = configurationFunction.apply(config); + } + } + return value; + } + + /** + * Loads the delegate instance from the {@link NamedContextFactory} using the + * component type returned by {@link #componentType()}. + * + *

    Example Usage: + *

    {@code
    +     * T instance = decoratedFeignComponent.loadInstance();
    +     * }
    + * + * @return the loaded delegate instance + */ protected T loadInstance() { - Class componentType = componentType(); + Class componentType = componentType(); String contextId = contextId(); - writeLock.lock(); - try { - T component = getFeignContext().getInstance(contextId, componentType); - if (component == null) - component = BeanUtils.instantiateClass(componentType); - this.delegate = component; - return component; - } catch (Throwable ex) { - this.delegate = BeanUtils.instantiateClass(componentType); - return delegate; - } finally { - writeLock.unlock(); - } + return loadInstanceFromContextFactory(contextId, componentType); } + /** {@inheritDoc} */ @Override public int hashCode() { return delegate().hashCode(); } + /** {@inheritDoc} */ @Override public boolean equals(Object obj) { return delegate().equals(obj); } + /** {@inheritDoc} */ @Override public String toString() { return delegate().toString(); } - public static , T> W instantiate(Class decoratedClass, Class componentClass, - String contextId, FeignContext feignContext, FeignClientProperties clientProperties, T delegate) { - try { - Constructor constructor = decoratedClass.getConstructor(String.class, FeignContext.class, FeignClientProperties.class, componentClass); - return BeanUtils.instantiateClass(constructor, contextId, feignContext, clientProperties, delegate); - } catch (NoSuchMethodException noSuchMethodException) { - throw new BeanInstantiationException(decoratedClass, noSuchMethodException.getLocalizedMessage()); - } - + /** + * Factory method to instantiate a {@link DecoratedFeignComponent} subclass by locating + * the appropriate constructor via reflection. + * + *

    Example Usage: + *

    {@code
    +     * DecoratedContract contract = DecoratedFeignComponent.instantiate(
    +     *     DecoratedContract.class, Contract.class,
    +     *     "my-client", contextFactory, clientProperties, originalContract);
    +     * }
    + * + * @param the decorated component type + * @param the Feign component type + * @param decoratedClass the {@link DecoratedFeignComponent} subclass to instantiate + * @param componentClass the Feign component interface class + * @param contextId the Feign client context ID + * @param contextFactory the {@link NamedContextFactory} for context resolution + * @param clientProperties the {@link FeignClientProperties} for configuration + * @param delegate the original delegate instance + * @return a new instance of the decorated component + */ + public static , T> W instantiate(Class decoratedClass, + Class componentClass, + String contextId, + NamedContextFactory contextFactory, + FeignClientProperties clientProperties, + T delegate) { + Constructor constructor = findConstructor(decoratedClass, String.class, NamedContextFactory.class, FeignClientProperties.class, componentClass); + return instantiateClass(constructor, contextId, contextFactory, clientProperties, delegate); } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedQueryMapEncoder.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedQueryMapEncoder.java index d66f9323..37eb3d1a 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedQueryMapEncoder.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedQueryMapEncoder.java @@ -1,58 +1,113 @@ package io.microsphere.spring.cloud.openfeign.components; import feign.QueryMapEncoder; -import io.microsphere.logging.Logger; +import org.springframework.cloud.context.named.NamedContextFactory; import org.springframework.cloud.openfeign.FeignClientProperties; -import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; +import org.springframework.cloud.openfeign.FeignClientSpecification; +import org.springframework.cloud.openfeign.support.PageableSpringQueryMapEncoder; import java.lang.invoke.MethodHandle; import java.util.Map; import static io.microsphere.invoke.MethodHandleUtils.findVirtual; -import static io.microsphere.logging.LoggerFactory.getLogger; +import static io.microsphere.invoke.MethodHandleUtils.handleInvokeExactFailure; +import static io.microsphere.invoke.MethodHandlesLookupUtils.NOT_FOUND_METHOD_HANDLE; /** * @author 韩超 + * @author Mercy * @since 0.0.1 */ public class DecoratedQueryMapEncoder extends DecoratedFeignComponent implements QueryMapEncoder { - private static final Logger logger = getLogger(DecoratedQueryMapEncoder.class); - private static final String getQueryMapEncoderMethodName = "getQueryMapEncoder"; - private static final MethodHandle getQueryMapEncoderMethodHandle = findVirtual(FeignClientProperties.FeignClientConfiguration.class, getQueryMapEncoderMethodName); + static final MethodHandle getQueryMapEncoderMethodHandle = findVirtual(FeignClientConfiguration.class, getQueryMapEncoderMethodName); - public DecoratedQueryMapEncoder(String contextId, FeignContext feignContext, FeignClientProperties clientProperties, QueryMapEncoder delegate) { - super(contextId, feignContext, clientProperties, delegate); + /** + * Constructs a {@link DecoratedQueryMapEncoder} wrapping the given {@link QueryMapEncoder} delegate. + * + *

    Example Usage: + *

    {@code
    +     * DecoratedQueryMapEncoder encoder = new DecoratedQueryMapEncoder(
    +     *     "my-client", contextFactory, clientProperties, new QueryMapEncoder.Default());
    +     * }
    + * + * @param contextId the Feign client context ID + * @param contextFactory the {@link NamedContextFactory} for resolving per-client contexts + * @param clientProperties the {@link FeignClientProperties} for configuration lookup + * @param delegate the original {@link QueryMapEncoder} to delegate to + */ + public DecoratedQueryMapEncoder(String contextId, NamedContextFactory contextFactory, FeignClientProperties clientProperties, QueryMapEncoder delegate) { + super(contextId, contextFactory, clientProperties, delegate); } + /** + * Returns the configured {@link QueryMapEncoder} class from {@link FeignClientConfiguration}, + * falling back to {@link PageableSpringQueryMapEncoder} if not configured. + * + *

    Example Usage: + *

    {@code
    +     * Class type = decoratedQueryMapEncoder.componentType();
    +     * }
    + * + * @return the {@link QueryMapEncoder} component type class + */ @Override - protected Class componentType() { + protected Class componentType() { Class queryMapEncoderClass = getQueryMapEncoder(getCurrentConfiguration()); if (queryMapEncoderClass == null) { queryMapEncoderClass = getQueryMapEncoder(getDefaultConfiguration()); } - return queryMapEncoderClass == null ? QueryMapEncoder.class : queryMapEncoderClass; + return queryMapEncoderClass == null ? PageableSpringQueryMapEncoder.class : queryMapEncoderClass; + } + + private Class getQueryMapEncoder(FeignClientConfiguration feignClientConfiguration) { + return getQueryMapEncoder(getQueryMapEncoderMethodHandle, feignClientConfiguration); } - private Class getQueryMapEncoder(FeignClientProperties.FeignClientConfiguration feignClientConfiguration) { - if (feignClientConfiguration == null || getQueryMapEncoderMethodHandle == null) { + /** + * Retrieves the {@link QueryMapEncoder} class from a {@link FeignClientConfiguration} + * using a {@link MethodHandle} for compatibility across Spring Cloud versions. + * + *

    Example Usage: + *

    {@code
    +     * Class encoderClass = DecoratedQueryMapEncoder.getQueryMapEncoder(
    +     *     getQueryMapEncoderMethodHandle, feignClientConfiguration);
    +     * }
    + * + * @param methodHandle the {@link MethodHandle} to invoke {@code getQueryMapEncoder} + * @param feignClientConfiguration the configuration to read from + * @return the configured {@link QueryMapEncoder} class, or {@code null} if unavailable + */ + static Class getQueryMapEncoder(MethodHandle methodHandle, FeignClientConfiguration feignClientConfiguration) { + if (methodHandle == NOT_FOUND_METHOD_HANDLE) { return null; } Class queryMapEncoderClass = null; try { queryMapEncoderClass = (Class) getQueryMapEncoderMethodHandle.invokeExact(feignClientConfiguration); } catch (Throwable e) { - if (logger.isWarnEnabled()) { - logger.warn("FeignClientProperties.FeignClientConfiguration#getQueryMapEncoder() method can't be invoked , instance : {}", feignClientConfiguration); - } + handleInvokeExactFailure(e, getQueryMapEncoderMethodHandle, feignClientConfiguration); } return queryMapEncoderClass; } + /** + * Encodes the given object into a query parameter map by delegating to the + * underlying {@link QueryMapEncoder}. + * + *

    Example Usage: + *

    {@code
    +     * Map queryParams = decoratedQueryMapEncoder.encode(myQueryObject);
    +     * }
    + * + * @param object the object to encode as query parameters + * @return a map of query parameter names to values + */ @Override public Map encode(Object object) { return delegate().encode(object); } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedRetryer.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedRetryer.java index 6f2d743b..ae028a36 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedRetryer.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/DecoratedRetryer.java @@ -2,42 +2,102 @@ import feign.RetryableException; import feign.Retryer; +import org.springframework.cloud.context.named.NamedContextFactory; import org.springframework.cloud.openfeign.FeignClientProperties; -import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; +import org.springframework.cloud.openfeign.FeignClientSpecification; /** * @author 韩超 + * @author Mercy * @since 0.0.1 */ -public class DecoratedRetryer extends DecoratedFeignComponent implements Retryer { +public class DecoratedRetryer extends DecoratedFeignComponent implements Retryer { - public DecoratedRetryer(String contextId, FeignContext feignContext, FeignClientProperties clientProperties, Retryer delegate) { - super(contextId, feignContext, clientProperties, delegate); + /** + * Constructs a {@link DecoratedRetryer} wrapping the given {@link Retryer} delegate. + * + *

    Example Usage: + *

    {@code
    +     * DecoratedRetryer retryer = new DecoratedRetryer(
    +     *     "my-client", contextFactory, clientProperties, new Retryer.Default());
    +     * }
    + * + * @param contextId the Feign client context ID + * @param contextFactory the {@link NamedContextFactory} for resolving per-client contexts + * @param clientProperties the {@link FeignClientProperties} for configuration lookup + * @param delegate the original {@link Retryer} to delegate to + */ + public DecoratedRetryer(String contextId, NamedContextFactory contextFactory, FeignClientProperties clientProperties, Retryer delegate) { + super(contextId, contextFactory, clientProperties, delegate); } + /** + * Returns the configured {@link Retryer} class from {@link FeignClientConfiguration}, + * falling back to {@link Retryer} if not configured. + * + *

    Example Usage: + *

    {@code
    +     * Class type = decoratedRetryer.componentType();
    +     * }
    + * + * @return the {@link Retryer} component type class + */ @Override - protected Class componentType() { - Class retryerClass = null; - if (getDefaultConfiguration() != null && getDefaultConfiguration().getRetryer() != null) - retryerClass = getDefaultConfiguration().getRetryer(); - - if (getCurrentConfiguration() != null && getCurrentConfiguration().getRetryer() != null) - retryerClass = getCurrentConfiguration().getRetryer(); - - if (retryerClass != null) - return retryerClass; - return Retryer.class; + protected Class componentType() { + Class retryerClass = get(FeignClientConfiguration::getRetryer); + return retryerClass == null ? Retryer.class : retryerClass; } + /** + * Continues or propagates the retry by delegating to the underlying {@link Retryer}. + * + *

    Example Usage: + *

    {@code
    +     * try {
    +     *     // Feign call
    +     * } catch (RetryableException e) {
    +     *     decoratedRetryer.continueOrPropagate(e);
    +     * }
    +     * }
    + * + * @param e the {@link RetryableException} to evaluate for retry + */ @Override public void continueOrPropagate(RetryableException e) { - Retryer retryer = delegate(); - if (retryer != null) + continueOrPropagate(delegate(), e); + } + + /** + * Delegates the continue-or-propagate decision to the given {@link Retryer} if it + * is not {@code null}. + * + *

    Example Usage: + *

    {@code
    +     * DecoratedRetryer.continueOrPropagate(retryerInstance, retryableException);
    +     * }
    + * + * @param retryer the {@link Retryer} to delegate to, may be {@code null} + * @param e the {@link RetryableException} to evaluate + */ + static void continueOrPropagate(Retryer retryer, RetryableException e) { + if (retryer != null) { retryer.continueOrPropagate(e); + } } + /** + * Returns a clone of the delegate {@link Retryer}. + * + *

    Example Usage: + *

    {@code
    +     * Retryer cloned = decoratedRetryer.clone();
    +     * }
    + * + * @return a cloned {@link Retryer} instance + */ @Override public Retryer clone() { return delegate().clone(); } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/NoOpRequestInterceptor.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/NoOpRequestInterceptor.java index c6e19987..42504bd5 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/NoOpRequestInterceptor.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/NoOpRequestInterceptor.java @@ -5,16 +5,36 @@ /** * @author 韩超 + * @author Mercy * @since 0.0.1 */ public class NoOpRequestInterceptor implements RequestInterceptor { + /** + * A no-operation {@link RequestInterceptor} that does nothing when applied. + * Used as a placeholder to ensure at least one interceptor is registered. + * + *

    Example Usage: + *

    {@code
    +     * RequestInterceptor noOp = NoOpRequestInterceptor.INSTANCE;
    +     * noOp.apply(requestTemplate); // does nothing
    +     * }
    + */ public static final NoOpRequestInterceptor INSTANCE = new NoOpRequestInterceptor(); - private NoOpRequestInterceptor() {}; - + /** + * Applies this interceptor to the given {@link RequestTemplate}. This implementation + * intentionally performs no operation. + * + *

    Example Usage: + *

    {@code
    +     * NoOpRequestInterceptor.INSTANCE.apply(requestTemplate);
    +     * }
    + * + * @param template the {@link RequestTemplate} (ignored) + */ @Override public void apply(RequestTemplate template) { //no op } -} +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/Refreshable.java b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/Refreshable.java index b4653fe5..482f5353 100644 --- a/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/Refreshable.java +++ b/microsphere-spring-cloud-openfeign/src/main/java/io/microsphere/spring/cloud/openfeign/components/Refreshable.java @@ -2,10 +2,14 @@ /** * @author 韩超 + * @author Mercy * @since 0.0.1 */ @FunctionalInterface public interface Refreshable { - void refresh(); -} + /** + * Refresh + */ + void refresh(); +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/main/resources/META-INF/spring.factories b/microsphere-spring-cloud-openfeign/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports similarity index 58% rename from microsphere-spring-cloud-openfeign/src/main/resources/META-INF/spring.factories rename to microsphere-spring-cloud-openfeign/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 896056cc..6284a91b 100644 --- a/microsphere-spring-cloud-openfeign/src/main/resources/META-INF/spring.factories +++ b/microsphere-spring-cloud-openfeign/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,2 +1 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ io.microsphere.spring.cloud.openfeign.autoconfigure.FeignClientAutoRefreshAutoConfiguration \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/BaseClient.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/BaseClient.java index 3a4edcf0..47de7f4d 100644 --- a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/BaseClient.java +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/BaseClient.java @@ -9,7 +9,7 @@ * @author 韩超 * @since 0.0.1 */ -@FeignClient(contextId = "my-client", name = "my-client") +@FeignClient(contextId = "my-client", name = "my-client", configuration = {MockCapability.class}) public interface BaseClient { @GetMapping("echo") diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/BaseTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/BaseTest.java index 7197e845..6ad69f3c 100644 --- a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/BaseTest.java +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/BaseTest.java @@ -1,13 +1,9 @@ package io.microsphere.spring.cloud.openfeign; +import io.microsphere.logging.Logger; import io.microsphere.spring.cloud.openfeign.autoconfigure.EnableFeignAutoRefresh; import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration; import org.springframework.cloud.context.environment.EnvironmentChangeEvent; import org.springframework.cloud.endpoint.event.RefreshEvent; import org.springframework.cloud.openfeign.EnableFeignClients; @@ -24,24 +20,29 @@ import java.util.Map; import java.util.Set; +import static io.microsphere.logging.LoggerFactory.getLogger; + /** * @author 韩超 * @since 0.0.1 */ @TestPropertySource(properties = { "spring.main.allow-bean-definition-overriding=true", - "feign.client.config.default.encoder=io.microsphere.spring.cloud.openfeign.encoder.AEncoder", - "feign.client.config.default.request-interceptors[0]=io.microsphere.spring.cloud.openfeign.requestInterceptor.ARequestInterceptor", - "feign.client.config.default.default-request-headers.app=my-app", - "feign.client.config.default.default-query-parameters.sign=my-sign", + "spring.cloud.openfeign.client.config.default.encoder=io.microsphere.spring.cloud.openfeign.encoder.AEncoder", + "spring.cloud.openfeign.client.config.default.error-decoder=io.microsphere.spring.cloud.openfeign.errordecoder.AErrorDecoder", + "spring.cloud.openfeign.client.config.default.query-map-encoder=io.microsphere.spring.cloud.openfeign.querymapencoder.AQueryMapEncoder", + "spring.cloud.openfeign.client.config.default.retryer=io.microsphere.spring.cloud.openfeign.retryer.ARetry", + "spring.cloud.openfeign.client.config.default.decoder=io.microsphere.spring.cloud.openfeign.decoder.ADecoder", + "spring.cloud.openfeign.client.config.default.request-interceptors[0]=io.microsphere.spring.cloud.openfeign.requestInterceptor.ARequestInterceptor", + "spring.cloud.openfeign.client.config.default.default-request-headers.app=my-app", + "spring.cloud.openfeign.client.config.default.default-query-parameters.sign=my-sign", }) @ComponentScan(basePackages = "io.microsphere.spring.cloud.openfeign") @EnableFeignClients(clients = BaseClient.class) @EnableFeignAutoRefresh -@AutoConfigureAfter(ConfigurationPropertiesRebinderAutoConfiguration.class) public abstract class BaseTest { - private static final Logger log = LoggerFactory.getLogger(BaseTest.class); + private static final Logger log = getLogger(BaseTest.class); @Autowired private ApplicationEventPublisher publisher; @Autowired @@ -50,7 +51,9 @@ public abstract class BaseTest { private BaseClient client; protected abstract String afterTestComponentConfigKey(); + protected abstract Class beforeTestComponentClass(); protected abstract Class afterTestComponent(); + protected abstract FeignComponentAssert loadFeignComponentAssert(); public void replaceConfig() { final String key = afterTestComponentConfigKey(); @@ -70,21 +73,22 @@ public void replaceConfig() { } @Test - public void testInternal() { + protected void testInternal() { + ObservableFeignInvocationHandler.componentAssert = loadFeignComponentAssert(); + + ObservableFeignInvocationHandler.expectComponentClass = beforeTestComponentClass(); try { this.client.echo("hello", "1.0"); - } catch (Exception ignored) { - } replaceConfig(); + + ObservableFeignInvocationHandler.expectComponentClass = afterTestComponent(); try { this.client.echo("world", "1.0"); } catch (Exception ignored) { } - - } protected void triggerRefreshEvent() { diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/FeignComponentAssert.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/FeignComponentAssert.java new file mode 100644 index 00000000..57f56d86 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/FeignComponentAssert.java @@ -0,0 +1,19 @@ +package io.microsphere.spring.cloud.openfeign; + +import feign.ResponseHandler; + +/** + * @author 韩超 + * @since 0.0.1 + */ +public abstract class FeignComponentAssert { + + + protected abstract T loadCurrentComponent(Object configuration, ResponseHandler responseHandler) throws Exception; + + public boolean expect(Object configuration, ResponseHandler responseHandler, Class expectedClass) throws Exception { + T component = loadCurrentComponent(configuration, responseHandler); + return expectedClass.equals(component.getClass()); + } + +} diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/MockCapability.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/MockCapability.java new file mode 100644 index 00000000..53562b77 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/MockCapability.java @@ -0,0 +1,18 @@ +package io.microsphere.spring.cloud.openfeign; + +import feign.Capability; +import feign.InvocationHandlerFactory; + +/** + * @author 韩超 + * @since 0.0.1 + */ +public class MockCapability implements Capability { + + @Override + public InvocationHandlerFactory enrich(InvocationHandlerFactory invocationHandlerFactory) { + return ObservableFeignInvocationHandler::new; + } + + +} diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/ObservableFeignInvocationHandler.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/ObservableFeignInvocationHandler.java new file mode 100644 index 00000000..30a9e51e --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/ObservableFeignInvocationHandler.java @@ -0,0 +1,101 @@ +package io.microsphere.spring.cloud.openfeign; + +import feign.InvocationHandlerFactory; +import feign.ResponseHandler; +import feign.Target; +import io.microsphere.logging.Logger; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Map; + +import static feign.Util.checkNotNull; +import static io.microsphere.logging.LoggerFactory.getLogger; + +/** + * @author 韩超 + * @since 0.0.1 + */ +public class ObservableFeignInvocationHandler implements InvocationHandler { + + + private static final Logger log = getLogger(ObservableFeignInvocationHandler.class); + public static FeignComponentAssert componentAssert; + public static Class expectComponentClass; + + private final Target target; + private final Map dispatch; + + ObservableFeignInvocationHandler(Target target, Map dispatch) { + this.target = checkNotNull(target, "target"); + this.dispatch = checkNotNull(dispatch, "dispatch for %s", target); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if ("equals".equals(method.getName())) { + try { + Object otherHandler = + args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; + return equals(otherHandler); + } catch (IllegalArgumentException e) { + return false; + } + } else if ("hashCode".equals(method.getName())) { + return hashCode(); + } else if ("toString".equals(method.getName())) { + return toString(); + } else if (!dispatch.containsKey(method)) { + throw new UnsupportedOperationException( + String.format("Method \"%s\" should not be called", method.getName())); + } + + InvocationHandlerFactory.MethodHandler methodHandler = dispatch.get(method); + Object methodHandlerConfiguration = loadMethodHandlerConfiguration(methodHandler); + ResponseHandler responseHandler = loadResponseHandler(methodHandler); + Assert.isTrue(componentAssert.expect(methodHandlerConfiguration, responseHandler, expectComponentClass), "unexpected component"); + log.info("component validation is True"); + return dispatch.get(method).invoke(args); + } + + protected Object loadMethodHandlerConfiguration(InvocationHandlerFactory.MethodHandler methodHandler) throws Exception { + if (ClassUtils.isPresent("feign.MethodHandlerConfiguration", ObservableFeignInvocationHandler.class.getClassLoader())) { + Class configurationType = methodHandler.getClass(); + Field field = configurationType.getDeclaredField("methodHandlerConfiguration"); + field.setAccessible(true); + return field.get(methodHandler); + } + return methodHandler; + + } + + protected ResponseHandler loadResponseHandler(InvocationHandlerFactory.MethodHandler methodHandler) throws Exception { + Class configurationType = methodHandler.getClass(); + Field field = configurationType.getDeclaredField("responseHandler"); + field.setAccessible(true); + return (ResponseHandler) field.get(methodHandler); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ObservableFeignInvocationHandler) { + ObservableFeignInvocationHandler other = (ObservableFeignInvocationHandler) obj; + return target.equals(other.target); + } + return false; + } + + @Override + public int hashCode() { + return target.hashCode(); + } + + @Override + public String toString() { + return target.toString(); + } +} diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/autoconfigure/FeignClientSpecificationPostProcessorTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/autoconfigure/FeignClientSpecificationPostProcessorTest.java new file mode 100644 index 00000000..fc67907b --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/autoconfigure/FeignClientSpecificationPostProcessorTest.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.openfeign.autoconfigure; + + +import io.microsphere.spring.test.junit.jupiter.SpringLoggingTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.openfeign.FeignClientSpecification; + +import static io.microsphere.spring.cloud.openfeign.autoconfigure.FeignClientSpecificationPostProcessor.AUTO_REFRESH_CAPABILITY_CLASS; +import static io.microsphere.util.ArrayUtils.combine; +import static io.microsphere.util.ArrayUtils.ofArray; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +/** + * {@link FeignClientSpecificationPostProcessor} Test + * + * @author Mercy + * @see FeignClientSpecificationPostProcessor + * @since 1.0.0 + */ +@SpringLoggingTest +class FeignClientSpecificationPostProcessorTest { + + private FeignClientSpecificationPostProcessor postProcessor; + + @BeforeEach + void setUp() { + this.postProcessor = new FeignClientSpecificationPostProcessor(); + } + + @Test + void testInjectAutoRefreshCapability() { + Class[] configurationClasses = ofArray(FeignClientSpecificationPostProcessorTest.class); + FeignClientSpecification specification = new FeignClientSpecification("test", "TestClass", configurationClasses); + assertArrayEquals(configurationClasses, specification.getConfiguration()); + this.postProcessor.injectAutoRefreshCapability(specification); + assertArrayEquals(combine(AUTO_REFRESH_CAPABILITY_CLASS, configurationClasses), specification.getConfiguration()); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/autorefresh/AutoRefreshCapabilityTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/autorefresh/AutoRefreshCapabilityTest.java new file mode 100644 index 00000000..ef575be2 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/autorefresh/AutoRefreshCapabilityTest.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.openfeign.autorefresh; + + +import feign.Contract; +import feign.QueryMapEncoder; +import feign.RequestInterceptor; +import feign.Retryer; +import feign.codec.Decoder; +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.openfeign.FeignClientFactory; +import org.springframework.cloud.openfeign.FeignClientProperties; +import org.springframework.context.support.GenericApplicationContext; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * {@link AutoRefreshCapability} Test + * + * @author Mercy + * @see AutoRefreshCapability + * @since 1.0.0 + */ +class AutoRefreshCapabilityTest { + + private String contextId; + + private FeignClientProperties feignClientProperties; + + private FeignClientFactory feignClientFactory; + + private GenericApplicationContext context; + + private FeignComponentRegistry feignComponentRegistry; + + private AutoRefreshCapability capability; + + @BeforeEach + void setUp() { + this.contextId = "test-context"; + this.feignClientProperties = new FeignClientProperties(); + this.feignClientFactory = new FeignClientFactory(); + this.context = new GenericApplicationContext(); + this.context.setId(contextId); + this.context.registerBean(FeignClientProperties.class, () -> this.feignClientProperties); + this.context.refresh(); + this.feignComponentRegistry = new FeignComponentRegistry(this.contextId, this.context); + this.capability = new AutoRefreshCapability(this.feignClientProperties, this.feignClientFactory, this.feignComponentRegistry); + initFeignClientProperties(); + } + + void initFeignClientProperties() { + Map config = this.feignClientProperties.getConfig(); + config.put(this.contextId, new FeignClientProperties.FeignClientConfiguration()); + config.put(this.feignClientProperties.getDefaultConfig(), new FeignClientProperties.FeignClientConfiguration()); + } + + @Test + void testEnrich() { + assertNull(this.capability.enrich((Retryer) null)); + assertNull(this.capability.enrich((Contract) null)); + assertNull(this.capability.enrich((Decoder) null)); + assertNull(this.capability.enrich((Encoder) null)); + assertNull(this.capability.enrich((ErrorDecoder) null)); + assertNull(this.capability.enrich((RequestInterceptor) null)); + assertNull(this.capability.enrich((QueryMapEncoder) null)); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/autorefresh/FeignComponentRegistryTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/autorefresh/FeignComponentRegistryTest.java new file mode 100644 index 00000000..4e24c119 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/autorefresh/FeignComponentRegistryTest.java @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.openfeign.autorefresh; + + +import feign.Contract; +import feign.QueryMapEncoder; +import feign.RequestInterceptor; +import feign.Retryer; +import feign.codec.Decoder; +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; +import io.microsphere.spring.cloud.openfeign.components.CompositedRequestInterceptor; +import io.microsphere.spring.cloud.openfeign.components.Refreshable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.support.GenericApplicationContext; + +import java.util.List; + +import static io.microsphere.spring.cloud.openfeign.autorefresh.FeignComponentRegistry.getComponentClass; +import static io.microsphere.spring.cloud.openfeign.components.NoOpRequestInterceptor.INSTANCE; +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link FeignComponentRegistry} Test + * + * @author Mercy + * @see FeignComponentRegistry + * @since 1.0.0 + */ +class FeignComponentRegistryTest { + + private String clientName = ""; + + private GenericApplicationContext context; + + private FeignComponentRegistry registry; + + @BeforeEach + void setUp() { + this.clientName = "test-client"; + this.context = new GenericApplicationContext(); + this.context.refresh(); + this.registry = new FeignComponentRegistry(this.clientName, this.context); + } + + @Test + void testGetComponentClass() { + + assertEquals(Retryer.class, getComponentClass("retryer")); + + assertEquals(ErrorDecoder.class, getComponentClass("error-decoder")); + assertEquals(ErrorDecoder.class, getComponentClass("errorDecoder")); + + assertEquals(RequestInterceptor.class, getComponentClass("request-interceptors")); + assertEquals(RequestInterceptor.class, getComponentClass("requestInterceptors")); + + assertEquals(RequestInterceptor.class, getComponentClass("default-request-headers")); + assertEquals(RequestInterceptor.class, getComponentClass("defaultRequestHeaders")); + + assertEquals(RequestInterceptor.class, getComponentClass("default-query-parameters")); + assertEquals(RequestInterceptor.class, getComponentClass("defaultQueryParameters")); + + assertEquals(Decoder.class, getComponentClass("decoder")); + + assertEquals(Encoder.class, getComponentClass("encoder")); + + assertEquals(Contract.class, getComponentClass("contract")); + + assertEquals(QueryMapEncoder.class, getComponentClass("query-map-encoder")); + assertEquals(QueryMapEncoder.class, getComponentClass("queryMapEncoder")); + + assertNull(getComponentClass("unknown")); + assertNull(getComponentClass("")); + assertNull(getComponentClass(" ")); + assertNull(getComponentClass(null)); + } + + @Test + void testGetComponentClassForMultipleConfigs() { + + assertEquals(Retryer.class, getComponentClass("retryer[0]")); + + assertEquals(ErrorDecoder.class, getComponentClass("error-decoder[0]")); + assertEquals(ErrorDecoder.class, getComponentClass("errorDecoder[0]")); + + assertEquals(RequestInterceptor.class, getComponentClass("request-interceptors[0]")); + assertEquals(RequestInterceptor.class, getComponentClass("requestInterceptors[0]")); + + assertEquals(RequestInterceptor.class, getComponentClass("default-request-headers[0]")); + assertEquals(RequestInterceptor.class, getComponentClass("defaultRequestHeaders[0]")); + + assertEquals(RequestInterceptor.class, getComponentClass("default-query-parameters[0]")); + assertEquals(RequestInterceptor.class, getComponentClass("defaultQueryParameters[0]")); + + assertEquals(Decoder.class, getComponentClass("decoder[0]")); + + assertEquals(Encoder.class, getComponentClass("encoder[0]")); + + assertEquals(Contract.class, getComponentClass("contract[0]")); + + assertEquals(QueryMapEncoder.class, getComponentClass("query-map-encoder[0]")); + assertEquals(QueryMapEncoder.class, getComponentClass("queryMapEncoder[0]")); + } + + @Test + void testRegisterOnIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> this.registry.register(null, (Refreshable) null)); + assertThrows(IllegalArgumentException.class, () -> this.registry.register("", (Refreshable) null)); + assertThrows(IllegalArgumentException.class, () -> this.registry.register(" ", (Refreshable) null)); + assertThrows(IllegalArgumentException.class, () -> this.registry.register(this.clientName, (Refreshable) null)); + assertThrows(IllegalArgumentException.class, () -> this.registry.register(this.clientName, (List) null)); + assertThrows(IllegalArgumentException.class, () -> this.registry.register(this.clientName, emptyList())); + } + + @Test + void testRegisterRequestInterceptor() { + assertTrue(this.registry.registerRequestInterceptor(this.clientName, INSTANCE) instanceof CompositedRequestInterceptor); + assertSame(INSTANCE, this.registry.registerRequestInterceptor(this.clientName, INSTANCE)); + } + + @Test + void testRegisterRequestInterceptorOnIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> this.registry.registerRequestInterceptor(null, null)); + assertThrows(IllegalArgumentException.class, () -> this.registry.registerRequestInterceptor("", null)); + assertThrows(IllegalArgumentException.class, () -> this.registry.registerRequestInterceptor(" ", null)); + assertThrows(IllegalArgumentException.class, () -> this.registry.registerRequestInterceptor(this.clientName, null)); + } + + @Test + void testRefresh() { + testRefresh(this.clientName); + testRefresh("test-client-2"); + } + + void testRefresh(String clientName) { + this.registry.refresh(clientName, "unknown"); + this.registry.refresh(clientName, "retryer"); + this.registry.refresh(clientName, "error-decoder", "decoder", "encoder"); + this.registry.refresh(clientName, "request-interceptors", "default-request-headers", "default-query-parameters"); + + this.registry.refresh(clientName, "retryer[0]"); + this.registry.refresh(clientName, "error-decoder[0]", "decoder[0]", "encoder[0]"); + this.registry.refresh(clientName, "request-interceptors[0]", "default-request-headers[0]", "default-query-parameters[0]"); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/CompositedRequestInterceptorTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/CompositedRequestInterceptorTest.java new file mode 100644 index 00000000..57eb2754 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/CompositedRequestInterceptorTest.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.openfeign.components; + + +import feign.RequestInterceptor; +import feign.RequestTemplate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.openfeign.FeignClientProperties; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; +import org.springframework.context.support.GenericApplicationContext; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static io.microsphere.spring.cloud.openfeign.components.NoOpRequestInterceptor.INSTANCE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link CompositedRequestInterceptor} Test + * + * @author Mercy + * @see CompositedRequestInterceptor + * @since 1.0.0 + */ +class CompositedRequestInterceptorTest { + + private static final String TEST_NAME = "test-name"; + + private static final String TEST_VALUE = "test-value"; + + protected String contextId; + + private FeignClientProperties feignClientProperties; + + private GenericApplicationContext context; + + private CompositedRequestInterceptor interceptor; + + @BeforeEach + void setUp() { + this.contextId = "test-context"; + this.feignClientProperties = new FeignClientProperties(); + this.context = new GenericApplicationContext(); + this.context.setId(contextId); + this.context.registerBean(FeignClientProperties.class, () -> feignClientProperties); + this.context.refresh(); + this.interceptor = new CompositedRequestInterceptor(this.contextId, this.context); + initFeignClientProperties(); + } + + void initFeignClientProperties() { + Map config = this.feignClientProperties.getConfig(); + config.put(this.contextId, new FeignClientConfiguration()); + config.put(this.feignClientProperties.getDefaultConfig(), new FeignClientConfiguration()); + } + + FeignClientConfiguration getDefaultConfiguration() { + return this.feignClientProperties.getConfig().get(this.feignClientProperties.getDefaultConfig()); + } + + FeignClientConfiguration getCurrentConfiguration() { + return this.feignClientProperties.getConfig().get(this.contextId); + } + + @Test + void testGetRequestInterceptors() { + assertTrue(this.interceptor.getRequestInterceptors().isEmpty()); + } + + @Test + void testAddRequestInterceptor() { + this.interceptor.addRequestInterceptor(INSTANCE); + assertEquals(1, this.interceptor.getRequestInterceptors().size()); + assertTrue(this.interceptor.getRequestInterceptors().contains(INSTANCE)); + } + + @Test + void testApply() { + RequestTemplate template = new RequestTemplate(); + this.interceptor.apply(template); + this.interceptor.addRequestInterceptor(INSTANCE); + this.interceptor.apply(template); + } + + @Test + void testRefresh() { + this.interceptor.refresh(); + } + + @Test + void testRefreshOnDefaultConfiguration() { + FeignClientConfiguration defaultConfiguration = getDefaultConfiguration(); + testRefresh(defaultConfiguration); + } + + @Test + void testRefreshOnCurrentConfiguration() { + FeignClientConfiguration currentConfiguration = getCurrentConfiguration(); + addRequestInterceptor(currentConfiguration, TestRequestInterceptor.class); + testRefresh(currentConfiguration); + } + + void testRefresh(FeignClientConfiguration configuration) { + initFeignClientConfiguration(configuration); + this.interceptor.refresh(); + + RequestTemplate template = new RequestTemplate(); + this.interceptor.apply(template); + } + + void initFeignClientConfiguration(FeignClientConfiguration configuration) { + addRequestInterceptor(configuration, NoOpRequestInterceptor.class); + + Map> headers = configuration.getDefaultRequestHeaders(); + add(headers, TEST_NAME, TEST_VALUE); + + Map> parameters = configuration.getDefaultQueryParameters(); + add(parameters, TEST_NAME, TEST_VALUE); + } + + void addRequestInterceptor(FeignClientConfiguration configuration, Class requestInterceptorClass) { + List> requestInterceptors = configuration.getRequestInterceptors(); + if (requestInterceptors == null) { + requestInterceptors = new ArrayList<>(); + configuration.setRequestInterceptors(requestInterceptors); + } + requestInterceptors.add((Class) requestInterceptorClass); + } + + void add(Map> map, String name, String value) { + Collection values = map.computeIfAbsent(name, n -> new ArrayList<>()); + values.add(value); + } + + static class TestRequestInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate template) { + template.query(TEST_NAME, TEST_VALUE); + template.header(TEST_NAME, TEST_VALUE); + } + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedContractTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedContractTest.java new file mode 100644 index 00000000..550b07eb --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedContractTest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.openfeign.components; + + +import feign.Contract; +import feign.MethodMetadata; +import io.microsphere.spring.cloud.openfeign.BaseClient; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; +import org.springframework.cloud.openfeign.support.SpringMvcContract; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link DecoratedContract} Test + * + * @author Mercy + * @see DecoratedContract + * @since 1.0.0 + */ +class DecoratedContractTest extends DecoratedFeignComponentTest { + + @Override + protected Contract createDelegate() { + return new SpringMvcContract(); + } + + @Override + protected void configureDelegateClass(FeignClientConfiguration configuration, Class delegateClass) { + configuration.setContract(delegateClass); + } + + @Test + void testParseAndValidateMetadata() { + assertMethodMetadataList(this.delegate.parseAndValidateMetadata(BaseClient.class), + this.decoratedComponent.parseAndValidateMetadata(BaseClient.class)); + } + + void assertMethodMetadataList(List one, List another) { + assertEquals(one.size(), another.size()); + for (int i = 0; i < one.size(); i++) { + MethodMetadata oneMethodMetadata = one.get(i); + MethodMetadata anotherMethodMetadata = another.get(i); + assertMethodMetadata(oneMethodMetadata, anotherMethodMetadata); + } + } + + void assertMethodMetadata(MethodMetadata one, MethodMetadata another) { + assertEquals(one.method(), another.method()); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedDecoderTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedDecoderTest.java new file mode 100644 index 00000000..b6169ccd --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedDecoderTest.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.openfeign.components; + + +import feign.Response; +import feign.codec.Decoder; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; +import org.springframework.cloud.openfeign.support.SpringDecoder; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link DecoratedDecoder} Test + * + * @author Mercy + * @see DecoratedDecoder + * @since 1.0.0 + */ +class DecoratedDecoderTest extends DecoratedFeignComponentTest { + + @Override + protected Decoder createDelegate() { + HttpMessageConverters httpMessageConverters = new HttpMessageConverters(); + ObjectFactory messageConverters = () -> httpMessageConverters; + return new SpringDecoder(messageConverters); + } + + @Override + protected void configureDelegateClass(FeignClientConfiguration configuration, Class delegateClass) { + configuration.setDecoder(delegateClass); + } + + @Test + void testDecode() throws IOException { + Response response = createTestResponse(); + assertEquals(this.decoratedComponent.decode(response, String.class), this.delegate.decode(response, String.class)); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedEncoderTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedEncoderTest.java new file mode 100644 index 00000000..ab90d283 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedEncoderTest.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.openfeign.components; + + +import feign.RequestTemplate; +import feign.codec.Encoder; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; +import org.springframework.cloud.openfeign.support.SpringEncoder; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +/** + * {@link DecoratedEncoder} Test + * + * @author Mercy + * @see DecoratedEncoder + * @since 1.0.0 + */ +class DecoratedEncoderTest extends DecoratedFeignComponentTest { + + @Override + protected Encoder createDelegate() { + HttpMessageConverters httpMessageConverters = new HttpMessageConverters(); + ObjectFactory messageConverters = () -> httpMessageConverters; + return new SpringEncoder(messageConverters); + } + + @Override + protected void configureDelegateClass(FeignClientConfiguration configuration, Class delegateClass) { + configuration.setEncoder(delegateClass); + } + + @Test + void testEncode() { + RequestTemplate template = new RequestTemplate(); + String value = "Test"; + this.decoratedComponent.encode(value, String.class, template); + byte[] body = template.body(); + assertArrayEquals(value.getBytes(template.requestCharset()), body); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedErrorDecoderTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedErrorDecoderTest.java new file mode 100644 index 00000000..0732c8d3 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedErrorDecoderTest.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.openfeign.components; + + +import feign.Response; +import feign.codec.ErrorDecoder; +import feign.codec.ErrorDecoder.Default; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link DecoratedErrorDecoder} Test + * + * @author Mercy + * @see DecoratedErrorDecoder + * @since 1.0.0 + */ +class DecoratedErrorDecoderTest extends DecoratedFeignComponentTest { + + @Override + protected ErrorDecoder createDelegate() { + return new Default(); + } + + @Override + protected void configureDelegateClass(FeignClientConfiguration configuration, Class delegateClass) { + configuration.setErrorDecoder(delegateClass); + } + + @Test + void testDecode() { + Response response = createTestResponse(); + assertTrue(this.decoratedComponent.decode("echo", response) instanceof Exception); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedFeignComponentTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedFeignComponentTest.java new file mode 100644 index 00000000..48365e09 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedFeignComponentTest.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.openfeign.components; + +import feign.Request; +import feign.RequestTemplate; +import feign.Response; +import io.microsphere.spring.test.junit.jupiter.SpringLoggingTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.context.named.NamedContextFactory; +import org.springframework.cloud.openfeign.FeignClientFactory; +import org.springframework.cloud.openfeign.FeignClientProperties; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; +import org.springframework.cloud.openfeign.FeignClientSpecification; +import org.springframework.core.ResolvableType; + +import java.util.Map; + +import static feign.Request.HttpMethod.GET; +import static feign.Request.create; +import static feign.Response.builder; +import static io.microsphere.spring.cloud.openfeign.components.DecoratedFeignComponent.instantiate; +import static io.microsphere.util.ArrayUtils.EMPTY_BYTE_ARRAY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.emptyMap; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.core.ResolvableType.forClass; + +/** + * Abstract {@link DecoratedFeignComponent} Test + * + * @author Mercy + * @see DecoratedFeignComponent + * @since 1.0.0 + */ +@SpringLoggingTest +abstract class DecoratedFeignComponentTest> { + + protected String contextId; + + protected NamedContextFactory contextFactory; + + protected FeignClientProperties clientProperties; + + protected Class componentClass; + + protected Class decoratedComponentClass; + + protected C delegate; + + protected D decoratedComponent; + + @BeforeEach + void setUp() { + this.contextId = "test-context"; + this.contextFactory = new FeignClientFactory(); + this.clientProperties = new FeignClientProperties(); + + ResolvableType resolvableType = forClass(this.getClass()).as(DecoratedFeignComponentTest.class); + this.componentClass = (Class) resolvableType.getGeneric(0).resolve(); + this.decoratedComponentClass = (Class) resolvableType.getGeneric(1).resolve(); + + this.delegate = createDelegate(); + this.decoratedComponent = instantiate(decoratedComponentClass, componentClass, contextId, contextFactory, clientProperties, delegate); + } + + @Test + void testComponentTypeFromDefaultConfiguration() { + initDefaultConfiguration(); + Class delegateClass = getDelegateClass(); + configureDelegateClass(this.decoratedComponent.getDefaultConfiguration(), delegateClass); + assertSame(delegateClass, this.decoratedComponent.componentType()); + } + + @Test + void testComponentTypeFromCurrentConfiguration() { + initCurrentConfiguration(); + Class delegateClass = getDelegateClass(); + configureDelegateClass(this.decoratedComponent.getCurrentConfiguration(), delegateClass); + assertSame(delegateClass, this.decoratedComponent.componentType()); + } + + @Test + void testComponentType() { + assertTrue(componentClass.isAssignableFrom(this.decoratedComponent.componentType())); + } + + @Test + void testEquals() { + assertEquals(this.decoratedComponent, this.delegate); + } + + @Test + void testHashCode() { + assertEquals(this.decoratedComponent.hashCode(), this.delegate.hashCode()); + } + + @Test + void testToString() { + assertEquals(this.decoratedComponent.toString(), this.delegate.toString()); + } + + @Test + void testLoadInstanceFromContextFactory() { + String contextId = this.decoratedComponent.contextId(); + Class componentType = this.decoratedComponent.componentType(); + C component = this.decoratedComponent.loadInstanceFromContextFactory(contextId, componentType); + assertNotNull(component); + + assertNotNull(this.decoratedComponent.loadInstanceFromContextFactory(contextId, String.class)); + } + + protected abstract C createDelegate(); + + protected abstract void configureDelegateClass(FeignClientConfiguration configuration, Class delegateClass); + + protected Class getDelegateClass() { + return (Class) this.delegate.getClass(); + } + + protected Request createTestRequest() { + return create(GET, "http://localhost", emptyMap(), EMPTY_BYTE_ARRAY, UTF_8, new RequestTemplate()); + } + + protected Response createTestResponse() { + return builder() + .status(200) + .request(createTestRequest()) + .body(new byte[1024]) + .build(); + } + + void initDefaultConfiguration() { + String defaultConfig = this.clientProperties.getDefaultConfig(); + setConfiguration(defaultConfig, new FeignClientConfiguration()); + } + + void initCurrentConfiguration() { + setConfiguration(this.contextId, new FeignClientConfiguration()); + } + + void setConfiguration(String id, FeignClientConfiguration configuration) { + Map config = this.clientProperties.getConfig(); + config.put(id, configuration); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedQueryMapEncoderTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedQueryMapEncoderTest.java new file mode 100644 index 00000000..4ff596c2 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedQueryMapEncoderTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.openfeign.components; + + +import feign.QueryMapEncoder; +import feign.querymap.BeanQueryMapEncoder; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; + +import static io.microsphere.spring.cloud.openfeign.components.DecoratedQueryMapEncoder.getQueryMapEncoder; +import static io.microsphere.spring.cloud.openfeign.components.DecoratedQueryMapEncoder.getQueryMapEncoderMethodHandle; +import static java.util.Collections.emptyMap; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * {@link DecoratedQueryMapEncoder} Test + * + * @author Mercy + * @see DecoratedQueryMapEncoder + * @since 1.0.0 + */ +class DecoratedQueryMapEncoderTest extends DecoratedFeignComponentTest { + + @Override + protected QueryMapEncoder createDelegate() { + return new BeanQueryMapEncoder(); + } + + @Override + protected void configureDelegateClass(FeignClientConfiguration configuration, Class delegateClass) { + configuration.setQueryMapEncoder(delegateClass); + } + + @Test + void testEncode() { + assertSame(emptyMap(), this.decoratedComponent.encode(null)); + } + + @Test + void testGetQueryMapEncoder() { + assertNull(getQueryMapEncoder(null, null)); + assertNull(getQueryMapEncoder(getQueryMapEncoderMethodHandle, null)); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedRetryerTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedRetryerTest.java new file mode 100644 index 00000000..b0725ae6 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/components/DecoratedRetryerTest.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.microsphere.spring.cloud.openfeign.components; + + +import feign.Request; +import feign.RetryableException; +import feign.Retryer; +import feign.Retryer.Default; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration; + +import java.util.Date; + +import static feign.Request.HttpMethod.GET; +import static io.microsphere.spring.cloud.openfeign.components.DecoratedRetryer.continueOrPropagate; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * {@link DecoratedRetryer} Test + * + * @author Mercy + * @see DecoratedRetryer + * @since 1.0.0 + */ +class DecoratedRetryerTest extends DecoratedFeignComponentTest { + + @Override + protected Retryer createDelegate() { + return new Default(); + } + + @Override + protected void configureDelegateClass(FeignClientConfiguration configuration, Class delegateClass) { + configuration.setRetryer(delegateClass); + } + + @Test + void testContinueOrPropagate() { + this.decoratedComponent.refresh(); + Request request = createTestRequest(); + RetryableException e = new RetryableException(1, "error", GET, new Date(), request); + assertThrows(RetryableException.class, () -> this.decoratedComponent.continueOrPropagate(e)); + continueOrPropagate(null, e); + } + + @Test + void testClone() { + assertNotNull(this.decoratedComponent.clone()); + } +} \ No newline at end of file diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/decoder/ADecoder.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/decoder/ADecoder.java new file mode 100644 index 00000000..14b85353 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/decoder/ADecoder.java @@ -0,0 +1,21 @@ +package io.microsphere.spring.cloud.openfeign.decoder; + +import feign.FeignException; +import feign.Response; +import feign.codec.DecodeException; +import feign.codec.Decoder; + +import java.io.IOException; +import java.lang.reflect.Type; + +/** + * @author 韩超 + * @since 0.0.1 + */ +public class ADecoder implements Decoder { + + @Override + public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException { + return null; + } +} diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/decoder/BDecoder.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/decoder/BDecoder.java new file mode 100644 index 00000000..7762fe7d --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/decoder/BDecoder.java @@ -0,0 +1,21 @@ +package io.microsphere.spring.cloud.openfeign.decoder; + +import feign.FeignException; +import feign.Response; +import feign.codec.DecodeException; +import feign.codec.Decoder; + +import java.io.IOException; +import java.lang.reflect.Type; + +/** + * @author 韩超 + * @since 0.0.1 + */ +public class BDecoder implements Decoder { + + @Override + public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException { + return null; + } +} diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/decoder/DecoderChangedTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/decoder/DecoderChangedTest.java new file mode 100644 index 00000000..37c476cb --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/decoder/DecoderChangedTest.java @@ -0,0 +1,36 @@ +package io.microsphere.spring.cloud.openfeign.decoder; + +import feign.codec.Decoder; +import io.microsphere.spring.cloud.openfeign.BaseTest; +import io.microsphere.spring.cloud.openfeign.FeignComponentAssert; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * @author 韩超 + * @since 0.0.1 + */ +@SpringBootTest(classes = DecoderChangedTest.class) +@EnableAutoConfiguration +class DecoderChangedTest extends BaseTest { + + @Override + protected String afterTestComponentConfigKey() { + return "spring.cloud.openfeign.client.config.my-client.decoder"; + } + + @Override + protected Class beforeTestComponentClass() { + return ADecoder.class; + } + + @Override + protected Class afterTestComponent() { + return BDecoder.class; + } + + @Override + protected FeignComponentAssert loadFeignComponentAssert() { + return new DecoderComponentAssert(); + } +} diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/decoder/DecoderComponentAssert.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/decoder/DecoderComponentAssert.java new file mode 100644 index 00000000..c43b0c4a --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/decoder/DecoderComponentAssert.java @@ -0,0 +1,24 @@ +package io.microsphere.spring.cloud.openfeign.decoder; + +import feign.ResponseHandler; +import feign.codec.Decoder; +import io.microsphere.spring.cloud.openfeign.FeignComponentAssert; +import io.microsphere.spring.cloud.openfeign.components.DecoratedDecoder; + +import java.lang.reflect.Field; + +/** + * @author 韩超 + * @since 0.0.1 + */ +public class DecoderComponentAssert extends FeignComponentAssert { + + @Override + protected Decoder loadCurrentComponent(Object configuration, ResponseHandler responseHandler) throws Exception { + Class responseHandlerClass = ResponseHandler.class; + Field decoderField = responseHandlerClass.getDeclaredField("decoder"); + decoderField.setAccessible(true); + DecoratedDecoder decoder = (DecoratedDecoder)decoderField.get(responseHandler); + return decoder.delegate(); + } +} diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/encoder/AEncoder.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/encoder/AEncoder.java index a9883bad..23abbb89 100644 --- a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/encoder/AEncoder.java +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/encoder/AEncoder.java @@ -14,7 +14,6 @@ public class AEncoder implements Encoder { @Override public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { - System.out.println("AEncoder is working..."); template.body(object.toString()); } } diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/encoder/BEncoder.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/encoder/BEncoder.java index 302c04e2..1d3300d1 100644 --- a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/encoder/BEncoder.java +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/encoder/BEncoder.java @@ -14,7 +14,6 @@ public class BEncoder implements Encoder { @Override public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { - System.out.println("BEncoder is working..."); template.body(object.toString()); } } diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/encoder/EncoderChangedTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/encoder/EncoderChangedTest.java index 938ea9fb..3a772a46 100644 --- a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/encoder/EncoderChangedTest.java +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/encoder/EncoderChangedTest.java @@ -2,6 +2,7 @@ import feign.codec.Encoder; import io.microsphere.spring.cloud.openfeign.BaseTest; +import io.microsphere.spring.cloud.openfeign.FeignComponentAssert; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; @@ -11,11 +12,22 @@ */ @SpringBootTest(classes = EncoderChangedTest.class) @EnableAutoConfiguration -public class EncoderChangedTest extends BaseTest { +class EncoderChangedTest extends BaseTest { + + + @Override + protected Class beforeTestComponentClass() { + return AEncoder.class; + } + + @Override + protected FeignComponentAssert loadFeignComponentAssert() { + return EncoderComponentAssert.INSTANCE; + } @Override protected String afterTestComponentConfigKey() { - return "feign.client.config.my-client.encoder"; + return "spring.cloud.openfeign.client.config.my-client.encoder"; } @Override diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/encoder/EncoderComponentAssert.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/encoder/EncoderComponentAssert.java new file mode 100644 index 00000000..b79c0d35 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/encoder/EncoderComponentAssert.java @@ -0,0 +1,34 @@ +package io.microsphere.spring.cloud.openfeign.encoder; + +import feign.ResponseHandler; +import feign.codec.Encoder; +import io.microsphere.spring.cloud.openfeign.FeignComponentAssert; +import io.microsphere.spring.cloud.openfeign.components.DecoratedEncoder; + +import java.lang.reflect.Field; + +/** + * @author 韩超 + * @since 0.0.1 + */ +public class EncoderComponentAssert extends FeignComponentAssert { + + public static final EncoderComponentAssert INSTANCE = new EncoderComponentAssert(); + + private EncoderComponentAssert() { + + } + + @Override + protected Encoder loadCurrentComponent(Object configuration, ResponseHandler responseHandler) throws Exception { + Class configurationClass = configuration.getClass(); + Field buildTemplateFromArgs = configurationClass.getDeclaredField("buildTemplateFromArgs"); + buildTemplateFromArgs.setAccessible(true); + Object buildTemplateFromArgsValue = buildTemplateFromArgs.get(configuration); + Class buildTemplateFromArgsType = buildTemplateFromArgsValue.getClass(); + Field encoderField = buildTemplateFromArgsType.getDeclaredField("encoder"); + encoderField.setAccessible(true); + DecoratedEncoder encoder = (DecoratedEncoder)encoderField.get(buildTemplateFromArgsValue); + return encoder.delegate(); + } +} diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/errordecoder/AErrorDecoder.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/errordecoder/AErrorDecoder.java index 7fb72d91..db06bbf4 100644 --- a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/errordecoder/AErrorDecoder.java +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/errordecoder/AErrorDecoder.java @@ -2,8 +2,8 @@ import feign.Response; import feign.codec.ErrorDecoder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import io.microsphere.logging.Logger; +import static io.microsphere.logging.LoggerFactory.getLogger; /** * @author 韩超 @@ -11,11 +11,10 @@ */ public class AErrorDecoder implements ErrorDecoder { - private static final Logger log = LoggerFactory.getLogger(AErrorDecoder.class); + private static final Logger log = getLogger(AErrorDecoder.class); @Override public Exception decode(String methodKey, Response response) { - log.trace("Error decoding {}", methodKey); return new IllegalArgumentException(""); } } diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/errordecoder/BErrorEncoder.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/errordecoder/BErrorEncoder.java new file mode 100644 index 00000000..4c7bac0f --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/errordecoder/BErrorEncoder.java @@ -0,0 +1,16 @@ +package io.microsphere.spring.cloud.openfeign.errordecoder; + +import feign.Response; +import feign.codec.ErrorDecoder; + +/** + * @author 韩超 + * @since 0.0.1 + */ +public class BErrorEncoder implements ErrorDecoder { + + @Override + public Exception decode(String methodKey, Response response) { + return null; + } +} diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/errordecoder/ErrorDecoderChangedTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/errordecoder/ErrorDecoderChangedTest.java index e431c1f4..5ee8d7ef 100644 --- a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/errordecoder/ErrorDecoderChangedTest.java +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/errordecoder/ErrorDecoderChangedTest.java @@ -2,6 +2,7 @@ import feign.codec.ErrorDecoder; import io.microsphere.spring.cloud.openfeign.BaseTest; +import io.microsphere.spring.cloud.openfeign.FeignComponentAssert; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; @@ -11,15 +12,25 @@ */ @SpringBootTest(classes = ErrorDecoderChangedTest.class) @EnableAutoConfiguration -public class ErrorDecoderChangedTest extends BaseTest { +class ErrorDecoderChangedTest extends BaseTest { + + @Override + protected Class beforeTestComponentClass() { + return AErrorDecoder.class; + } + + @Override + protected FeignComponentAssert loadFeignComponentAssert() { + return new ErrorDecoderComponentAssert(); + } @Override protected String afterTestComponentConfigKey() { - return "feign.client.config.my-client.error-decoder"; + return "spring.cloud.openfeign.client.config.my-client.error-decoder"; } @Override protected Class afterTestComponent() { - return AErrorDecoder.class; + return BErrorEncoder.class; } } diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/errordecoder/ErrorDecoderComponentAssert.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/errordecoder/ErrorDecoderComponentAssert.java new file mode 100644 index 00000000..10892595 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/errordecoder/ErrorDecoderComponentAssert.java @@ -0,0 +1,24 @@ +package io.microsphere.spring.cloud.openfeign.errordecoder; + +import feign.ResponseHandler; +import feign.codec.ErrorDecoder; +import io.microsphere.spring.cloud.openfeign.FeignComponentAssert; +import io.microsphere.spring.cloud.openfeign.components.DecoratedErrorDecoder; + +import java.lang.reflect.Field; + +/** + * @author 韩超 + * @since 0.0.1 + */ +public class ErrorDecoderComponentAssert extends FeignComponentAssert { + + @Override + protected ErrorDecoder loadCurrentComponent(Object configuration, ResponseHandler responseHandler) throws Exception { + Class responseHandlerClass = ResponseHandler.class; + Field errorDecoderField = responseHandlerClass.getDeclaredField("errorDecoder"); + errorDecoderField.setAccessible(true); + DecoratedErrorDecoder errorDecoder = (DecoratedErrorDecoder)errorDecoderField.get(responseHandler); + return errorDecoder.delegate(); + } +} diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/querymapencoder/AQueryMapEncoder.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/querymapencoder/AQueryMapEncoder.java index 4cde8f05..e07793e5 100644 --- a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/querymapencoder/AQueryMapEncoder.java +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/querymapencoder/AQueryMapEncoder.java @@ -1,12 +1,13 @@ package io.microsphere.spring.cloud.openfeign.querymapencoder; import feign.QueryMapEncoder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import io.microsphere.logging.Logger; import java.util.HashMap; import java.util.Map; +import static io.microsphere.logging.LoggerFactory.getLogger; + /** * @author 韩超 * @since 1.0 @@ -14,11 +15,10 @@ public class AQueryMapEncoder implements QueryMapEncoder { - private static final Logger log = LoggerFactory.getLogger(AQueryMapEncoder.class); + private static final Logger log = getLogger(AQueryMapEncoder.class); @Override public Map encode(Object object) { - log.trace("AQueryMapEncoder.encode({})", object); return new HashMap<>(); } } diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/querymapencoder/BQueryMapEncoder.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/querymapencoder/BQueryMapEncoder.java index 08032694..18762064 100644 --- a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/querymapencoder/BQueryMapEncoder.java +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/querymapencoder/BQueryMapEncoder.java @@ -1,12 +1,13 @@ package io.microsphere.spring.cloud.openfeign.querymapencoder; import feign.QueryMapEncoder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import io.microsphere.logging.Logger; import java.util.HashMap; import java.util.Map; +import static io.microsphere.logging.LoggerFactory.getLogger; + /** * @author 韩超 * @since 1.0 @@ -14,11 +15,10 @@ public class BQueryMapEncoder implements QueryMapEncoder { - private static final Logger log = LoggerFactory.getLogger(BQueryMapEncoder.class); + private static final Logger log = getLogger(BQueryMapEncoder.class); @Override public Map encode(Object object) { - log.trace("AQueryMapEncoder.encode({})", object); return new HashMap<>(); } } diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/querymapencoder/QueryMapEncoderChangedTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/querymapencoder/QueryMapEncoderChangedTest.java index 69928a64..1374594a 100644 --- a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/querymapencoder/QueryMapEncoderChangedTest.java +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/querymapencoder/QueryMapEncoderChangedTest.java @@ -2,6 +2,7 @@ import feign.QueryMapEncoder; import io.microsphere.spring.cloud.openfeign.BaseTest; +import io.microsphere.spring.cloud.openfeign.FeignComponentAssert; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; @@ -11,11 +12,21 @@ */ @SpringBootTest(classes = QueryMapEncoderChangedTest.class) @EnableAutoConfiguration -public class QueryMapEncoderChangedTest extends BaseTest { +class QueryMapEncoderChangedTest extends BaseTest { + + @Override + protected Class beforeTestComponentClass() { + return AQueryMapEncoder.class; + } + + @Override + protected FeignComponentAssert loadFeignComponentAssert() { + return new QueryMapEncoderComponentAssert(); + } @Override protected String afterTestComponentConfigKey() { - return "feign.client.config.my-client.query-map-encoder"; + return "spring.cloud.openfeign.client.config.my-client.query-map-encoder"; } @Override diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/querymapencoder/QueryMapEncoderComponentAssert.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/querymapencoder/QueryMapEncoderComponentAssert.java new file mode 100644 index 00000000..9eeb1802 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/querymapencoder/QueryMapEncoderComponentAssert.java @@ -0,0 +1,28 @@ +package io.microsphere.spring.cloud.openfeign.querymapencoder; + +import feign.QueryMapEncoder; +import feign.ResponseHandler; +import io.microsphere.spring.cloud.openfeign.FeignComponentAssert; +import io.microsphere.spring.cloud.openfeign.components.DecoratedQueryMapEncoder; + +import java.lang.reflect.Field; + +/** + * @author 韩超 + * @since 0.0.1 + */ +public class QueryMapEncoderComponentAssert extends FeignComponentAssert { + + @Override + protected QueryMapEncoder loadCurrentComponent(Object configuration, ResponseHandler responseHandler) throws Exception { + Class configurationClass = configuration.getClass(); + Field buildTemplateFromArgs = configurationClass.getDeclaredField("buildTemplateFromArgs"); + buildTemplateFromArgs.setAccessible(true); + Object buildTemplateFromArgsValue = buildTemplateFromArgs.get(configuration); + Class buildTemplateFromArgsType = buildTemplateFromArgsValue.getClass().getSuperclass(); + Field encoderField = buildTemplateFromArgsType.getDeclaredField("queryMapEncoder"); + encoderField.setAccessible(true); + DecoratedQueryMapEncoder encoder = (DecoratedQueryMapEncoder)encoderField.get(buildTemplateFromArgsValue); + return encoder.delegate(); + } +} diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/requestInterceptor/ARequestInterceptor.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/requestInterceptor/ARequestInterceptor.java index 84ffa4d6..d7e8fc0b 100644 --- a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/requestInterceptor/ARequestInterceptor.java +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/requestInterceptor/ARequestInterceptor.java @@ -11,6 +11,5 @@ public class ARequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { - System.out.println(getClass() + ": A is working..."); } } diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/requestInterceptor/BRequestInterceptor.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/requestInterceptor/BRequestInterceptor.java index 108f58cf..b06dd608 100644 --- a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/requestInterceptor/BRequestInterceptor.java +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/requestInterceptor/BRequestInterceptor.java @@ -11,6 +11,5 @@ public class BRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { - System.out.println(getClass() + "B is working..."); } } diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/requestInterceptor/RequestInterceptorChangedTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/requestInterceptor/RequestInterceptorChangedTest.java index 870b94a6..09e2f835 100644 --- a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/requestInterceptor/RequestInterceptorChangedTest.java +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/requestInterceptor/RequestInterceptorChangedTest.java @@ -2,23 +2,33 @@ import feign.RequestInterceptor; import io.microsphere.spring.cloud.openfeign.BaseTest; -import org.junit.jupiter.api.extension.ExtendWith; +import io.microsphere.spring.cloud.openfeign.FeignComponentAssert; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; /** * @author 韩超 * @since 0.0.1 */ -@SpringBootTest(classes = RequestInterceptorChangedTest.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@SpringBootTest(classes = RequestInterceptorChangedTest.class, webEnvironment = NONE) @EnableAutoConfiguration -public class RequestInterceptorChangedTest extends BaseTest { +class RequestInterceptorChangedTest extends BaseTest { + + @Override + protected Class beforeTestComponentClass() { + return ARequestInterceptor.class; + } + + @Override + protected FeignComponentAssert loadFeignComponentAssert() { + return new RequestInterceptorComponentAssert(); + } @Override protected String afterTestComponentConfigKey() { - return "feign.client.config.my-client.request-interceptors[0]"; + return "spring.cloud.openfeign.client.config.my-client.request-interceptors[0]"; } @Override diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/requestInterceptor/RequestInterceptorComponentAssert.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/requestInterceptor/RequestInterceptorComponentAssert.java new file mode 100644 index 00000000..c3eb60a6 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/requestInterceptor/RequestInterceptorComponentAssert.java @@ -0,0 +1,40 @@ +package io.microsphere.spring.cloud.openfeign.requestInterceptor; + +import feign.RequestInterceptor; +import feign.ResponseHandler; +import io.microsphere.spring.cloud.openfeign.FeignComponentAssert; +import io.microsphere.spring.cloud.openfeign.components.CompositedRequestInterceptor; + +import java.lang.reflect.Field; +import java.util.List; + +/** + * @author 韩超 + * @since 0.0.1 + */ +public class RequestInterceptorComponentAssert extends FeignComponentAssert { + + @Override + protected CompositedRequestInterceptor loadCurrentComponent(Object configuration, ResponseHandler responseHandler) throws Exception { + Class configurationClass = configuration.getClass(); + Field retryField = configurationClass.getDeclaredField("requestInterceptors"); + retryField.setAccessible(true); + List retryer = (List) retryField.get(configuration); + for (RequestInterceptor interceptor : retryer) { + if (interceptor instanceof CompositedRequestInterceptor) { + return (CompositedRequestInterceptor) interceptor; + } + } + return null; + } + + @Override + public boolean expect(Object configuration, ResponseHandler responseHandler, Class expectedClass) throws Exception { + CompositedRequestInterceptor requestInterceptor = loadCurrentComponent(configuration, responseHandler); + for(RequestInterceptor interceptor : requestInterceptor.getRequestInterceptors()) { + if (expectedClass.equals(interceptor.getClass())) + return true; + } + return false; + } +} diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/retryer/ARetry.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/retryer/ARetry.java index b43e6436..a4856551 100644 --- a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/retryer/ARetry.java +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/retryer/ARetry.java @@ -2,8 +2,8 @@ import feign.RetryableException; import feign.Retryer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import io.microsphere.logging.Logger; +import static io.microsphere.logging.LoggerFactory.getLogger; /** * @author 韩超 @@ -11,7 +11,7 @@ */ public class ARetry implements Retryer { - private static final Logger log = LoggerFactory.getLogger(ARetry.class); + private static final Logger log = getLogger(ARetry.class); @Override public void continueOrPropagate(RetryableException e) { diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/retryer/BRetry.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/retryer/BRetry.java new file mode 100644 index 00000000..b8ac51c0 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/retryer/BRetry.java @@ -0,0 +1,21 @@ +package io.microsphere.spring.cloud.openfeign.retryer; + +import feign.RetryableException; +import feign.Retryer; + +/** + * @author 韩超 + * @since 0.0.1 + */ +public class BRetry implements Retryer { + + @Override + public void continueOrPropagate(RetryableException e) { + + } + + @Override + public Retryer clone() { + return this; + } +} diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/retryer/RetryerChangedTest.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/retryer/RetryerChangedTest.java index 5c66d255..a983430a 100644 --- a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/retryer/RetryerChangedTest.java +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/retryer/RetryerChangedTest.java @@ -2,6 +2,7 @@ import feign.Retryer; import io.microsphere.spring.cloud.openfeign.BaseTest; +import io.microsphere.spring.cloud.openfeign.FeignComponentAssert; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; @@ -11,15 +12,25 @@ */ @SpringBootTest(classes = RetryerChangedTest.class) @EnableAutoConfiguration -public class RetryerChangedTest extends BaseTest { +class RetryerChangedTest extends BaseTest { + + @Override + protected Class beforeTestComponentClass() { + return ARetry.class; + } + + @Override + protected FeignComponentAssert loadFeignComponentAssert() { + return new RetryerComponentAssert(); + } @Override protected String afterTestComponentConfigKey() { - return "feign.client.config.my-client.retryer"; + return "spring.cloud.openfeign.client.config.my-client.retryer"; } @Override protected Class afterTestComponent() { - return ARetry.class; + return BRetry.class; } } diff --git a/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/retryer/RetryerComponentAssert.java b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/retryer/RetryerComponentAssert.java new file mode 100644 index 00000000..19dc78c1 --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/java/io/microsphere/spring/cloud/openfeign/retryer/RetryerComponentAssert.java @@ -0,0 +1,24 @@ +package io.microsphere.spring.cloud.openfeign.retryer; + +import feign.ResponseHandler; +import feign.Retryer; +import io.microsphere.spring.cloud.openfeign.FeignComponentAssert; +import io.microsphere.spring.cloud.openfeign.components.DecoratedRetryer; + +import java.lang.reflect.Field; + +/** + * @author 韩超 + * @since 0.0.1 + */ +public class RetryerComponentAssert extends FeignComponentAssert { + + @Override + protected Retryer loadCurrentComponent(Object configuration, ResponseHandler responseHandler) throws Exception { + Class configurationClass = configuration.getClass(); + Field retryField = configurationClass.getDeclaredField("retryer"); + retryField.setAccessible(true); + DecoratedRetryer retryer = (DecoratedRetryer) retryField.get(configuration); + return retryer.delegate(); + } +} diff --git a/microsphere-spring-cloud-openfeign/src/test/resources/logback-test.xml b/microsphere-spring-cloud-openfeign/src/test/resources/logback-test.xml new file mode 100644 index 00000000..0d11a9ed --- /dev/null +++ b/microsphere-spring-cloud-openfeign/src/test/resources/logback-test.xml @@ -0,0 +1,27 @@ + + + + + + + ${ENCODER_PATTERN} + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/microsphere-spring-cloud-parent/pom.xml b/microsphere-spring-cloud-parent/pom.xml index 58aafe55..1727b7ea 100644 --- a/microsphere-spring-cloud-parent/pom.xml +++ b/microsphere-spring-cloud-parent/pom.xml @@ -20,12 +20,31 @@ - 0.1.1 - 1.20.1 + 0.2.6 + 2.0.4 + + 6.0.3 + + + org.junit + junit-bom + ${junit-jupiter.version} + pom + import + + + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + @@ -45,15 +64,6 @@ import - - - io.github.microsphere-projects - microsphere-spring-boot-dependencies - ${microsphere-spring-boot.version} - pom - import - - com.alibaba.cloud @@ -63,11 +73,11 @@ import - + - org.testcontainers - testcontainers-bom - ${testcontainers.version} + io.github.microsphere-projects + microsphere-spring-boot-dependencies + ${microsphere-spring-boot.version} pom import @@ -77,130 +87,71 @@ + - spring-cloud-hoxton + spring-cloud-2022 - 2.3.12.RELEASE - Hoxton.SR12 - 2.2.10 + 3.0.13 + 2022.0.5 + 2022.0.0.2 - spring-cloud-2020 + spring-cloud-2023 - 2.4.13 - 2020.0.5 - 2021.1 + 3.3.13 + 2023.0.6 + 2023.0.3.4 - spring-cloud-2021 - - true - + spring-cloud-2024 - 2.6.15 - 2021.0.9 - 2021.0.6.1 + 3.4.12 + 2024.0.3 + 2023.0.3.4 - - spring-cloud-netflix-eureka - - - org.springframework.cloud - spring-cloud-netflix-eureka-client - test - - - - com.netflix.eureka - eureka-client - test - - - - org.springframework.cloud - spring-cloud-netflix-eureka-server - test - - - - - - - spring-cloud-alibaba-nacos - - - com.alibaba.cloud - spring-cloud-starter-alibaba-nacos-discovery - test - - - - com.alibaba.cloud - spring-cloud-starter-alibaba-nacos-config - test - - - - - - - spring-cloud-zookeeper - - - org.springframework.cloud - spring-cloud-starter-zookeeper-discovery - test - - - - org.springframework.cloud - spring-cloud-starter-zookeeper-config - test - - + spring-cloud-2025 + + true + + + 3.5.8 + 2025.0.1 + 2025.0.0.0 + - - spring-cloud-consul - - - org.springframework.cloud - spring-cloud-starter-consul-discovery - test - - - - org.springframework.cloud - spring-cloud-starter-consul-config - test - - - - org.testcontainers - consul - test - - + spring-cloud-2025.1 + + 4.0.2 + 2025.1.1 + 2025.1.0.0 + - + - spring-cloud-kubernetes - - - org.springframework.cloud - spring-cloud-starter-kubernetes-client-all - test - - + testcontainers + + + + org.apache.maven.plugins + maven-surefire-plugin + + + true + + + + + diff --git a/mvnw b/mvnw new file mode 100755 index 00000000..bd8896bf --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 00000000..5761d948 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index b5788ef7..ef2394b3 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.github.microsphere-projects microsphere-build - 0.1.0 + 0.2.5 4.0.0 @@ -52,7 +52,8 @@ - 0.1.1-SNAPSHOT + 0.2.5-SNAPSHOT + 17 @@ -62,28 +63,4 @@ microsphere-spring-cloud-openfeign - - - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - - - snapshot - - true - - - false - - https://s01.oss.sonatype.org/content/repositories/snapshots - - -
    \ No newline at end of file