当restTemplate通过异步执行程序使用时,与MockRestService的ConcurrentModificationException

时间:2016-06-03 13:04:44

标签: java rest asynchronous spring-boot spring-web

尝试使用spring MockRestService模拟其他响应进行集成测试,但当实际代码异步使用rest模板时,AbstractRequestExpectationManager会一直运行到ConcurrentModificationException

测试伪代码片段:

@Autowired
RestTemplate restTemplate;
MockRestServiceServer mockRestServiceServer;

@Test
public test() {
    // given
    mockRestServiceServer = MockRestServiceServer
            .bindTo( restTemplate )
            .ignoreExpectOrder()  // supported from spring 4.3
            .build();
    prepareRestResponse( "/resource/url", "mock json content".getBytes() );
    // when
    myservice.refreshPricesForProductGroup( 2 );

    // then
    // assertions
}

private void prepareRestResponse( final String urlTail, final byte[] responseContent ) {
    mockRestServiceServer
            .expect( requestTo( endsWith( urlTail ) ) )
            .andExpect( method( HttpMethod.GET ) )
            .andRespond( withSuccess()
                    .body( responseContent )
                    .contentType( APPLICATION_JSON_UTF8 ) );
}

访问其余模板的实际代码:

@Autowired
Executor executor
@Autowired
PriceRestClient priceClient
@Autowired
ProductRestClient productClient

/../

private void refreshPricesForProductGroup( final int groupId ) {

    List<Product> products = productClient.findAllProductsForGroup( groupId );

    products.forEach( p ->
            executor.execute( () -> {
                final Price price = priceClient.getPrice( p.getId() );
                priceRepository.updatePrice( price );
            } )
    );
}

PriceRestClient.getPrice()执行简单的休息调用:

Price getPrice( String productId ) {

    try {
        ResponseEntity<byte[]> entity = restTemplate.exchange(
                restUtil.getProductPriceDataUrl(),
                HttpMethod.GET,
                restUtil.createGzipEncodingRequestEntity(),
                byte[].class,
                productId );

        if ( entity.getStatusCode() == HttpStatus.OK ) {
            String body = restUtil.unmarshalGzipBody( entity.getBody() );
            return priceEntityParser.parse( body );
        }

    } catch ( HttpClientErrorException e ) {
        // TODO
    } catch ( ResourceAccessException e ) {
        // TODO
    } catch ( IOException e ) {
        // TODO
    }

    return null;
}

抛出异常:

Exception in thread "AsyncExecutor-2" java.util.ConcurrentModificationException
    at java.util.LinkedHashMap$LinkedHashIterator.nextNode(LinkedHashMap.java:711)
    at java.util.LinkedHashMap$LinkedKeyIterator.next(LinkedHashMap.java:734)
    at org.springframework.test.web.client.AbstractRequestExpectationManager$RequestExpectationGroup.findExpectation(AbstractRequestExpectationManager.java:167)
    at org.springframework.test.web.client.UnorderedRequestExpectationManager.validateRequestInternal(UnorderedRequestExpectationManager.java:42)
    at org.springframework.test.web.client.AbstractRequestExpectationManager.validateRequest(AbstractRequestExpectationManager.java:71)
    at org.springframework.test.web.client.MockRestServiceServer$MockClientHttpRequestFactory$1.executeInternal(MockRestServiceServer.java:286)
    at org.springframework.mock.http.client.MockClientHttpRequest.execute(MockClientHttpRequest.java:93)
    at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:93)
    at com.mycompany.myproduct.web.client.HttpRequestInterceptorLoggingClient.interceptReq(HttpRequestInterceptorLoggingClient.java:32)
    at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:85)
    at org.springframework.http.client.InterceptingClientHttpRequest.executeInternal(InterceptingClientHttpRequest.java:69)
    at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
    at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:53)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:596)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:557)
    at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:475)
    at com.mycompany.myproduct.rest.PriceRestClient.getPrice(PriceRestClient.java:48)
    at com.mycompany.myproduct.service.ProductPriceSourcingService.lambda$null$29(ProductPriceSourcingService.java:132)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

我在这里做错了什么,或者它可能是MockRestService的错误?

3 个答案:

答案 0 :(得分:1)

通过创建UnorderedRequestExpectationManager

的副本来“修复”此问题
package cucumber.testbeans;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.test.web.client.*;

/**
* Essentially an {@link UnorderedRequestExpectationManager}, but overrides some implementations of {@link AbstractRequestExpectationManager}
* in order to allow adding more expectations after the first request has been made.
*/
public class RestRequestExpectationManager extends AbstractRequestExpectationManager {

    private final RequestExpectationGroup remainingExpectations = new RequestExpectationGroup();

    @Override
    public ResponseActions expectRequest( ExpectedCount count, RequestMatcher matcher ) {
        //        Assert.state(getRequests().isEmpty(), "Cannot add more expectations after actual requests are made.");
        RequestExpectation expectation = new DefaultRequestExpectation( count, matcher );
        getExpectations().add( expectation );
        return expectation;
    }

    @Override
    public ClientHttpResponse validateRequest( ClientHttpRequest request ) throws IOException {
        //        if (getRequests().isEmpty()) {
        afterExpectationsDeclared();
        //        }
        ClientHttpResponse response = validateRequestInternal( request );
        getRequests().add( request );
        return response;
    }

