Description
Description
We encountered an issue while using Spring Framework (v5.3.29) in a Java 17 modular application. The problem arises when trying to invoke a public method from an exported class that is overridden by another class (within an internal package) using Spring's MethodInvokingFactoryBean
. The method is not accessible due to Java Module System restrictions. This issue is reproducible in Java 17 and does not occur in Java 8.
(Tested with: 5.3.29, 5.3.39, 6.2.0)
Steps to reproduce
Start with a simple maven project and choose Java 17 as the preferred JDK
1. Dependencies (pom.xml):
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.29</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
2. Custom Implementation
Extending org.apache.logging.log4j.core.LoggerContext
to create a custom logger context class:
package com.example.impl;
import org.apache.logging.log4j.core.LoggerContext;
public class MyLoggerContext extends LoggerContext {
public MyLoggerContext(String name) {
super(name);
System.out.println("Initializing LoggerContext " + name);
}
@Override
public void reconfigure() {
super.reconfigure();
System.out.println("Called reconfigure with " + getName());
}
}
3. Factory Class
A factory class LoggerContextFactory
that provides an instance of MyLoggerContext
:
package com.example.api;
import com.example.impl.MyLoggerContext;
import org.apache.logging.log4j.core.LoggerContext;
public class LoggerContextFactory {
public static LoggerContext createLoggerContext() {
return new MyLoggerContext("TestLoggerContext");
}
}
4. Module Configuration
The module-info.java
file exports only the com.example.api
package, keeping com.example.impl
internal:
module CustomLoggerContext {
requires org.apache.logging.log4j.core;
requires spring.context;
exports com.example.api;
}
5. Spring Configuration
Defined in springApplicationContext.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="loggerContextFactory" class="com.example.api.LoggerContextFactory"/>
<bean id="loggerContextInstance" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject" ref="loggerContextFactory"/>
<property name="targetMethod" value="createLoggerContext"/>
</bean>
<bean id="invokeReconfigureMethod" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject" ref="loggerContextInstance"/>
<property name="targetMethod" value="reconfigure"/>
</bean>
</beans>
6. MainClass
Used to initialize the Spring ApplicationContext
:
package com.example.runner;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainClass {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("springApplicationContext.xml");
System.out.println("Successful");
}
}
Execution Output / Stacktrace
Initializing LoggerContext TestLoggerContext
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'invokeReconfigureMethod' defined in class path resource [springApplicationContext.xml]: Invocation of init method failed; nested exception is java.lang.IllegalAccessException: class org.springframework.util.MethodInvoker cannot access class com.example.impl.MyLoggerContext (in module CustomLoggerContext) because module CustomLoggerContext does not export com.example.impl to unnamed module @277c0f21
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1804)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:936)
at spring.context@5.3.29/org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:921)
at spring.context@5.3.29/org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
at spring.context@5.3.29/org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:144)
at spring.context@5.3.29/org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:85)
at CustomLoggerContext/com.example.runner.MainClass.main(MainClass.java:7)
Caused by: java.lang.IllegalAccessException: class org.springframework.util.MethodInvoker cannot access class com.example.impl.MyLoggerContext (in module CustomLoggerContext) because module CustomLoggerContext does not export com.example.impl to unnamed module @277c0f21
at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:392)
at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:674)
at java.base/java.lang.reflect.Method.invoke(Method.java:561)
at org.springframework.util.MethodInvoker.invoke(MethodInvoker.java:283)
at org.springframework.beans.factory.config.MethodInvokingBean.invokeWithTargetException(MethodInvokingBean.java:123)
at org.springframework.beans.factory.config.MethodInvokingFactoryBean.afterPropertiesSet(MethodInvokingFactoryBean.java:108)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800)
... 12 more
Process finished with exit code 1
Observations
- This issue does not occur in Java 8 or when not using JPMS.
- The default behavior of
MethodInvokingFactoryBean
fails because theMethodInvoker
attempts to access a method inMyLoggerContext
via reflection, which is restricted due to Java Module System encapsulation. - A workaround exists, but it requires an undesirable modification:
Original Bean Definition:
<bean id="invokeReconfigureMethod" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="targetObject" ref="loggerContextInstance"/> <property name="targetMethod" value="reconfigure"/> </bean>Modified Bean Definition with staticMethod:
<bean id="invokeReconfigureMethod" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="targetObject" ref="loggerContextInstance"/> <property name="staticMethod" value="org.apache.logging.log4j.core.LoggerContext.reconfigure"/> </bean>This workaround works because we are specifying the class name to take the method from to be the exported one (
LoggerContext
) instead of the internal one. TheMethodInvoker#prepare()
method handles property staticMethod and invokes theresolveClassName()
method that seems to be helping in this case.Execution Output/Stacktrace with the workaround
Initializing LoggerContext TestLoggerContext Called reconfigure with TestLoggerContext Successful
The output line
Called reconfigure with TestLoggerContext
signifies that overriddenreconfigure()
method was called fromMyLoggerContext
class.
Ideally, staticMethod should not be used to invoke a non-static method.
- The inability to invoke public methods in parent classes/interfaces just because they are overridden by internal classes creates a limitation. We have customers that have been using this kind of Spring configurations for a long time, and now they are facing a regression when they upgrade their Java version to one that supports JPMS. Even though we have the option to provide the workaround, this is coming as an unexpected breaking change for them.
Proposed Fix:
A similar issue was addressed previously in Spring EL (SpEL) by enhancing the ReflectivePropertyAccessor
to locate accessor methods in public interfaces and public (super-)classes, defaulting to current logic if not found. (See #21385).
We suggest implementing a comparable fix for Spring's MethodInvokingFactoryBean
in the bean creation process to handle such cases.