+ * This task reads configuration from {@link ServiceMetadataExtension} and writes + * a {@code service-metadata.properties} file to the build resources directory. + * The generated file contains service identification, versioning, grouping, + * description, and any additional custom properties. + *
+ * + * @author 海子 Yang + * @since 1.0 2026/3/10 21:59 + */ +@DisableCachingByDefault(because = "Not worth caching") +public abstract class GenerateServiceMetadata extends DefaultTask { + + @TaskAction + public void generate() throws IOException { + Project project = getProject(); + List+ * The traversal order is deterministic: directories and files are processed in alphabetical order + * to ensure consistent results across runs. + * + * @author Harry Yang + * @since 4.0 + */ +public abstract class ServiceInterfacesFinder { + + private static final String DOT_CLASS = ".class"; + + private static final String SERVICE = "infra.stereotype.Service"; + + /** + * File filter that accepts only files ending with {@code .class}. + */ + private static final FileFilter CLASS_FILE_FILTER = ServiceInterfacesFinder::isClassFile; + + /** + * File filter that accepts only directories that do not start with a dot (hidden directories). + */ + private static final FileFilter PACKAGE_DIRECTORY_FILTER = ServiceInterfacesFinder::isPackageDirectory; + + /** + * Checks if the given file is a valid Java class file. + * + * @param file the file to check + * @return {@code true} if the file is a regular file ending with {@code .class}, {@code false} otherwise + */ + private static boolean isClassFile(File file) { + return file.isFile() && file.getName().endsWith(DOT_CLASS); + } + + /** + * Checks if the given file is a valid package directory. + *
+ * A valid package directory is a directory that does not start with a dot (i.e., not hidden).
+ *
+ * @param file the file to check
+ * @return {@code true} if the file is a directory and not hidden, {@code false} otherwise
+ */
+ private static boolean isPackageDirectory(File file) {
+ return file.isDirectory() && !file.getName().startsWith(".");
+ }
+
+ /**
+ * Finds all public interface names within the specified root directory that are annotated
+ * with the given annotation name.
+ *
+ * @param rootDirectory the root directory to start scanning from
+ * @param annotationName the fully qualified name of the annotation to filter by, or {@code null} to ignore annotation filtering
+ * @return a list of fully qualified interface names matching the criteria
+ * @throws IOException if an I/O error occurs while reading class files
+ */
+ public static List
+ * If the callback returns a non-null result, the traversal stops immediately and the result is returned.
+ *
+ * @param rootDirectory the root directory to start scanning from
+ * @param callback the callback to invoke for each discovered interface
+ * @param
+ * Sorting ensures a deterministic traversal order.
+ *
+ * @param stack the stack to push files onto
+ * @param files the array of files to sort and push; may be {@code null}
+ */
+ private static void pushAllSorted(Deque