Skip to content

Commit 9024318

Browse files
noetrojimmyjames
andauthored
Improve JWT parse / decode performance (#620)
* Optimise parsing of token for well-defined JWT format * Update error message in test to match new code * Fixing checkstyle issues * Added missing test case for no parts * Return new JWTDecodeException Return a new JWTDecodeException from private utility method `wrongNumberOfParts`, instead of throwing, since we throw from `splitToken()`. * Add JMH support to build script * Add benchmark for decoder and cleanup build file * Optimise JWT deserialisation by re-using threadsafe Jackson objects * Disable lint checks on JMH source set that is for testing * Remove extra line break --------- Co-authored-by: Jim Anderson <jim.anderson@auth0.com> Co-authored-by: Jim Anderson <jimranderson@gmail.com>
1 parent 12ae664 commit 9024318

13 files changed

+181
-103
lines changed

lib/build.gradle

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,28 @@ plugins {
66
id 'checkstyle'
77
}
88

9+
sourceSets {
10+
jmh {
11+
12+
}
13+
}
14+
15+
configurations {
16+
jmhImplementation {
17+
extendsFrom implementation
18+
}
19+
}
20+
921
checkstyle {
1022
toolVersion '10.0'
11-
checkstyleTest.enabled = false //We are disabling lint checks for tests
1223
}
24+
//We are disabling lint checks for tests
25+
tasks.named("checkstyleTest").configure({
26+
enabled = false
27+
})
28+
tasks.named("checkstyleJmh").configure({
29+
enabled = false
30+
})
1331

1432
logger.lifecycle("Using version ${version} for ${group}.${name}")
1533

@@ -61,6 +79,10 @@ dependencies {
6179
testImplementation 'net.jodah:concurrentunit:0.4.6'
6280
testImplementation 'org.hamcrest:hamcrest:2.2'
6381
testImplementation 'org.mockito:mockito-core:4.4.0'
82+
83+
jmhImplementation sourceSets.main.output
84+
jmhImplementation 'org.openjdk.jmh:jmh-core:1.35'
85+
jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.35'
6486
}
6587

6688
jacoco {
@@ -143,3 +165,25 @@ task exportVersion() {
143165
new File(rootDir, "version.txt").text = "$version"
144166
}
145167
}
168+
169+
// you can pass any arguments JMH accepts via Gradle args.
170+
// Example: ./gradlew runJMH --args="-lrf"
171+
tasks.register('runJMH', JavaExec) {
172+
description 'Run JMH benchmarks.'
173+
group 'verification'
174+
175+
main 'org.openjdk.jmh.Main'
176+
classpath sourceSets.jmh.runtimeClasspath
177+
178+
args project.hasProperty("args") ? project.property("args").split() : ""
179+
}
180+
tasks.register('jmhHelp', JavaExec) {
181+
description 'Prints the available command line options for JMH.'
182+
group 'help'
183+
184+
main 'org.openjdk.jmh.Main'
185+
classpath sourceSets.jmh.runtimeClasspath
186+
187+
args '-h'
188+
}
189+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.auth0.jwt.benchmark;
2+
3+
import com.auth0.jwt.JWT;
4+
import org.openjdk.jmh.annotations.Benchmark;
5+
import org.openjdk.jmh.annotations.BenchmarkMode;
6+
import org.openjdk.jmh.annotations.Mode;
7+
import org.openjdk.jmh.infra.Blackhole;
8+
9+
/**
10+
* This class is a JMH benchmark for decoding JWTs.
11+
*/
12+
public class JWTDecoderBenchmark {
13+
private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
14+
15+
@Benchmark
16+
@BenchmarkMode(Mode.Throughput)
17+
public void throughputDecodeTime(Blackhole blackhole) {
18+
blackhole.consume(JWT.decode(TOKEN));
19+
}
20+
}

lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
import com.auth0.jwt.interfaces.Claim;
44
import com.auth0.jwt.interfaces.Header;
5+
import com.fasterxml.jackson.core.ObjectCodec;
56
import com.fasterxml.jackson.databind.JsonNode;
6-
import com.fasterxml.jackson.databind.ObjectReader;
77

88
import java.io.Serializable;
99
import java.util.Collections;
10-
import java.util.HashMap;
1110
import java.util.Map;
1211

1312
import static com.auth0.jwt.impl.JsonNodeClaim.extractClaim;
@@ -23,22 +22,22 @@ class BasicHeader implements Header, Serializable {
2322
private final String contentType;
2423
private final String keyId;
2524
private final Map<String, JsonNode> tree;
26-
private final ObjectReader objectReader;
25+
private final ObjectCodec objectCodec;
2726

2827
BasicHeader(
2928
String algorithm,
3029
String type,
3130
String contentType,
3231
String keyId,
3332
Map<String, JsonNode> tree,
34-
ObjectReader objectReader
33+
ObjectCodec objectCodec
3534
) {
3635
this.algorithm = algorithm;
3736
this.type = type;
3837
this.contentType = contentType;
3938
this.keyId = keyId;
40-
this.tree = Collections.unmodifiableMap(tree == null ? new HashMap<>() : tree);
41-
this.objectReader = objectReader;
39+
this.tree = tree == null ? Collections.emptyMap() : Collections.unmodifiableMap(tree);
40+
this.objectCodec = objectCodec;
4241
}
4342

4443
Map<String, JsonNode> getTree() {
@@ -67,6 +66,6 @@ public String getKeyId() {
6766

6867
@Override
6968
public Claim getHeaderClaim(String name) {
70-
return extractClaim(name, tree, objectReader);
69+
return extractClaim(name, tree, objectCodec);
7170
}
7271
}

lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
import com.auth0.jwt.HeaderParams;
44
import com.auth0.jwt.exceptions.JWTDecodeException;
5+
import com.auth0.jwt.interfaces.Header;
56
import com.fasterxml.jackson.core.JsonParser;
67
import com.fasterxml.jackson.core.type.TypeReference;
78
import com.fasterxml.jackson.databind.DeserializationContext;
89
import com.fasterxml.jackson.databind.JsonNode;
9-
import com.fasterxml.jackson.databind.ObjectReader;
1010
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
1111

1212
import java.io.IOException;
@@ -19,22 +19,14 @@
1919
*
2020
* @see JWTParser
2121
*/
22-
class HeaderDeserializer extends StdDeserializer<BasicHeader> {
22+
class HeaderDeserializer extends StdDeserializer<Header> {
2323

24-
private final ObjectReader objectReader;
25-
26-
HeaderDeserializer(ObjectReader objectReader) {
27-
this(null, objectReader);
28-
}
29-
30-
private HeaderDeserializer(Class<?> vc, ObjectReader objectReader) {
31-
super(vc);
32-
33-
this.objectReader = objectReader;
24+
HeaderDeserializer() {
25+
super(Header.class);
3426
}
3527

3628
@Override
37-
public BasicHeader deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
29+
public Header deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
3830
Map<String, JsonNode> tree = p.getCodec().readValue(p, new TypeReference<Map<String, JsonNode>>() {
3931
});
4032
if (tree == null) {
@@ -45,7 +37,7 @@ public BasicHeader deserialize(JsonParser p, DeserializationContext ctxt) throws
4537
String type = getString(tree, HeaderParams.TYPE);
4638
String contentType = getString(tree, HeaderParams.CONTENT_TYPE);
4739
String keyId = getString(tree, HeaderParams.KEY_ID);
48-
return new BasicHeader(algorithm, type, contentType, keyId, tree, objectReader);
40+
return new BasicHeader(algorithm, type, contentType, keyId, tree, p.getCodec());
4941
}
5042

5143
String getString(Map<String, JsonNode> tree, String claimName) {

lib/src/main/java/com/auth0/jwt/impl/JWTParser.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,21 @@
1616
* {@link HeaderSerializer} and {@link PayloadSerializer}.
1717
*/
1818
public class JWTParser implements JWTPartsParser {
19+
private static final ObjectMapper DEFAULT_OBJECT_MAPPER = createDefaultObjectMapper();
20+
private static final ObjectReader DEFAULT_PAYLOAD_READER = DEFAULT_OBJECT_MAPPER.readerFor(Payload.class);
21+
private static final ObjectReader DEFAULT_HEADER_READER = DEFAULT_OBJECT_MAPPER.readerFor(Header.class);
22+
1923
private final ObjectReader payloadReader;
2024
private final ObjectReader headerReader;
2125

2226
public JWTParser() {
23-
this(getDefaultObjectMapper());
27+
this.payloadReader = DEFAULT_PAYLOAD_READER;
28+
this.headerReader = DEFAULT_HEADER_READER;
2429
}
2530

2631
JWTParser(ObjectMapper mapper) {
2732
addDeserializers(mapper);
33+
2834
this.payloadReader = mapper.readerFor(Payload.class);
2935
this.headerReader = mapper.readerFor(Header.class);
3036
}
@@ -55,18 +61,24 @@ public Header parseHeader(String json) throws JWTDecodeException {
5561
}
5662
}
5763

58-
private void addDeserializers(ObjectMapper mapper) {
64+
static void addDeserializers(ObjectMapper mapper) {
5965
SimpleModule module = new SimpleModule();
60-
ObjectReader reader = mapper.reader();
61-
module.addDeserializer(Payload.class, new PayloadDeserializer(reader));
62-
module.addDeserializer(Header.class, new HeaderDeserializer(reader));
66+
module.addDeserializer(Payload.class, new PayloadDeserializer());
67+
module.addDeserializer(Header.class, new HeaderDeserializer());
6368
mapper.registerModule(module);
6469
}
6570

6671
static ObjectMapper getDefaultObjectMapper() {
72+
return DEFAULT_OBJECT_MAPPER;
73+
}
74+
75+
private static ObjectMapper createDefaultObjectMapper() {
6776
ObjectMapper mapper = new ObjectMapper();
6877
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
6978
mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
79+
80+
addDeserializers(mapper);
81+
7082
return mapper;
7183
}
7284

lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
import com.auth0.jwt.interfaces.Claim;
55
import com.fasterxml.jackson.core.JsonParser;
66
import com.fasterxml.jackson.core.JsonProcessingException;
7+
import com.fasterxml.jackson.core.ObjectCodec;
78
import com.fasterxml.jackson.core.type.TypeReference;
89
import com.fasterxml.jackson.databind.JsonNode;
9-
import com.fasterxml.jackson.databind.ObjectReader;
1010

1111
import java.io.IOException;
1212
import java.lang.reflect.Array;
@@ -21,12 +21,12 @@
2121
*/
2222
class JsonNodeClaim implements Claim {
2323

24-
private final ObjectReader objectReader;
24+
private final ObjectCodec codec;
2525
private final JsonNode data;
2626

27-
private JsonNodeClaim(JsonNode node, ObjectReader objectReader) {
27+
private JsonNodeClaim(JsonNode node, ObjectCodec codec) {
2828
this.data = node;
29-
this.objectReader = objectReader;
29+
this.codec = codec;
3030
}
3131

3232
@Override
@@ -82,7 +82,7 @@ public <T> T[] asArray(Class<T> clazz) throws JWTDecodeException {
8282
T[] arr = (T[]) Array.newInstance(clazz, data.size());
8383
for (int i = 0; i < data.size(); i++) {
8484
try {
85-
arr[i] = objectReader.treeToValue(data.get(i), clazz);
85+
arr[i] = codec.treeToValue(data.get(i), clazz);
8686
} catch (JsonProcessingException e) {
8787
throw new JWTDecodeException("Couldn't map the Claim's array contents to " + clazz.getSimpleName(), e);
8888
}
@@ -99,7 +99,7 @@ public <T> List<T> asList(Class<T> clazz) throws JWTDecodeException {
9999
List<T> list = new ArrayList<>();
100100
for (int i = 0; i < data.size(); i++) {
101101
try {
102-
list.add(objectReader.treeToValue(data.get(i), clazz));
102+
list.add(codec.treeToValue(data.get(i), clazz));
103103
} catch (JsonProcessingException e) {
104104
throw new JWTDecodeException("Couldn't map the Claim's array contents to " + clazz.getSimpleName(), e);
105105
}
@@ -113,11 +113,11 @@ public Map<String, Object> asMap() throws JWTDecodeException {
113113
return null;
114114
}
115115

116-
try {
117-
TypeReference<Map<String, Object>> mapType = new TypeReference<Map<String, Object>>() {
118-
};
119-
JsonParser thisParser = objectReader.treeAsTokens(data);
120-
return thisParser.readValueAs(mapType);
116+
TypeReference<Map<String, Object>> mapType = new TypeReference<Map<String, Object>>() {
117+
};
118+
119+
try (JsonParser parser = codec.treeAsTokens(data)) {
120+
return parser.readValueAs(mapType);
121121
} catch (IOException e) {
122122
throw new JWTDecodeException("Couldn't map the Claim value to Map", e);
123123
}
@@ -129,8 +129,8 @@ public <T> T as(Class<T> clazz) throws JWTDecodeException {
129129
if (isMissing() || isNull()) {
130130
return null;
131131
}
132-
return objectReader.treeAsTokens(data).readValueAs(clazz);
133-
} catch (IOException e) {
132+
return codec.treeToValue(data, clazz);
133+
} catch (JsonProcessingException e) {
134134
throw new JWTDecodeException("Couldn't map the Claim value to " + clazz.getSimpleName(), e);
135135
}
136136
}
@@ -160,21 +160,23 @@ public String toString() {
160160
*
161161
* @param claimName the Claim to search for.
162162
* @param tree the JsonNode tree to search the Claim in.
163+
* @param objectCodec the object codec in use for deserialization
163164
* @return a valid non-null Claim.
164165
*/
165-
static Claim extractClaim(String claimName, Map<String, JsonNode> tree, ObjectReader objectReader) {
166+
static Claim extractClaim(String claimName, Map<String, JsonNode> tree, ObjectCodec objectCodec) {
166167
JsonNode node = tree.get(claimName);
167-
return claimFromNode(node, objectReader);
168+
return claimFromNode(node, objectCodec);
168169
}
169170

170171
/**
171172
* Helper method to create a Claim representation from the given JsonNode.
172173
*
173174
* @param node the JsonNode to convert into a Claim.
175+
* @param objectCodec the object codec in use for deserialization
174176
* @return a valid Claim instance. If the node is null or missing, a NullClaim will be returned.
175177
*/
176-
static Claim claimFromNode(JsonNode node, ObjectReader objectReader) {
177-
return new JsonNodeClaim(node, objectReader);
178+
static Claim claimFromNode(JsonNode node, ObjectCodec objectCodec) {
179+
return new JsonNodeClaim(node, objectCodec);
178180
}
179181

180182
}

lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.auth0.jwt.interfaces.Payload;
66
import com.fasterxml.jackson.core.JsonParser;
77
import com.fasterxml.jackson.core.JsonProcessingException;
8+
import com.fasterxml.jackson.core.ObjectCodec;
89
import com.fasterxml.jackson.core.type.TypeReference;
910
import com.fasterxml.jackson.databind.DeserializationContext;
1011
import com.fasterxml.jackson.databind.JsonNode;
@@ -24,16 +25,8 @@
2425
*/
2526
class PayloadDeserializer extends StdDeserializer<Payload> {
2627

27-
private final ObjectReader objectReader;
28-
29-
PayloadDeserializer(ObjectReader reader) {
30-
this(null, reader);
31-
}
32-
33-
private PayloadDeserializer(Class<?> vc, ObjectReader reader) {
34-
super(vc);
35-
36-
this.objectReader = reader;
28+
PayloadDeserializer() {
29+
super(Payload.class);
3730
}
3831

3932
@Override
@@ -46,16 +39,17 @@ public Payload deserialize(JsonParser p, DeserializationContext ctxt) throws IOE
4639

4740
String issuer = getString(tree, RegisteredClaims.ISSUER);
4841
String subject = getString(tree, RegisteredClaims.SUBJECT);
49-
List<String> audience = getStringOrArray(tree, RegisteredClaims.AUDIENCE);
42+
List<String> audience = getStringOrArray(p.getCodec(), tree, RegisteredClaims.AUDIENCE);
5043
Instant expiresAt = getInstantFromSeconds(tree, RegisteredClaims.EXPIRES_AT);
5144
Instant notBefore = getInstantFromSeconds(tree, RegisteredClaims.NOT_BEFORE);
5245
Instant issuedAt = getInstantFromSeconds(tree, RegisteredClaims.ISSUED_AT);
5346
String jwtId = getString(tree, RegisteredClaims.JWT_ID);
5447

55-
return new PayloadImpl(issuer, subject, audience, expiresAt, notBefore, issuedAt, jwtId, tree, objectReader);
48+
return new PayloadImpl(issuer, subject, audience, expiresAt, notBefore, issuedAt, jwtId, tree, p.getCodec());
5649
}
5750

58-
List<String> getStringOrArray(Map<String, JsonNode> tree, String claimName) throws JWTDecodeException {
51+
List<String> getStringOrArray(ObjectCodec codec, Map<String, JsonNode> tree, String claimName)
52+
throws JWTDecodeException {
5953
JsonNode node = tree.get(claimName);
6054
if (node == null || node.isNull() || !(node.isArray() || node.isTextual())) {
6155
return null;
@@ -67,7 +61,7 @@ List<String> getStringOrArray(Map<String, JsonNode> tree, String claimName) thro
6761
List<String> list = new ArrayList<>(node.size());
6862
for (int i = 0; i < node.size(); i++) {
6963
try {
70-
list.add(objectReader.treeToValue(node.get(i), String.class));
64+
list.add(codec.treeToValue(node.get(i), String.class));
7165
} catch (JsonProcessingException e) {
7266
throw new JWTDecodeException("Couldn't map the Claim's array contents to String", e);
7367
}

0 commit comments

Comments
 (0)