WW-4874 Introduces Servlet3 plugin by adding support for async action methods

This commit is contained in:
Yasser Zamani
2017-10-28 16:39:55 +03:30
parent e9fe38608a
commit 1c0976a869
9 changed files with 396 additions and 37 deletions
@@ -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);
}
@@ -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<String, Object> 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;
}
@@ -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());
@@ -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);
+1
View File
@@ -56,6 +56,7 @@
<module>spring</module>
<module>testng</module>
<module>tiles</module>
<module>servlet3</module>
</modules>
<dependencies>
+46
View File
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/*
* 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.
*/
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>2.5.14-SNAPSHOT</version>
</parent>
<artifactId>struts2-servlet3-plugin</artifactId>
<name>Struts 2 Servlet3 Plugin</name>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
@@ -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();
}
}
@@ -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 {
}
}
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
/*
* 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.
*/
-->
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
"http://struts.apache.org/dtds/struts-2.5.dtd">
<struts>
<bean type="com.opensymphony.xwork2.AsyncManager" name="default"
class="org.apache.struts2.servlet3.async.Servlet3AsyncManager" scope="prototype" />
</struts>