Skip to content

MethodInvokingFactoryBean fails to invoke publicly exported methods overridden by internal classes when using JPMS #34028

Open
@anandkarandikar

Description

@anandkarandikar

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

  1. This issue does not occur in Java 8 or when not using JPMS.
  2. The default behavior of MethodInvokingFactoryBean fails because the MethodInvoker attempts to access a method in MyLoggerContext via reflection, which is restricted due to Java Module System encapsulation.
  3. 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. The MethodInvoker#prepare() method handles property staticMethod and invokes the resolveClassName() 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 overridden reconfigure() method was called from MyLoggerContext class.

Ideally, staticMethod should not be used to invoke a non-static method.

  1. 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.


Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    in: coreIssues in core modules (aop, beans, core, context, expression)type: enhancementA general enhancement

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions