Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 fix #1030 The Spring Boot jar fails to run when using @AiServic… #20

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

@langchain4j
Copy link
Owner

Hi @clear2x almost all tests in langchain4j-spring-boot-starter module are failing with this change, could you please check?

@clear2x
Copy link
Author

clear2x commented May 21, 2024

Sorry, I have already fixed it.

@clear2x
Copy link
Author

clear2x commented May 21, 2024

I found that the dev.langchain4j.service.spring.mode.automatic.withTools.AiServicesAutoConfigIT#should_create_AI_service_with_tool_that_is_package_private_method_in_package_private_class case fails to execute both before and after the modification.

return reflections.getTypesAnnotatedWith(AiService.class);
Reflections reflections = new Reflections((new ConfigurationBuilder()).forPackage(basePackage));
Set<Class<?>> classes = reflections.getTypesAnnotatedWith(AiService.class);
classes.removeIf(clazz -> !clazz.getName().startsWith(basePackage));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something fishy is going on... Why is filtering required? Why does getTypesAnnotatedWith return all classes, even if we specify basePackage?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forPackage(basePackage)); 

This code calls the following piece of code :

public static Collection<URL> forResource(String resourceName, ClassLoader... classLoaders) {
        final List<URL> result = new ArrayList<>();
        final ClassLoader[] loaders = classLoaders(classLoaders);
        for (ClassLoader classLoader : loaders) {
            try {
                final Enumeration<URL> urls = classLoader.getResources(resourceName);
                while (urls.hasMoreElements()) {
                    final URL url = urls.nextElement();
                    int index = url.toExternalForm().lastIndexOf(resourceName);
                    if (index != -1) {
                    	// Add old url as contextUrl to support exotic url handlers
                        result.add(new URL(url, url.toExternalForm().substring(0, index)));
                    } else {
                        result.add(url);
                    }
                }
            } catch (IOException e) {
                if (Reflections.log != null) {
                    Reflections.log.error("error getting resources for " + resourceName, e);
                }
            }
        }
        return distinctUrls(result);
    }

where the key part is:

int index = url.toExternalForm().lastIndexOf(resourceName);
if (index != -1) {
 // Add old url as contextUrl to support exotic url handlers
    result.add(new URL(url, url.toExternalForm().substring(0, index)));
} else {
    result.add(url);
}

This segment extracts a part of the URL, and after extraction, it becomes test-classes.

Our goal is to find all AI Services under the base package and its sub-packages, so we perform filtering here .

@clear2x
Copy link
Author

clear2x commented May 23, 2024

A few other thoughts...... "In jar files, Reflections cannot scan classes." This issue has been discussed extensively in the link above. We might also consider not relying on Reflections and directly using Spring's related methods to achieve our goal.

@langchain4j
Copy link
Owner

@clear2x instead of Reflections, which spring methods can be used? I tried to use some, but they ignore interfaces

@clear2x
Copy link
Author

clear2x commented Jun 3, 2024

ResourcePatternResolver, PathMatchingResourcePatternResolver, StandardEnvironment, and SimpleMetadataReaderFactory can work together to achieve our goal.

public class AnnotationUtils extends org.springframework.core.annotation.AnnotationUtils {
    private static final PathMatchingResourcePatternResolver RESOLVER = new PathMatchingResourcePatternResolver();
    private static final SimpleMetadataReaderFactory REGISTER = new SimpleMetadataReaderFactory();
    private static final StandardEnvironment ENVIRONMENT = new StandardEnvironment();

    public static String getResourcePath(String packagePath) {
        if (StringUtils.isEmpty(packagePath)) {
            return "";
        }
        return ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
                + ClassUtils.convertClassNameToResourcePath(ENVIRONMENT.resolveRequiredPlaceholders(packagePath))
                + '/' + "**/*.class";
    }

    public static Set<Class<?>> getClazzFromAnnotation(String pkgPath, Class<? extends Annotation> annoClazz) {
        String pathPackage = getResourcePath(pkgPath);
        log.info("pathPackage: {}", pathPackage);
        Set<Class<?>> paths = new HashSet<>();
        Resource[] resources;
        try {
            resources = RESOLVER.getResources(pathPackage);
        } catch (IOException e) {
            return new HashSet<>();
        }
        for (Resource resource : resources) {
            MetadataReader metadataReader = null;
            try {
                metadataReader = REGISTER.getMetadataReader(resource);
            } catch (IOException e) {
                continue;
            }
            AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
            if (!annotationMetadata.hasAnnotation(annoClazz.getName())) {
                continue;
            }
            ClassMetadata classMetadata = metadataReader.getClassMetadata();
            String className = classMetadata.getClassName();
            try {
                ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
                Class<?> clazz = classLoader.loadClass(className);
                paths.add(clazz);
            } catch (ClassNotFoundException e) {
                log.warn(e.getMessage());
            }
        }
        return paths;
    }

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants