Description
ConcurrentModificationException Diagram:
Summary:
Hello.
-ConcurrentModificationException
occurs when one thread is modifying a collection with some interceptors, and another thread is iterating the same data structure with the elements in the context of the topic "ConcurrentModificationException Spring Framework Rest Template Setting Interceptors".
-The following code is not thread-safe and developers can potentially use the following code in a not thread-safe way.
-The currently shown source code allows thread-safe misusage, logging spikes, degraded performance and security risks.
-Nothing protects developers from abusing the Spring Framework to be used in an anti-design pattern on production servers.
-We can pass a synchronized collection hypothetically. The method responsible for setting the interceptors returns response always as an unsynchronized collection. The setInterceptors
function processes the elements of the data structure we pass in a not thread-safe manner using copy action of elements' references, sorting the interceptors redundantly.
Note:
https://docs.oracle.com/javase/7/docs/api/java/util/ConcurrentModificationException.html
"Therefore, it would be wrong to write a program that depended on this exception for its correctness: ConcurrentModificationException should be used only to detect bugs."
public abstract class InterceptingHttpAccessor extends HttpAccessor {
private final List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
...
public void setInterceptors(List<ClientHttpRequestInterceptor> interceptors) {
Assert.noNullElements(interceptors, "'interceptors' must not contain null elements");
// Take getInterceptors() List as-is when passed in here
if (this.interceptors != interceptors) {
this.interceptors.clear();
this.interceptors.addAll(interceptors);
AnnotationAwareOrderComparator.sort(this.interceptors);
}
}
...
}
class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest {
private final ClientHttpRequestFactory requestFactory;
private final List<ClientHttpRequestInterceptor> interceptors;
...
private class InterceptingRequestExecution implements ClientHttpRequestExecution {
private final Iterator<ClientHttpRequestInterceptor> iterator;
public InterceptingRequestExecution() {
this.iterator = interceptors.iterator();
}
@Override
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
if (this.iterator.hasNext()) {
ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
return nextInterceptor.intercept(request, body, this);
}
else {...}
...
}}}
Test Scenario:
DemoApplication.txt
package com.example.demo;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.EventListener;
import org.springframework.http.HttpRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@SpringBootApplication
public class DemoApplication {
class TestMiddleware implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
System.out.println("Executing " + request.getURI());
ClientHttpResponse response = execution.execute(request, body);
return response;
}
}
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
@Bean
public RestTemplate restTemplateFix() {
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
restTemplate.setInterceptors(interceptors);
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
@Qualifier("restTemplate")
RestTemplate restTemplate = new RestTemplate();
@Qualifier("restTemplateFix")
RestTemplate restTemplateFix = new RestTemplate();
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@EventListener(ApplicationReadyEvent.class)
public void doSomethingAfterStartup() {
//search for ConcurrentModificationException
//what is ConcurrentModificationException
//testConcurrentModificationExceptionInReadingIteration();
//trigger ConcurrentModificationException
testConcurrentModificationExceptionInRestTemplate();
//fix one
//testConcurrentModificationExceptionInRestTemplateFixWithSharedCollection();
//fix two
//testConcurrentModificationExceptionInRestTemplateFixWithInterceptorsCopy();
//fix three
//testConcurrentModificationExceptionInRestTemplateFixWithCheckInterceptorsCollection();
//fix four
//testConcurrentModificationExceptionInRestTemplateFixWithSetInterceptorsOnce();
}
private void testConcurrentModificationExceptionInRestTemplate() {
for (int i = 0; i < 2000; i++) {
new Thread(() -> {
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(new TestMiddleware());
restTemplate.setInterceptors(interceptors);
ResponseEntity<String> response = restTemplate.getForEntity("http://ip.jsontest.com/", String.class);
System.out.println(response);
}).start();
}
}
final List<ClientHttpRequestInterceptor> sharedCollection = new ArrayList<>(List.of(new ClientHttpRequestInterceptor[]{new TestMiddleware()}));
private void testConcurrentModificationExceptionInRestTemplateFixWithSharedCollection() {
for (int i = 0; i < 2000; i++) {
new Thread(() -> {
List<ClientHttpRequestInterceptor> interceptorsReference = restTemplate.getInterceptors();
interceptorsReference = sharedCollection;
ResponseEntity<String> response = restTemplate.getForEntity("http://ip.jsontest.com/", String.class);
System.out.println(response);
}).start();
}
}
private void testConcurrentModificationExceptionInRestTemplateFixWithInterceptorsCopy() {
for (int i = 0; i < 2000; i++) {
new Thread(() -> {
List<ClientHttpRequestInterceptor> interceptorsCopy = new ArrayList<>(List.of(new ClientHttpRequestInterceptor[]{new TestMiddleware()}));
List<ClientHttpRequestInterceptor> interceptorsReference = restTemplate.getInterceptors();
interceptorsReference = interceptorsCopy;
ResponseEntity<String> response = restTemplate.getForEntity("http://ip.jsontest.com/", String.class);
System.out.println(response);
}).start();
}
}
private void testConcurrentModificationExceptionInRestTemplateFixWithCheckInterceptorsCollection() {
for (int i = 0; i < 2000; i++) {
new Thread(() -> {
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
if (CollectionUtils.isEmpty(interceptors)) {
interceptors = new ArrayList<>();
interceptors.add(new TestMiddleware());
}
restTemplate.setInterceptors(interceptors);
ResponseEntity<String> response = restTemplate.getForEntity("http://ip.jsontest.com/", String.class);
System.out.println(response);
}).start();
}
}
private void testConcurrentModificationExceptionInRestTemplateFixWithSetInterceptorsOnce() {
for (int i = 0; i < 2000; i++) {
new Thread(() -> {
ResponseEntity<String> response = restTemplateFix.getForEntity("http://ip.jsontest.com/", String.class);
System.out.println(response);
}).start();
}
}
private void testConcurrentModificationExceptionInReadingIteration() {
List<Integer> integers = new ArrayList();
System.out.println("before");
for (int i = 0; i < 20; i++) {
int finalI = i;
new Thread(() -> {
System.out.println("thread" + finalI + " add item");
integers.add(finalI);
}).start();
}
new Thread(() -> {
for (Integer integer : integers) {
System.out.println("reading " + integers.get(integer));
}
}).start();
System.out.println("after");
}
}
Debugging steps:
-Run Spring Boot Demo Server
@SpringBootApplication
public class DemoApplication {
...
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@EventListener(ApplicationReadyEvent.class)
public void doSomethingAfterStartup() {
testConcurrentModificationExceptionInRestTemplate();
}
...
}
-Create restTemplate bean
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
-Execute testConcurrentModificationExceptionInRestTemplate() method
private void testConcurrentModificationExceptionInRestTemplate() {
for (int i = 0; i < 2000; i++) {
new Thread(() -> {
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(new TestMiddleware());
restTemplate.setInterceptors(interceptors);
ResponseEntity<String> response = restTemplate.getForEntity("http://ip.jsontest.com/", String.class);
System.out.println(response);
}).start();
}
}
Debugging results:
Exception in thread "Thread-730" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1043)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:997)
at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:92)
at com.example.demo.DemoApplication$TestMiddleware.intercept(DemoApplication.java:31)
at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:93)
at org.springframework.http.client.InterceptingClientHttpRequest.executeInternal(InterceptingClientHttpRequest.java:77)
at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:776)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:711)
at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:361)
at com.example.demo.DemoApplication.lambda$testConcurrentModificationExceptionInRestTemplate$0(DemoApplication.java:105)
at java.base/java.lang.Thread.run(Thread.java:834)
...
Exception in thread "Thread-1855" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList.sort(ArrayList.java:1752)
at org.springframework.core.annotation.AnnotationAwareOrderComparator.sort(AnnotationAwareOrderComparator.java:111)
at org.springframework.http.client.support.InterceptingHttpAccessor.setInterceptors(InterceptingHttpAccessor.java:66)
at com.example.demo.DemoApplication.lambda$testConcurrentModificationExceptionInRestTemplate$0(DemoApplication.java:104)
at java.base/java.lang.Thread.run(Thread.java:834)