Skip to content

Rework resolving the default for Context.INITIAL_CONTEXT_FACTORY #579

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

Closed
wants to merge 2 commits into from
Closed

Rework resolving the default for Context.INITIAL_CONTEXT_FACTORY #579

wants to merge 2 commits into from

Conversation

iherasymenko
Copy link
Contributor

A follow up for #568, #570, #543.

With this approach in place, one will be able to specify a different implementation using the setContextFactory method in case com.sun.jndi.ldap.LdapCtxFactory gets removed one day (since this class is not exported by the java.naming modules in JDK 9+ and is an implementation detail).

Having the contextFactory field initialized as follows would lead to the ExceptionInInitializerError which users would not be able to overcome.
private static final Class<?> DEFAULT_CONTEXT_FACTORY = org.springframework.util.ClassUtils.resolveClassName("com.sun.jndi.ldap.LdapCtxFactory", null)

@Yberion
Copy link

Yberion commented Apr 15, 2021

Hello,

Will this PR fix Java 16 error ? (I'm using gradle 7)

SbtemplateApplicationTests > contextLoads() FAILED
    java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:132
        Caused by: org.springframework.beans.factory.BeanCreationException at ConstructorResolver.java:658
            Caused by: org.springframework.beans.BeanInstantiationException at SimpleInstantiationStrategy.java:185
                Caused by: java.lang.IllegalAccessError at AbstractContextSource.java:77

I tried few versions and this error appear on every of them :

  • 2.3.9.BUILD-SNAPSHOT
  • 2.4.5-SNAPSHOT
  • 2.4.6-SNAPSHOT
  • 2.5.0-SNAPSHOT

Thank you.

@iherasymenko
Copy link
Contributor Author

iherasymenko commented Apr 15, 2021

@Yberion yes, it will.

You can try out a snapshot build from jitpack.

https://jitpack.io/#iherasymenko/spring-ldap/java-9-and-above-compatibility-2.3.2.RELEASE-g87d79f1-49

@bryant1410
Copy link
Contributor

?

@iherasymenko
Copy link
Contributor Author

@bryant1410 sorry, I tagged you accidentally.

@Yberion
Copy link

Yberion commented Apr 15, 2021

Thank you for the answer.

repositories {
	...
	maven {
	    url 'https://jitpack.io'
    }
}
dependencies {
        ...
	implementation 'com.github.iherasymenko:spring-ldap:java-9-and-above-compatibility-SNAPSHOT'
}

Fixed it, waiting for the official release ! :)

Copy link
Collaborator

@jzheaux jzheaux left a comment

Choose a reason for hiding this comment

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

Thanks for the PR, @iherasymenko! Please see my comments inline.

