#ComponentScan does not detect classes in module path - java

I'm working on a JavaFX app with Java 11 and Spring. The app module is bundled with a custom JRE using jlink, which only allows named modules to be included in the bundle. Since Spring doesn't provide named modules but relies on automatic modules to achieve Java 9 Module System support, I use moditect to add module descriptors (module-info.java) to the Spring JARs.
Compiling, jlinking and executing the app works without any problems. However, Spring does not detect any of my app's classes annotated with #Component, despite my AppConfig class is annotated with #ComponentScan:
#Configuration
#ComponentScan
public class AppConfig {
}
In Main, I create an AnnotationConfigApplicationContext based on AppConfig and print all registered beans as well as the resources available on class path:
public class Main extends Application {
private ConfigurableApplicationContext applicationContext;
public static void main(String[] args) {
launch(args);
}
#Override
public void init() {
applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
}
#Override
public void start(Stage mainWindow) throws IOException {
System.out.println("Printing beans: " + applicationContext.getBeanDefinitionNames().length);
for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
System.out.println(Arrays.toString(resolver.getResources("classpath*:com/myapp/**/*.class")));
}
#Override
public void stop() {
applicationContext.stop();
}
}
If I run the app using IntelliJ, PathMatchingResourcePatternResolver finds all my classes on the class path (I guess because IntelliJ runs the app using the class path, not the module path). Consequently, all components are detected via component scan and the respective beans are created:
Printing beans: 8
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalPersistenceAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
appConfig
com.myapp.services.UserServiceImpl
com.myapp.services.BookingServiceImpl
[file [/Users/user/myapp/target/classes/com/myapp/AppConfig.class], file [/Users/user/myapp/target/classes/com/myapp/Main.class], file [/Users/user/myapp/target/classes/com/myapp/services/UserService.class], file [/Users/user/myapp/target/classes/com/myapp/services/UserServiceImpl.class], file [/Users/user/myapp/target/classes/com/myapp/services/BookingService.class], file [/Users/user/myapp/target/classes/com/myapp/services/BookingServiceImpl.class]]
But if I run the app via the jlink'ed bundle, i.e. on the custom JRE using the module path, Spring is unable to detect any of my classes:
Printing beans: 5
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
appConfig
[]
PathMatchingResourcePatternResolver does not find any classes (because everything is now located on the module path), and not a single bean is instantiated by the component scan.
If I import the component classes manually into AppConfig, the beans are created properly and also injected via #Autowired:
#Configuration
#Import({
com.myapp.service.UserServiceImpl.class,
com.myapp.service.BookingServiceImpl.class
})
public class AppConfig {
}
Why is Spring able to create the beans when using #Import, but does not detect them via #ComponentScan? How can I resolve my components via #ComponentScan?

Another possible solution than the one provided by #IggyBlob in the question's comments is to patch PathMatchingResourcePatternResolver to search resources into the module path at least until a fully module compliant Spring version is released.
One possible implementation:
public class PathMatchingResourcePatternResolverJigsaw extends PathMatchingResourcePatternResolver {
public PathMatchingResourcePatternResolverJigsaw() {
}
public PathMatchingResourcePatternResolverJigsaw(ResourceLoader resourceLoader) {
super(resourceLoader);
}
public PathMatchingResourcePatternResolverJigsaw(ClassLoader classLoader) {
super(classLoader);
}
public List<Resource> getResourcesFromModules(String locationPattern) throws IOException {
String pattern = locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length());
List<Resource> list = new ArrayList<>();
ModuleLayer.boot().configuration().modules().stream()
.map(ResolvedModule::reference)
.forEach(mref -> {
try (ModuleReader reader = mref.open()) {
list.addAll(reader.list()
.filter(p -> getPathMatcher().match(pattern, p))
.map(p -> {
try {
return convertClassLoaderURL(reader.find(p).get().toURL());
} catch (Exception e) {
return null;
}
})
.collect(Collectors.toList()));
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
});
return list;
}
#Override
public Resource[] getResources(String locationPattern) throws IOException {
boolean addModSearch = true;
Resource[] result = super.getResources(locationPattern);
if (addModSearch && locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
List<Resource> list = getResourcesFromModules(locationPattern);
list.addAll(Arrays.asList(result));
result = list.toArray(new Resource[0]);
}
return result;
}
}
And initialize your spring appllication context with:
private ConfigurableApplicationContext context;
#Override
public void init() throws Exception {
ApplicationContextInitializer<GenericApplicationContext> initializer = new ApplicationContextInitializer<GenericApplicationContext>() {
#Override
public void initialize(GenericApplicationContext genericApplicationContext) {
genericApplicationContext.setResourceLoader(new PathMatchingResourcePatternResolverJigsaw());
}
};
this.context = new SpringApplicationBuilder().sources(MyApplication.class)
.initializers(initializer)
.build().run(new String[0]);
}
This is only an ugly workaround, so use it with caution

