-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Replace hardcoded classnames and reflection with ServiceLoader #496
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Originally I was very on-board for this PR and the idea of ServiceLoader
for JJWT's internal implementations.
However, I don't see the need for it for any of JJWT's internal implementations that shouldn't be modified or overridden by users (e.g. spec-compliant implementations).
Please see other comments regarding the SignatureAlgorithm interface change as well.
api/src/main/java/io/jsonwebtoken/lang/ImplementationNotFoundException.java
Outdated
Show resolved
Hide resolved
api/src/main/java/io/jsonwebtoken/lang/ImplementationNotFoundException.java
Outdated
Show resolved
Hide resolved
api/src/main/java/io/jsonwebtoken/security/KeyPairGenerator.java
Outdated
Show resolved
Hide resolved
622c65d
to
8fa13f5
Compare
@lhazlewood I've reduced the scope of this PR down to the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bdemers based on this, the DefaultCompressionCodecResolver
also needs to be updated to resolve any/all codecs found in the classpath. But please ensure that JJWT's Deflate
and Gzip
instances always take precedence for for values of (case-insensitive) ZIP
and DEF
. (In other words, even if a user specifies a codec with name zip
or gzip
, JJWT's instances will still be used because they reflect the RFC standard).
This ensures that if providing a custom compression codec by dropping one in the classpath, you wouldn't have to manually configure a CustomCodecResolver
on the parser (as is required today).
This change also requires us to update the JavaDoc for CompressionCodecResolver
and DefaultCompressionCodecResolver
to explain what happens by default, as well as update the README documentation here: https://github.com/jwtk/jjwt#compression
Also, IMO, this implies that we would probably have a .compressWith(String name)
method that allows the user to specify the codec name, and we look it up with a new utility method in CompressionCodecs
, e.g. CompressionCodecs.forName(String name)
. The lookup should be lazy IMO, and only assert that it actually exists during the build()
method (and not when compressWith
is called), per normal builder semantics (because compressWith
might be called multiple times via framework configuration code, and only the last value matters).
Thoughts?
Good call on the I like the ? |
hrm - you've gone through all this work, it'd feel like taking an unnecessary step back IMO if we don't just finish these few extra things. Ensuring JJWT's codecs are used first is pretty easy IMO, unless I'm missing something: In |
In fact, move lines 57-62 into |
Since the library, itself wouldn't be taking advantage of loading it's CompressionCodec's this way, I was questioning if we should continue down this path, and whether this change would be making this extensible just for the sake of extensibility. (NOTE: I have zero data on how often custom implementations are used Either way, adding an additional method to CompressionCodec resolveCompressionCodec(String name) throws CompressionException; This could be used for your proposed Thinking out loud. |
I'm suggesting to keep the logic that you've added (public static final variables with static class init block). I'm proposing this (simplified for brevity - needs javadoc comments, etc): private static final String DEFLATE_IMPL_CLASSNAME = "io.jsonwebtoken.impl.compression.DeflateCompressionCodec";
private static final String GZIP_IMPL_CLASSNAME = "io.jsonwebtoken.impl.compression.GzipCompressionCodec";
private static final Map<String,CompressionCodec> CODECS;
public static final CompressionCodec DEFLATE;
public static final CompressionCodec GZIP;
static {
CODECS = new HashMap<String,CompressionCodec>();
Collection<CompressionCodec> loaded = Services.loadAll(CompressionCodec.class);
for (CompressionCodec codec : loaded) {
String className = codec.getClass().getName();
if (DEFLATE_IMPL_CLASSNAME.equals(className)) {
DEFLATE = codec;
} else if (GZIP_IMPL_CLASSNAME.equals(className)) {
GZIP=codec;
}
String name = codec.getName();
// TODO: if !Strings.hasText(name) throw exception
name = name.toUpperCase() //guarantee canonical for map storage and lookup
CODECS.put(name, codec);
}
//TODO: if either DEFLATE or GZIP are null here, throw an IllegalStateException
// Now ensure JJWT's GZIP and DEFLATE always take precedence for RFC-standard names:
CODECS.put(DEFLATE.getName(), DEFLATE);
CODECS.put(GZIP.getName(), GZIP);
}
public static final CompressionCodec forName(String name) {
if (!Strings.hasText(name)) { ... throw exception ... }
String name = name.toUpperCase(); //normalize for map lookup
CompressionCodec codec = CODECS.get(name);
if (codec == null) { throw new UnsupportedCodecException("There is no codec with algorithm name " + name + ": Ensure jjwt-impl.jar or your custom implementation .jar are in the classpath according to the documentation at https://github.com/jwtk/jjwt#whatever." }
} |
Hrm - the reflection angle is super easy too. Maybe a hybrid approach makes sense: do reflection for our internal implementations (no serviceloader metadata) and allow custom CompressionCodecs to be added to the classpath for parser lookup? I like the reflection angle for our internal stuff actually, and serviceloader for plugins. I think that's super clean as we wouldn't conflict with any plugins, ever. |
So based on that last comment, how about this: private static final String DEFLATE_IMPL_CLASSNAME = "io.jsonwebtoken.impl.compression.DeflateCompressionCodec";
private static final String GZIP_IMPL_CLASSNAME = "io.jsonwebtoken.impl.compression.GzipCompressionCodec";
private static final Map<String,CompressionCodec> CODECS;
public static final CompressionCodec DEFLATE;
public static final CompressionCodec GZIP;
static {
CODECS = new HashMap<String,CompressionCodec>();
Collection<CompressionCodec> loaded = Services.loadAll(CompressionCodec.class);
for (CompressionCodec codec : loaded) {
String name = codec.getName();
// TODO: if !Strings.hasText(name) throw exception
name = name.toUpperCase() //guarantee canonical for map storage and lookup
CODECS.put(name, codec);
}
DEFLATE = Classes.newInstance(DEFLATE_IMPL_CLASSNAME);
CODECS.put(DEFLATE.getName, DEFLATE);
GZIP = Classes.newInstance(GZIP_IMPL_CLASSNAME);
CODECS.put(GZIP.getName(), GZIP);
}
public static final CompressionCodec forName(String name) {
if (!Strings.hasText(name)) { ... throw exception ... }
String name = name.toUpperCase(); //normalize for map lookup
CompressionCodec codec = CODECS.get(name);
if (codec == null) { throw new UnsupportedCodecException("There is no codec with algorithm name " + name + ": Ensure jjwt-impl.jar or your custom implementation .jar are in the classpath according to the documentation at https://github.com/jwtk/jjwt#whatever." }
} |
17cd810
to
e78acaa
Compare
@lhazlewood getting back to this. |
impl/src/test/groovy/io/jsonwebtoken/impl/io/RuntimeClasspathSerializerLocatorTest.groovy
Outdated
Show resolved
Hide resolved
impl/src/main/resources/META-INF/services/io.jsonwebtoken.JwtFactory
Outdated
Show resolved
Hide resolved
e78acaa
to
975bf3e
Compare
a couple more minor tweaks found in a quick self review |
impl/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java
Outdated
Show resolved
Hide resolved
impl/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java
Show resolved
Hide resolved
impl/src/main/java/io/jsonwebtoken/impl/io/RuntimeClasspathDeserializerLocator.java
Outdated
Show resolved
Hide resolved
impl/src/main/java/io/jsonwebtoken/impl/io/RuntimeClasspathSerializerLocator.java
Outdated
Show resolved
Hide resolved
So will this all work with JDK9+ modules now? |
@skjolber the big blocker for 9+ was the split package issue in: #488 If you run into anything specific let us know! We haven't added module-info's as we are still technically testing/building on 1.7 |
2b3320c
to
426d348
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some more comments. I love seeing deleted code! 😄
api/src/main/java/io/jsonwebtoken/lang/UnavailableImplementationException.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice stuff @bdemers !
I have just a teeny few nits remaining - only because I'm a little paranoid about any changes in the api/src/main
location due to its exposure to (now) thousands of projects. Once they're done, we're good to merge!
api/src/main/java/io/jsonwebtoken/lang/UnavailableImplementationException.java
Outdated
Show resolved
Hide resolved
impl/src/main/java/io/jsonwebtoken/impl/BackwardCompatibilityUtil.java
Outdated
Show resolved
Hide resolved
418192a
to
2a6feaf
Compare
api/src/main/java/io/jsonwebtoken/lang/UnavailableImplementationException.java
Show resolved
Hide resolved
All refs to |
…sses. By using ServiceLoader the hardcoded dependency of implementation classes becomes obsolete, so that the API will be truly independent from the implementation. Also this approach paves the way for migration to JPMS modules, as these also leverage the ServiceLoader API. Use ServiceLoader instead of reflection to resolve CompressionCodec implementation classes. Isolate key- and key-pair generators and use ServiceLoader instead of reflection to invert dependencies. Move FactoryLoader logic to Services class and improve package layout. Resolve Deserializer using the ServiceLoader instead of reflection and hardcoded reference. Resolve Serializer using the ServiceLoader instead of reflection and hardcoded reference.
2a6feaf
to
7037d64
Compare
@jaapcoomans I'm dusting this off, sorry for the delay #458
I love the
FakeServiceDescriptorClassLoader
trick!TODO:
Deprecate theClasses
util class? (not sure if this is possible given the last two remaining usages of this class:IMHO, that first item does NOT block this issue, but I'd like to hear if anyone has any thoughts on the topic.