try {
this.contextFactory = Class.forName("com.sun.jndi.ldap.LdapCtxFactory");
} catch (ClassNotFoundException e) {
LOG.error("Cannot resolve com.sun.jndi.ldap.LdapCtxFactory. contextFactory must be explicitly specified", e);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This isn't necessarily an error since the application may call setContextFactory later. Would this work be better in the afterPropertiesSet method? Then, you would be able to state definitively that the class is correctly or incorrectly configured and throw an exception accordingly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed the logging level to TRACE and added the validation to the afterPropertiesSet method. I did not completely move the resolution to the afterPropertiesSet method since having it there would result in the same behavior change described here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you elaborate on how the same behavior would result? It would be an error to use this class before calling afterPropertiesSet.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

elaborated below.

Ihor Herasymenko added 2 commits April 16, 2021 17:17
…hat the method `getContextFactory()` returns the assigned default

With this approach in place, one will be able to specify a different implementation using the setContextFactory method should `com.sun.jndi.ldap.LdapCtxFactory` gets removed one day (since this class is not exported by the `java.naming` modules in JDK 9+ and is an implementation detail).
Copy link
Collaborator

@jzheaux jzheaux left a comment

Choose a reason for hiding this comment

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

Thanks for the quick turn around, @iherasymenko! Please see an additional comment in my review.

private DirContextAuthenticationStrategy authenticationStrategy = new SimpleDirContextAuthenticationStrategy();

public AbstractContextSource() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this should be moved into afterPropertiesSet, too, so that it's all in one place.

For example, in afterPropertiesSet you would do:

if (this.contextFactory == null) {
    try {
        this.contextFactory = Class.forName(DEFAULT_CONTEXT_FACTORY);
    } catch (ClassNotFoundException e) {
        throw new IllegalArgumentException("Could not find " + DEFAULT_CONTEXT_FACTORY + " and no context factory specified");
    }
}

One thing that's nice about this is that the ClassNotFoundException is not generated unless no context factory is specified. Since exceptions are expensive to generate, it's nice when they can be avoided.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was my original idea, yes. However, there will be a slight behavioral change compared to 2.3.3.

LdapContextSource ctx = new LdapContextSource();
// afterPropertiesSet is not required.
Class<?> contextFactory = ctx.getContextFactory(); // Version 2.3.3 would return com.sun.jndi.ldap.LdapCtxFactory

whereas with the change applied

LdapContextSource ctx = new LdapContextSource();
ctx.afterPropertiesSet(); // required otherwise the below will return a null value
Class<?> contextFactory = ctx.getContextFactory(); 

Please let me know if that is acceptable and I will update the PR.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Would it alleviate your concern if the PR also changed to run this logic in a private getter?

For example, you could add:

private Class<?> ensureContextFactoryName() {
    if (this.contextFactory != null) {
        return this.contextFactory.getName();
    }
    try {
        this.contextFactory = Class.forName(DEFAULT_CONTEXT_FACTORY);
    } catch (ClassNotFoundException e) {
        throw new IllegalArgumentException("Could not find " + DEFAULT_CONTEXT_FACTORY + " and no context factory specified");
    }
    return this.contextFactory.getName();
}

Then, in afterPropertiesSet, you'd do:

ensureContextFactoryName();

And in setupAnonymousEnv:

env.put(Context.INITIAL_CONTEXT_FACTORY, ensureContextFactoryName());

Choose a reason for hiding this comment

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

Why not just initialize the DEFAULT_CONTEXT_FACTORY in the static code block and do not make any other changes?

    private static final Class<?> DEFAULT_CONTEXT_FACTORY;
    static {
        try {
            DEFAULT_CONTEXT_FACTORY = Class.forName("com.sun.jndi.ldap.LdapCtxFactory");
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

Copy link
Collaborator

Choose a reason for hiding this comment

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

That's problematic since it errors on something that may not be a problem. The class not being present is only a problem if the application does not set a context factory.

Choose a reason for hiding this comment

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

@jzheaux The com.sun.jndi.ldap.LdapCtxFactory class is always present but not always could be accessed. The ClassNotFoundException will never be thrown and catching it is just because it's a checked exception. In other words the DEFAULT_CONTEXT_FACTORY will always be present and so will be set the contextFactory field by default as well.
Following code example works on Java 16 properly:

public class MainClass {
    private static final Class<?> DEFAULT_CONTEXT_FACTORY;
    static {
        try {
            DEFAULT_CONTEXT_FACTORY = Class.forName("com.sun.jndi.ldap.LdapCtxFactory");
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        System.out.println(DEFAULT_CONTEXT_FACTORY.getName());
    }
}

Value of the DEFAULT_CONTEXT_FACTORY is used in the AbstractContextSource only for two things:

  1. return value of getContextFactory()
  2. env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory.getName());

With my version of change all of the above will continue to work properly and no behavior change will be introduced.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I see. It's interesting that Class.forName works when the direct reference does not. Given that, the PR's constructor won't throw the exception in normal JDK 16 circumstances.

@jzheaux jzheaux closed this in 0f30b05 May 4, 2021
@jzheaux
Copy link
Collaborator

jzheaux commented May 4, 2021

Thanks, @iherasymenko for the PR! Based on this comment, it appears that the extra work to delay the thrown exception is needless.

I squashed your two commits adjusted the copyright header to be 2021, resulting in 0f30b05

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

Successfully merging this pull request may close these issues.

5 participants