Related

Using Netflix Ribbon without Spring Boot in Legacy Application

I work at an application which is using Apache Mina as SFTP Server. The application itself is started as jar and sends rest requests to our backend.
I now want to use Netflix Ribbon without turning the whole application into a spring boot project or spring project in general.
My approach is to access the api directly like in the example:
public class MyClass {
#Autowired
private LoadBalancerClient loadBalancer;
public void doStuff() {
ServiceInstance instance = loadBalancer.choose("stores");
URI storesUri = URI.create(String.format("http://%s:%s", instance.getHost(), instance.getPort()));
// ... do something with the URI
}
}
Examples in the documentation only show how it is done if configuration is done by spring automatically. However this is not working for me and I cannot get spring to automatically provide the loadbalancer bean.
I solved the problem by "hardcoding" the spring parts:
#Configuration
public class LoadbalancerConfig {
#Bean
public ILoadBalancer loadBalancer() {
BaseLoadBalancer baseLoadBalancer = new BaseLoadBalancer("balancer", rule(), new LoadBalancerStats("balancer"));
baseLoadBalancer.addServers(serverList().getInitialListOfServers());
return baseLoadBalancer;
}
#Bean
public IRule rule() {
return new RandomRule();
}
#Bean
public ServerList<Server> serverList() {
return new StaticServerList<>((new Server("host1", 80)),
new Server("host2", 80));
}
}
Util class for getting bean at later point:
public class BeanUtil implements ApplicationContextAware {
private static final Logger log = LogManager.getLogger(BeanUtil.class);
private static ApplicationContext applicationContext;
#Override
public void setApplicationContext(final ApplicationContext ctx) throws BeansException {
applicationContext = ctx;
}
public static <T> T getBean(Class<T> beanClass) {
return applicationContext.getBean(beanClass);
}
}
I initiate them through a xml file:
<context:component-scan base-package="package.of.loadbalancerconfig" />
<bean id="applicationContextProvider" lazy-init="false" class="my.package.BeanUtil" />
Dont forget to create your applicationContext at initialization:
ApplicationContext context = new FileSystemXmlApplicationContext("file:/path/to/beans.xml");
Now I could get the loadbalancer and the instances:
if (loadBalancer == null) {
loadBalancer = BeanUtil.getBean(ILoadBalancer.class);
}
Server instance = loadBalancer.chooseServer("balancer");
URI uri = URI.create(String.format("http://%s:%s", instance.getHost(), instance.getPort()));
I'm sure there is a more elegant way, but it worked for me.

Failed to set value with application.properties

I'm trying a very simple code to inject a value from application.properties.
The value which is setted is the property name.
What's wrong with the code?
application.properties
set.browser = ie
public class A {
#Value("${set.browser}")
private String browser;
public A(){}
public void print(){
System.out.println(browser);
}
}
#Configuration
public class ABean {
#Bean
public A getA(){
return new A();
}
}
public class AMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(A.class);
A a = context.getBean(A.class);
a.print();
}
}
First of all your application is not spring boot application - you've just instanted spring context even without component scan. Secondly beacause of lack of component scan, your ABean is never created - your context has only A bean. To fix this you can create context from ABean:
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ABean.class);
Thirdly you didn't configured PropertySource (if your application was spring boot application, application.properties would be default property source and it wouldn't be needed):
#PropertySource("classpath:/application.properties")
#Configuration
class ABean {
#Bean
public A getA() {
return new A();
}
}

