From 1c0976a8693519f03b3796215bb9140a3c3160fc Mon Sep 17 00:00:00 2001 From: Yasser Zamani Date: Sat, 28 Oct 2017 16:39:55 +0330 Subject: [PATCH] WW-4874 Introduces Servlet3 plugin by adding support for async action methods --- .../com/opensymphony/xwork2/AsyncManager.java | 35 ++++ .../xwork2/DefaultActionInvocation.java | 87 ++++++---- .../apache/struts2/dispatcher/Dispatcher.java | 12 +- .../struts2/dispatcher/PrepareOperations.java | 9 +- plugins/pom.xml | 1 + plugins/servlet3/pom.xml | 46 ++++++ .../struts2/servlet3/async/AsyncAction.java | 65 ++++++++ .../servlet3/async/Servlet3AsyncManager.java | 149 ++++++++++++++++++ .../src/main/resources/struts-plugin.xml | 29 ++++ 9 files changed, 396 insertions(+), 37 deletions(-) create mode 100644 core/src/main/java/com/opensymphony/xwork2/AsyncManager.java create mode 100644 plugins/servlet3/pom.xml create mode 100644 plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/AsyncAction.java create mode 100644 plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/Servlet3AsyncManager.java create mode 100644 plugins/servlet3/src/main/resources/struts-plugin.xml diff --git a/core/src/main/java/com/opensymphony/xwork2/AsyncManager.java b/core/src/main/java/com/opensymphony/xwork2/AsyncManager.java new file mode 100644 index 000000000..fb6563859 --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/AsyncManager.java @@ -0,0 +1,35 @@ +/* + * 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 com.opensymphony.xwork2; + +import java.util.concurrent.Callable; + +/** + * Adds support for invoke async actions. This allows us to support action methods that return {@link Callable} + * as well as invoking them in separate not-container thread then executing the result in another container thread. + * + * @since 2.5.14 + */ +public interface AsyncManager { + boolean hasAsyncActionResult(); + + Object getAsyncActionResult(); + + void invokeAsyncAction(Callable asyncAction); +} diff --git a/core/src/main/java/com/opensymphony/xwork2/DefaultActionInvocation.java b/core/src/main/java/com/opensymphony/xwork2/DefaultActionInvocation.java index 5777007c1..da38e4eb5 100644 --- a/core/src/main/java/com/opensymphony/xwork2/DefaultActionInvocation.java +++ b/core/src/main/java/com/opensymphony/xwork2/DefaultActionInvocation.java @@ -40,6 +40,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.Callable; /** * The Default ActionInvocation implementation @@ -71,6 +72,8 @@ public class DefaultActionInvocation implements ActionInvocation { protected Container container; protected UnknownHandlerManager unknownHandlerManager; protected OgnlUtil ognlUtil; + protected AsyncManager asyncManager; + protected Callable asyncAction; protected WithLazyParams.LazyParamInjector lazyParamInjector; public DefaultActionInvocation(final Map extraContext, final boolean pushAction) { @@ -108,6 +111,11 @@ public class DefaultActionInvocation implements ActionInvocation { this.ognlUtil = ognlUtil; } + @Inject(required=false) + public void setAsyncManager(AsyncManager asyncManager) { + this.asyncManager = asyncManager; + } + public Object getAction() { return action; } @@ -237,49 +245,61 @@ public class DefaultActionInvocation implements ActionInvocation { throw new IllegalStateException("Action has already executed"); } - if (interceptors.hasNext()) { - final InterceptorMapping interceptorMapping = interceptors.next(); - String interceptorMsg = "interceptorMapping: " + interceptorMapping.getName(); - UtilTimerStack.push(interceptorMsg); - try { - Interceptor interceptor = interceptorMapping.getInterceptor(); - if (interceptor instanceof WithLazyParams) { - interceptor = lazyParamInjector.injectParams(interceptor, interceptorMapping.getParams(), invocationContext); + if (asyncManager == null || !asyncManager.hasAsyncActionResult()) { + if (interceptors.hasNext()) { + final InterceptorMapping interceptorMapping = interceptors.next(); + String interceptorMsg = "interceptorMapping: " + interceptorMapping.getName(); + UtilTimerStack.push(interceptorMsg); + try { + Interceptor interceptor = interceptorMapping.getInterceptor(); + if (interceptor instanceof WithLazyParams) { + interceptor = lazyParamInjector.injectParams(interceptor, interceptorMapping.getParams(), invocationContext); + } + resultCode = interceptor.intercept(DefaultActionInvocation.this); + } finally { + UtilTimerStack.pop(interceptorMsg); } - resultCode = interceptor.intercept(DefaultActionInvocation.this); - } finally { - UtilTimerStack.pop(interceptorMsg); + } else { + resultCode = invokeActionOnly(); } } else { - resultCode = invokeActionOnly(); + Object asyncActionResult = asyncManager.getAsyncActionResult(); + if (asyncActionResult instanceof Throwable) { + throw new Exception((Throwable) asyncActionResult); + } + asyncAction = null; + resultCode = saveResult(proxy.getConfig(), asyncActionResult); } - // this is needed because the result will be executed, then control will return to the Interceptor, which will - // return above and flow through again - if (!executed) { - if (preResultListeners != null) { - LOG.trace("Executing PreResultListeners for result [{}]", result); + if (asyncManager == null || asyncAction == null) { + // this is needed because the result will be executed, then control will return to the Interceptor, which will + // return above and flow through again + if (!executed) { + if (preResultListeners != null) { + LOG.trace("Executing PreResultListeners for result [{}]", result); - for (Object preResultListener : preResultListeners) { - PreResultListener listener = (PreResultListener) preResultListener; + for (Object preResultListener : preResultListeners) { + PreResultListener listener = (PreResultListener) preResultListener; - String _profileKey = "preResultListener: "; - try { - UtilTimerStack.push(_profileKey); - listener.beforeResult(this, resultCode); - } - finally { - UtilTimerStack.pop(_profileKey); + String _profileKey = "preResultListener: "; + try { + UtilTimerStack.push(_profileKey); + listener.beforeResult(this, resultCode); + } finally { + UtilTimerStack.pop(_profileKey); + } } } - } - // now execute the result, if we're supposed to - if (proxy.getExecuteResult()) { - executeResult(); - } + // now execute the result, if we're supposed to + if (proxy.getExecuteResult()) { + executeResult(); + } - executed = true; + executed = true; + } + } else { + asyncManager.invokeAsyncAction(asyncAction); } return resultCode; @@ -495,6 +515,9 @@ public class DefaultActionInvocation implements ActionInvocation { // Wire the result automatically container.inject(explicitResult); return null; + } else if (methodResult instanceof Callable) { + asyncAction = (Callable) methodResult; + return null; } else { return (String) methodResult; } diff --git a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java index dcc5fe72a..1672f9ead 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java @@ -561,8 +561,16 @@ public class Dispatcher { String name = mapping.getName(); String method = mapping.getMethod(); - ActionProxy proxy = getContainer().getInstance(ActionProxyFactory.class).createActionProxy( - namespace, name, method, extraContext, true, false); + ActionProxy proxy; + + //check if we are probably in an async resuming + ActionInvocation inv = ActionContext.getContext().getActionInvocation(); + if (inv == null || inv.isExecuted()) { + proxy = getContainer().getInstance(ActionProxyFactory.class).createActionProxy(namespace, name, method, + extraContext, true, false); + } else { + proxy = inv.getProxy(); + } request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, proxy.getInvocation().getStack()); diff --git a/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java b/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java index 354cad7fa..c216cdfb0 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java @@ -79,9 +79,12 @@ public class PrepareOperations { // detected existing context, so we are probably in a forward ctx = new ActionContext(new HashMap<>(oldContext.getContextMap())); } else { - ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack(); - stack.getContext().putAll(dispatcher.createContextMap(request, response, null)); - ctx = new ActionContext(stack.getContext()); + ctx = ServletActionContext.getActionContext(request); //checks if we are probably in an async + if (ctx == null) { + ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack(); + stack.getContext().putAll(dispatcher.createContextMap(request, response, null)); + ctx = new ActionContext(stack.getContext()); + } } request.setAttribute(CLEANUP_RECURSION_COUNTER, counter); ActionContext.setContext(ctx); diff --git a/plugins/pom.xml b/plugins/pom.xml index c336d767e..4a9c7647d 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -56,6 +56,7 @@ spring testng tiles + servlet3 diff --git a/plugins/servlet3/pom.xml b/plugins/servlet3/pom.xml new file mode 100644 index 000000000..36de62611 --- /dev/null +++ b/plugins/servlet3/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + org.apache.struts + struts2-plugins + 2.5.14-SNAPSHOT + + + struts2-servlet3-plugin + Struts 2 Servlet3 Plugin + jar + + + UTF-8 + + + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + diff --git a/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/AsyncAction.java b/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/AsyncAction.java new file mode 100644 index 000000000..912cfe765 --- /dev/null +++ b/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/AsyncAction.java @@ -0,0 +1,65 @@ +/* + * 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.struts2.servlet3.async; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; + +/** + * A {@link Callable} with a timeout value and an {@link Executor}. + * + * @since 2.5.14 + */ +public class AsyncAction implements Callable { + private Callable callable; + private Long timeout; + private Executor executor; + + public AsyncAction(Callable callable) { + this.callable = callable; + } + + public AsyncAction(long timeout, Callable callable) { + this(callable); + this.timeout = timeout; + } + + public AsyncAction(Executor executor, Callable callable) { + this(callable); + this.executor = executor; + } + + public AsyncAction(long timeout, Executor executor, Callable callable) { + this(timeout, callable); + this.executor = executor; + } + + public Long getTimeout() { + return timeout; + } + + public Executor getExecutor() { + return executor; + } + + @Override + public Object call() throws Exception { + return callable.call(); + } +} diff --git a/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/Servlet3AsyncManager.java b/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/Servlet3AsyncManager.java new file mode 100644 index 000000000..f72f58036 --- /dev/null +++ b/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/Servlet3AsyncManager.java @@ -0,0 +1,149 @@ +/* + * 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.struts2.servlet3.async; + +import com.opensymphony.xwork2.AsyncManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ServletActionContext; + +import javax.servlet.AsyncContext; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Implements {@link AsyncManager} to add support for invoke async actions via Servlet 3's API. + * + * @since 2.5.14 + */ +public class Servlet3AsyncManager implements AsyncManager, AsyncListener { + private static final Logger LOG = LogManager.getLogger(Servlet3AsyncManager.class); + private static final AtomicInteger threadCount = new AtomicInteger(0); + + private AsyncContext asyncContext; + private boolean asyncActionStarted; + private Boolean asyncCompleted; + private Object asyncActionResult; + + @Override + public void invokeAsyncAction(final Callable asyncAction) { + if (asyncActionStarted) { + return; + } + + Long timeout = null; + Executor executor = null; + if (asyncAction instanceof AsyncAction) { + AsyncAction customAsyncAction = (AsyncAction) asyncAction; + timeout = customAsyncAction.getTimeout(); + executor = customAsyncAction.getExecutor(); + } + + HttpServletRequest req = ServletActionContext.getRequest(); + asyncActionResult = null; + asyncCompleted = false; + + if (asyncContext == null || !req.isAsyncStarted()) { + asyncContext = req.startAsync(req, ServletActionContext.getResponse()); + asyncContext.addListener(this); + if (timeout != null) { + asyncContext.setTimeout(timeout); + } + } + asyncActionStarted = true; + LOG.debug("Async processing started for " + asyncContext); + + final Runnable task = new Runnable() { + @Override + public void run() { + try { + setAsyncActionResultAndDispatch(asyncAction.call()); + } catch (Throwable e) { + setAsyncActionResultAndDispatch(e); + } + } + }; + if (executor != null) { + executor.execute(task); + } else { + final Thread thread = new Thread(new Runnable() { + @Override + public void run() { + try { + task.run(); + } finally { + threadCount.decrementAndGet(); + } + } + }, this.getClass().getSimpleName() + "-" + threadCount.incrementAndGet()); + thread.start(); + } + } + + private void setAsyncActionResultAndDispatch(Object asyncActionResult) { + this.asyncActionResult = asyncActionResult; + + String log = "Async result [" + asyncActionResult + "] of " + asyncContext; + if (asyncCompleted) { + LOG.error(log + " - could not complete result executing due to timeout or network error"); + } else { + LOG.debug(log + " - dispatching request to execute result in container"); + asyncContext.dispatch(); + } + } + + @Override + public boolean hasAsyncActionResult() { + return asyncActionResult != null; + } + + @Override + public Object getAsyncActionResult() { + return asyncActionResult; + } + + @Override + public void onComplete(AsyncEvent asyncEvent) throws IOException { + asyncContext = null; + asyncCompleted = true; + } + + @Override + public void onTimeout(AsyncEvent asyncEvent) throws IOException { + LOG.debug("Processing timeout for " + asyncEvent.getAsyncContext()); + setAsyncActionResultAndDispatch("timeout"); + } + + @Override + public void onError(AsyncEvent asyncEvent) throws IOException { + Throwable e = asyncEvent.getThrowable(); + LOG.error("Processing error for " + asyncEvent.getAsyncContext(), e); + setAsyncActionResultAndDispatch(e); + } + + @Override + public void onStartAsync(AsyncEvent asyncEvent) throws IOException { + + } +} diff --git a/plugins/servlet3/src/main/resources/struts-plugin.xml b/plugins/servlet3/src/main/resources/struts-plugin.xml new file mode 100644 index 000000000..9c6a75208 --- /dev/null +++ b/plugins/servlet3/src/main/resources/struts-plugin.xml @@ -0,0 +1,29 @@ + + + + + + +