This is an automated email from the ASF dual-hosted git repository. liubao pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/servicecomb-java-chassis.git
The following commit(s) were added to refs/heads/master by this push: new 16b60ea [SCB-1968] add producer operation filter 16b60ea is described below commit 16b60ea5fd445e894e55b6bb7e0a793a4c406fa9 Author: wujimin <wuji...@huawei.com> AuthorDate: Mon Jun 1 21:20:42 2020 +0800 [SCB-1968] add producer operation filter --- .../IllegalArgumentExceptionConverter.java | 49 ++++++ .../core/filter/impl/DefaultFilterProvider.java | 3 +- .../core/filter/impl/ProducerOperationFilter.java | 94 ++++++++++ ...e.servicecomb.core.exception.ExceptionConverter | 1 + .../servicecomb/core/filter/FilterChainTest.java | 11 +- .../filter/impl/ProducerOperationFilterTest.java | 193 +++++++++++++++++++++ 6 files changed, 344 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/org/apache/servicecomb/core/exception/converter/IllegalArgumentExceptionConverter.java b/core/src/main/java/org/apache/servicecomb/core/exception/converter/IllegalArgumentExceptionConverter.java new file mode 100644 index 0000000..2e3ccb9 --- /dev/null +++ b/core/src/main/java/org/apache/servicecomb/core/exception/converter/IllegalArgumentExceptionConverter.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.servicecomb.core.exception.converter; + +import javax.annotation.Nullable; +import javax.ws.rs.core.Response.StatusType; + +import org.apache.servicecomb.core.Invocation; +import org.apache.servicecomb.core.exception.ExceptionConverter; +import org.apache.servicecomb.swagger.invocation.exception.InvocationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class IllegalArgumentExceptionConverter implements ExceptionConverter<IllegalArgumentException> { + private static final Logger LOGGER = LoggerFactory.getLogger(IllegalArgumentExceptionConverter.class); + + @Override + public int getOrder() { + return Short.MAX_VALUE; + } + + @Override + public boolean canConvert(Throwable throwable) { + return throwable instanceof IllegalArgumentException; + } + + @Override + public InvocationException convert(@Nullable Invocation invocation, IllegalArgumentException throwable, + StatusType genericStatus) { + LOGGER.error("convert IllegalArgumentException exception to InvocationException.", throwable); + + return new InvocationException(genericStatus, ExceptionConverter.getGenericCode(genericStatus), + "Parameters not valid or types not match.", throwable); + } +} diff --git a/core/src/main/java/org/apache/servicecomb/core/filter/impl/DefaultFilterProvider.java b/core/src/main/java/org/apache/servicecomb/core/filter/impl/DefaultFilterProvider.java index e2a2054..ab4e4ac 100644 --- a/core/src/main/java/org/apache/servicecomb/core/filter/impl/DefaultFilterProvider.java +++ b/core/src/main/java/org/apache/servicecomb/core/filter/impl/DefaultFilterProvider.java @@ -27,6 +27,7 @@ public class DefaultFilterProvider implements FilterProvider { public List<Class<? extends Filter>> getFilters() { return Arrays.asList( SimpleLoadBalanceFilter.class, - ScheduleFilter.class); + ScheduleFilter.class, + ProducerOperationFilter.class); } } diff --git a/core/src/main/java/org/apache/servicecomb/core/filter/impl/ProducerOperationFilter.java b/core/src/main/java/org/apache/servicecomb/core/filter/impl/ProducerOperationFilter.java new file mode 100644 index 0000000..8819522 --- /dev/null +++ b/core/src/main/java/org/apache/servicecomb/core/filter/impl/ProducerOperationFilter.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.servicecomb.core.filter.impl; + +import static org.apache.servicecomb.swagger.invocation.InvocationType.PRODUCER; + +import java.lang.reflect.Method; +import java.util.concurrent.CompletableFuture; + +import org.apache.servicecomb.core.Invocation; +import org.apache.servicecomb.core.exception.Exceptions; +import org.apache.servicecomb.core.filter.Filter; +import org.apache.servicecomb.core.filter.FilterMeta; +import org.apache.servicecomb.core.filter.FilterNode; +import org.apache.servicecomb.foundation.common.utils.AsyncUtils; +import org.apache.servicecomb.swagger.engine.SwaggerProducerOperation; +import org.apache.servicecomb.swagger.invocation.Response; +import org.apache.servicecomb.swagger.invocation.context.ContextUtils; +import org.apache.servicecomb.swagger.invocation.exception.InvocationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@FilterMeta(name = "producer-operation", invocationType = PRODUCER) +public class ProducerOperationFilter implements Filter { + private static final Logger LOGGER = LoggerFactory.getLogger(ProducerOperationFilter.class); + + @Override + public CompletableFuture<Response> onFilter(Invocation invocation, FilterNode nextNode) { + invocation.onBusinessMethodStart(); + + SwaggerProducerOperation producerOperation = invocation.getOperationMeta().getSwaggerProducerOperation(); + Object instance = producerOperation.getProducerInstance(); + Method method = producerOperation.getProducerMethod(); + Object[] args = invocation.toProducerArguments(); + return invoke(invocation, instance, method, args) + .thenApply(result -> convertResultToResponse(invocation, producerOperation, result)) + .whenComplete((response, throwable) -> whenComplete(invocation, throwable)); + } + + @SuppressWarnings("unchecked") + private CompletableFuture<Object> invoke(Invocation invocation, Object instance, Method method, Object[] args) { + ContextUtils.setInvocationContext(invocation); + + try { + Object result = method.invoke(instance, args); + if (result instanceof CompletableFuture) { + return (CompletableFuture<Object>) result; + } + + return CompletableFuture.completedFuture(result); + } catch (Throwable e) { + return AsyncUtils.completeExceptionally(Exceptions.unwrap(e)); + } finally { + ContextUtils.removeInvocationContext(); + } + } + + private Response convertResultToResponse(Invocation invocation, SwaggerProducerOperation producerOperation, + Object result) { + return producerOperation.getResponseMapper().mapResponse(invocation.getStatus(), result); + } + + private void whenComplete(Invocation invocation, Throwable throwable) { + if (shouldPrintErrorLog(throwable)) { + LOGGER.error("unexpected error {},", invocation.getInvocationQualifiedName(), throwable); + } + + invocation.onBusinessMethodFinish(); + invocation.onBusinessFinish(); + } + + protected boolean shouldPrintErrorLog(Throwable throwable) { + if (throwable == null) { + return false; + } + + Throwable unwrapped = Exceptions.unwrap(throwable); + return !(unwrapped instanceof InvocationException); + } +} diff --git a/core/src/main/resources/META-INF/services/org.apache.servicecomb.core.exception.ExceptionConverter b/core/src/main/resources/META-INF/services/org.apache.servicecomb.core.exception.ExceptionConverter index 798c7d1..f129ed1 100644 --- a/core/src/main/resources/META-INF/services/org.apache.servicecomb.core.exception.ExceptionConverter +++ b/core/src/main/resources/META-INF/services/org.apache.servicecomb.core.exception.ExceptionConverter @@ -16,4 +16,5 @@ # org.apache.servicecomb.core.exception.converter.InvocationExceptionConverter +org.apache.servicecomb.core.exception.converter.IllegalArgumentExceptionConverter org.apache.servicecomb.core.exception.converter.DefaultExceptionConverter \ No newline at end of file diff --git a/core/src/test/java/org/apache/servicecomb/core/filter/FilterChainTest.java b/core/src/test/java/org/apache/servicecomb/core/filter/FilterChainTest.java index a7d75c1..d184fa4 100644 --- a/core/src/test/java/org/apache/servicecomb/core/filter/FilterChainTest.java +++ b/core/src/test/java/org/apache/servicecomb/core/filter/FilterChainTest.java @@ -139,14 +139,13 @@ public class FilterChainTest { }; SimpleRetryFilter retryFilter = new SimpleRetryFilter().setMaxRetry(3); - ExecutionException executionException = (ExecutionException) catchThrowable( - () -> FilterNode.buildChain(retryFilter, recordThreadFilter, exceptionFilter) - .onFilter(invocation) - .get()); + CompletableFuture<Response> future = FilterNode.buildChain(retryFilter, recordThreadFilter, exceptionFilter) + .onFilter(invocation); assertThat(msg).containsExactly("main", "main", "main"); - assertThat(executionException.getCause()) - .isInstanceOf(IOException.class) + assertThat(future) + .hasFailedWithThrowableThat() + .isExactlyInstanceOf(IOException.class) .hasMessage("net error"); } } \ No newline at end of file diff --git a/core/src/test/java/org/apache/servicecomb/core/filter/impl/ProducerOperationFilterTest.java b/core/src/test/java/org/apache/servicecomb/core/filter/impl/ProducerOperationFilterTest.java new file mode 100644 index 0000000..0ed0c4b --- /dev/null +++ b/core/src/test/java/org/apache/servicecomb/core/filter/impl/ProducerOperationFilterTest.java @@ -0,0 +1,193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.servicecomb.core.filter.impl; + +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +import java.lang.reflect.Method; +import java.util.concurrent.CompletableFuture; + +import org.apache.servicecomb.core.Invocation; +import org.apache.servicecomb.core.exception.Exceptions; +import org.apache.servicecomb.core.filter.FilterNode; +import org.apache.servicecomb.foundation.test.scaffolding.exception.RuntimeExceptionWithoutStackTrace; +import org.apache.servicecomb.swagger.engine.SwaggerProducerOperation; +import org.apache.servicecomb.swagger.invocation.Response; +import org.apache.servicecomb.swagger.invocation.context.ContextUtils; +import org.apache.servicecomb.swagger.invocation.context.InvocationContext; +import org.apache.servicecomb.swagger.invocation.exception.CommonExceptionData; +import org.apache.servicecomb.swagger.invocation.exception.InvocationException; +import org.junit.Test; + +import mockit.Expectations; +import mockit.Injectable; +import mockit.Mocked; +import mockit.Verifications; + +public class ProducerOperationFilterTest { + ProducerOperationFilter filter = new ProducerOperationFilter(); + + @Injectable + Invocation invocation; + + @Mocked + SwaggerProducerOperation producerOperation; + + static InvocationContext threadInvocationContext; + + public static class Controller { + public void sync() { + threadInvocationContext = ContextUtils.getInvocationContext(); + } + + public void syncException() { + throw new RuntimeExceptionWithoutStackTrace("syncException"); + } + + public CompletableFuture<Void> async() { + threadInvocationContext = ContextUtils.getInvocationContext(); + return CompletableFuture.completedFuture(null); + } + + public CompletableFuture<Void> asyncException() { + throw new RuntimeExceptionWithoutStackTrace("asyncException"); + } + } + + @Test + public void should_record_invocation_trace_time() { + filter.onFilter(invocation, FilterNode.EMPTY); + + new Verifications() { + { + invocation.onBusinessMethodStart(); + times = 1; + + invocation.onBusinessMethodFinish(); + times = 1; + + invocation.onBusinessFinish(); + times = 1; + } + }; + } + + private void setInvokeMethod(String name) throws NoSuchMethodException { + Controller instance = new Controller(); + Method method = instance.getClass().getMethod(name); + new Expectations() { + { + producerOperation.getProducerInstance(); + result = instance; + + producerOperation.getProducerMethod(); + result = method; + } + }; + } + + private void setInvokeSyncMethod() throws NoSuchMethodException { + setInvokeMethod("sync"); + } + + private void setInvokeAsyncMethod() throws NoSuchMethodException { + setInvokeMethod("async"); + } + + @Test + public void should_provide_thread_local_invocation_context_for_sync_method() throws NoSuchMethodException { + setInvokeSyncMethod(); + + filter.onFilter(invocation, FilterNode.EMPTY); + + assertThat(threadInvocationContext).isSameAs(invocation); + } + + @Test + public void should_clear_thread_local_invocation_context_after_sync_method() throws NoSuchMethodException { + setInvokeSyncMethod(); + + filter.onFilter(invocation, FilterNode.EMPTY); + + assertThat(ContextUtils.getInvocationContext()).isNull(); + } + + @Test + public void should_catch_sync_business_exception() throws NoSuchMethodException { + setInvokeMethod("syncException"); + + CompletableFuture<Response> future = filter.onFilter(invocation, FilterNode.EMPTY); + + assertThat(future).hasFailedWithThrowableThat() + .isInstanceOf(RuntimeExceptionWithoutStackTrace.class) + .hasMessage("syncException"); + } + + @Test + public void should_provide_thread_local_invocation_context_for_async_method() throws NoSuchMethodException { + setInvokeAsyncMethod(); + + filter.onFilter(invocation, FilterNode.EMPTY); + + assertThat(threadInvocationContext).isSameAs(invocation); + } + + @Test + public void should_clear_thread_local_invocation_context_after_async_method() throws NoSuchMethodException { + setInvokeAsyncMethod(); + + filter.onFilter(invocation, FilterNode.EMPTY); + + assertThat(ContextUtils.getInvocationContext()).isNull(); + } + + @Test + public void should_catch_async_business_exception() throws NoSuchMethodException { + setInvokeMethod("asyncException"); + + CompletableFuture<Response> future = filter.onFilter(invocation, FilterNode.EMPTY); + + assertThat(future).hasFailedWithThrowableThat() + .isInstanceOf(RuntimeExceptionWithoutStackTrace.class) + .hasMessage("asyncException"); + } + + @Test + public void should_unify_IllegalArgumentException_message_when_convert_exception() throws NoSuchMethodException { + setInvokeSyncMethod(); + new Expectations() { + { + invocation.toProducerArguments(); + result = new Object[] {1}; + } + }; + + CompletableFuture<Response> future = filter.onFilter(invocation, FilterNode.EMPTY); + assertThat(future).hasFailedWithThrowableThat() + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("wrong number of arguments"); + + InvocationException throwable = Exceptions + .convert(invocation, catchThrowable(() -> future.get()), INTERNAL_SERVER_ERROR); + assertThat(throwable).hasCauseInstanceOf(IllegalArgumentException.class); + CommonExceptionData data = (CommonExceptionData) throwable.getErrorData(); + assertThat(data.getMessage()).isEqualTo("Parameters not valid or types not match."); + } +} \ No newline at end of file