#Autowired component is not available

My class is an sftp poller. It implements DirectoryListener and implements the fileAdded method to monitor when new events are added to an sftp directory. Code looks like
#SpringBootApplication
#Slf4j
public class SftpBridge implements DirectoryListener, IoErrorListener, InitialContentListener {
#Autowired
private SftpBridgeConfig config;
#Autowired
public SftpDirectory sftpDirectory;
public static void main(final String[] args) throws Exception {
SpringApplication.run(SftpBridge.class, args);
}
#PostConstruct
public void postConstruct() {
LOG.info("Initializing...");
initialize();
LOG.info("Initialized!");
}
private void initialize() {
pollSftp();
}
public void pollSftp() {
try {
while (true) {
LOG.info("monitoring directory: " + "/");
PolledDirectory polledDirectory = sftpDirectory;
DirectoryPoller dp = DirectoryPoller.newBuilder()
.addPolledDirectory(polledDirectory)
.addListener(new SftpBridge())
// other settings
//remove this later
.enableFileAddedEventsForInitialContent() // optional (disabled by default). FileAddedEvents fired for directories initial content.
//TODO: enable later for subdirectory polling
//.enableParallelPollingOfDirectories() // optional (disabled by default).
.setDefaultFileFilter(new RegexFileFilter(".*csv")) // optional. Only consider files ending with "xml".
.setThreadName("sftp-poller") // sets the name of the the polling thread
.setPollingInterval(10, TimeUnit.SECONDS)
.start();
TimeUnit.HOURS.sleep(2);
dp.stop();
}
} catch (final Exception e) {
LOG.error("Error monitoring ftp host", e);
}
}
Since pollSftp() is called by initialize() during Spring boot application init, it is able to see the #Autowired component SftpBridgeConfig config.
My problem is that my class implements DirectoryListener, I have to override the fileAdded event to take some action when a new ftp file is added.
#Override
public void fileAdded(FileAddedEvent event) {
LOG.info("Added: " + event.getFileElement());
//implementing DirectoryListener
//#Autowired component config is null here as it is called from a polling thread
}
in the fileAdded(FileAddedEvent event) method, my #Autowired component config is null, because this method is not called during Spring boot init. What is the best way to structure the code so that the #Autowired component config is available when fileAdded() is called by an sftp directory polling thread?
Thanks for any advice.
Edit: #Andreas - I've filled out my pollSftp() method which adds the class as a DirectoryListener. Thanks
Is there an annotaion in SftpBridgeConfig?
cf. #Service #Component
if configuration class, need to register #Bean in the spring context.
cf. #Bean(name="") or #Bean
ex) message source configuration
#Configuration
public class MessageSourceConfiguration {
#Bean
public MessageSource messageSource() {
// statements
}
}

Externalize properties and logback Spring