    @Override
    protected void afterExpectationsDeclared() {
        this.remainingExpectations.updateAll( getExpectations() );
    }

    @Override
    public ClientHttpResponse validateRequestInternal( ClientHttpRequest request ) throws IOException {
        RequestExpectation expectation = this.remainingExpectations.findExpectation( request );
        if ( expectation != null ) {
            ClientHttpResponse response = expectation.createResponse( request );
            this.remainingExpectations.update( expectation );
            return response;
        }
        throw createUnexpectedRequestError( request );
    }

    /**
    * Same as {@link AbstractRequestExpectationManager.RequestExpectationGroup}, but synchronizes operations on the {@code expectations}
    * set, so async operation would be possible.
    */
    private static class RequestExpectationGroup {

        private final Set<RequestExpectation> expectations = Collections.synchronizedSet( new LinkedHashSet<>() );

        public Set<RequestExpectation> getExpectations() {
            return this.expectations;
        }

        public void update( RequestExpectation expectation ) {
            if ( expectation.hasRemainingCount() ) {
                getExpectations().add( expectation );
            } else {
                getExpectations().remove( expectation );
            }
        }

        public void updateAll( Collection<RequestExpectation> expectations ) {
            for ( RequestExpectation expectation : expectations ) {
                update( expectation );
            }
        }

        public RequestExpectation findExpectation( ClientHttpRequest request ) throws IOException {
            synchronized ( this.expectations ) {
                for ( RequestExpectation expectation : getExpectations() ) {
                    try {
                        expectation.match( request );
                        return expectation;
                    } catch ( AssertionError error ) {
                        // Ignore
                    }
                }
                return null;
            }
        }
    }
}

有两件值得注意的事情:

  • 首先是RestRequestExpectationManager中的注释行,这使得我们能够在处理完第一个请求后添加期望(与手头的ConcurrentModificationException问题无关);
  • 然后在expectations中进行RestRequestExpectationManager.RequestExpectationGroup同步,以支持异步操作。似乎对我有用。

按如下方式初始化MockRestServiceServer:

MockRestServiceServer mockRestServiceServer = MockRestServiceServer
            .bindTo( restTemplate )
            .build( new RestRequestExpectationManager() );

答案 1 :(得分:0)

我使用RestTemplate / RestOperations对象遇到了多个线程的相同问题。好像MockRestServiceServer没有正确的线程安全。

我现在正试图解决这个问题,到目前为止,我已经复制了MockRestService类,并且正在为expectedRequests和actualRequests尝试CopyOnWriteArrayList。它似乎解决了问题,但现在我的测试失败了。

答案 2 :(得分:0)

我只需要线程安全,所以我稍微重构laur's answer并删除了其他功能(即时修改期望)。留在这里作为参考。

import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.test.web.client.AbstractRequestExpectationManager;
import org.springframework.test.web.client.RequestExpectation;
import org.springframework.test.web.client.UnorderedRequestExpectationManager;

import java.io.IOException;
import java.util.*;

public class SynchronizedUnorderedRequestExpectationManager extends AbstractRequestExpectationManager {
    private final List<RequestExpectation> expectations = Collections.synchronizedList(new LinkedList());
    private final List<ClientHttpRequest> requests = Collections.synchronizedList(new LinkedList());
    private final SynchronizedRequestExpectationGroup remainingExpectations = new SynchronizedRequestExpectationGroup();

    @Override
    protected List<RequestExpectation> getExpectations() {
        return this.expectations;
    }

    @Override
    protected List<ClientHttpRequest> getRequests() {
        return this.requests;
    }

    protected void afterExpectationsDeclared() {
        this.remainingExpectations.updateAll(this.getExpectations());
    }

    public ClientHttpResponse validateRequestInternal(ClientHttpRequest request) throws IOException {
        RequestExpectation expectation = this.remainingExpectations.findExpectation(request);

        if (expectation != null) {
            ClientHttpResponse response = expectation.createResponse(request);
            this.remainingExpectations.update(expectation);
            return response;
        }

        throw this.createUnexpectedRequestError(request);
    }

    public void reset() {
        super.reset();
        this.remainingExpectations.reset();
    }

    protected static class SynchronizedRequestExpectationGroup extends RequestExpectationGroup {

        private final Set<RequestExpectation> expectations = Collections.synchronizedSet(new LinkedHashSet<>());

        @Override
        public Set<RequestExpectation> getExpectations() {
            return this.expectations;
        }

        @Override
        public void updateAll(Collection<RequestExpectation> expectations) {
            synchronized (expectations) {
                super.updateAll(expectations);
            }
        }

        @Override
        public RequestExpectation findExpectation(ClientHttpRequest request) throws IOException {
            synchronized (expectations) {
                return super.findExpectation(request);
            }
        }
    }
}

背景

代码基本上是从UnorderedRequestExpectationManager复制的。不幸的是,这仍然是必要的,因为原始类紧密耦合到AbstractRequestExpectationManager.RequestExpectationGroup,这不是线程安全的。为了替换依赖,需要重写该类。其他线程不安全的集合依赖项(来自AbstractRequestExpectationManager)将替换为覆盖getExpectationsgetRequests方法。集合迭代由关键部分保护。

用法

RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate)
    .build(new SynchronizedUnorderedRequestExpectationManager());

谢谢劳尔! (upvoted)