I'm using Spring (without spring-boot). I want to build standalone application that can be run with default configuration (logback.xml and application.properties in resource folder) or with -Dconfig.folder=/path/to/custom/external/directory
(logback.xml and application.properties in /path/to/custom/external/directory). When application will be run with -Dconfig.folder param AppConfig should load both logback and properties from external directory.
Is there anyway to make external folder act like a resource folder?
If not, what is a common solution for this?
My current implementation (using default resource folder only):
App.java
public class App {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
SampleAction p = context.getBean(SampleAction.class);
p.performTask();
}
}
AppConfig.java
#ComponentScan
#PropertySource("classpath:application.properties")
class AppConfig {
#Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
SampleAction.java
#Component
public class SampleAction {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
#Value("${sample.prop}")
private String sampleProp;
public void performTask(){
logger.debug(sampleProp);
}
}
logback.xml and application.properties are not relevant to the problem
Unlike the other answer suggests, if you use file prefix in #PropertySource, you're screwed because it won't be able to load the default application.properties from the jar. What you should do is the following:
#PropertySource("${config.folder:'classpath:'}/application.properties")
public class AppConfig
For logback.xml:
#Value("${config.folder}:")
private String configFolder;
InputStream = Optional.of(new ClassPathResource(configFolder + "/logback.xml"))
.filter(r -> r.exists())
.orElse(new ClassPathResource("classpath:/logback.xml"))
.getInputStream();
In both cases, I gave preference to the command line argument over the default packaged files. Of course, I didn't compile the above, so there may be typos or minor errors, but you get the idea.
Edit:
Since OP claims to not understand where to run the above code -
public class AppConfig {
#PostConstruct
void init() {
// init logback here
}
}
For log4j.xml
-Dlog4j.configuration=C:\neon\log4j.xml as VM argument
In main() method:
String filename = System.getProperty("log4j.configuration");
DOMConfigurator.configure(filename);
For external properties file:
-Dext.prop.dir=C:\neon as VM argument
Change in your AppConfig class will be like
#PropertySource("file:///${ext.prop.dir}/application.properties")
public class AppConfig{
}
Run App class with VM arguments as below and both file will be use from external location
-Dlog4j.configuration=C:\neon\log4j.xml -Dext.prop.dir=C:\neon

Can I create multiple entry points to a Spring Boot app?

In Spring Boot, a main class needs to be specified, which is the entry point to the app. Typically this is a simple class with a standard main method, as follows;
#SpringBootApplication
public class MySpringApplication {
public static void main(String [] args) {
SpringApplication.run(MySpringApplication.class, args);
}
}
This class is then specified as the main entry point when the application runs.
However, I want to run my code using a different main class using config to define this, And without using a different jar!! (I know rebuilding the jar will enable me to specify an alternative main class, but this effectively gives me two apps, not one! So, how can I do this to utilise one jar with two main classes and select the one to use via the Spring application.yml file?
I found an answer - use the CommandLineRunner interface...
So now I have two classes;
public class ApplicationStartupRunner1 implements CommandLineRunner {
#Override
public void run(String... args) throws Exception {
//implement behaviour 1
}
and
public class ApplicationStartupRunner2 implements CommandLineRunner {
#Override
public void run(String... args) throws Exception {
//implement behaviour 2
}
and how to switch between them in config..
#Configuration
public class AppConfig {
#Value("${app.runner}")
private int runner;
#Bean
CommandLineRunner getCommandLineRunner() {
CommandLineRunner clRunner = null;
if (runner == 1) {
clRunner = new ApplicationStartupRunner1();
} (else if runner == 2) {
clRunner = new ApplicationStartupRunner2();
} else {
//handle this case..
}
return clRunner;
}
}
and finally in the application.properties file, use
app.runner=1
I would stick with the original pattern of having just one main class and main method, however within that method you could configure where you want to go. I.e. rather than having 2 main methods and configuring which gets called make these 2 methods just normal methods, and create one main method which uses the config to determine which of your two methods get run.
The answer of jonny.l is fine.
Another very similar, but more manual/DIY solution is to get the ApplicationContext, from which you can get all other things:
#SpringBootApplication
public class App {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(App.class);
ConfigurableEnvironment env = context.getBean(ConfigurableEnvironment.class);
String mainApp = env.getProperty("app.runner");
if (mainApp.equals("Main1")) {
Main1 main1 = context.getBean(Main1.class);
main1.run();
} else if (mainApp.equals("Main2")) {
Main2 main2 = context.getBean(Main2.class);
main2.run();
}
}
}
#Service #Lazy
public class Main1 {
public void run() {}
}
#Service #Lazy
public class Main2 {
public void run() {}
}
#Lazyis used to prevent those beans from loading unnecessarily automatically.

Categories

Resources