Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WW-5382 Fix stale injections in Dispatcher #826

Merged
merged 8 commits into from
Jan 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ public void setUp() throws Exception {
@After
public void tearDown() throws Exception {
XWorkTestCaseHelper.tearDown(configurationManager);
configurationManager = null;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary

configuration = null;
container = null;
actionProxyFactory = null;
}

protected void loadConfigurationProviders(ConfigurationProvider... providers) {
Expand Down
4 changes: 0 additions & 4 deletions core/src/main/java/com/opensymphony/xwork2/XWorkTestCase.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,6 @@ protected void setUp() throws Exception {
@Override
protected void tearDown() throws Exception {
XWorkTestCaseHelper.tearDown(configurationManager);
configurationManager = null;
configuration = null;
container = null;
actionProxyFactory = null;
}

protected void loadConfigurationProviders(ConfigurationProvider... providers) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,12 +325,8 @@ public Class<? extends Configuration> type() {
}

protected ActionContext setContext(Container cont) {
ActionContext context = ActionContext.getContext();
if (context == null) {
Copy link
Member Author

@kusalk kusalk Jan 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is only called twice, and it is always null on the first call, and never null on the second call. I can't see why we wouldn't want to replace the bootstrap context once the final context has been constructed. Although it is also unclear if the ActionContext is even used after this point - i.e. for loading the PackageProviders. The ActionContext is cleared once the PackageProviders are loaded.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest I would love to re-organise the whole proces. I assume the ActionContext is only needed when serving an action, it should be created just before and destroyed just after.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that would be ideal - if that were the case we'd be able to remove the need for the bootstrap context too it looks like

ValueStack vs = cont.getInstance(ValueStackFactory.class).createValueStack();
context = ActionContext.of(vs.getContext()).bind();
}
return context;
ValueStack vs = cont.getInstance(ValueStackFactory.class).createValueStack();
return ActionContext.of(vs.getContext()).bind();
}

protected Container createBootstrapContainer(List<ContainerProvider> providers) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,25 @@
/**
* Simple class to hold Container instance per thread to minimise number of attempts
* to read configuration and build each time a new configuration.
*
* <p>
* As ContainerHolder operates just per thread (which means per request) there is no need
* to check if configuration changed during the same request. If changed between requests,
* first call to store Container in ContainerHolder will be with the new configuration.
*/
class ContainerHolder {

private static ThreadLocal<Container> instance = new ThreadLocal<>();
private static final ThreadLocal<Container> instance = new ThreadLocal<>();

public static void store(Container instance) {
ContainerHolder.instance.set(instance);
public static void store(Container newInstance) {
instance.set(newInstance);
}

public static Container get() {
return ContainerHolder.instance.get();
return instance.get();
}

public static void clear() {
ContainerHolder.instance.remove();
instance.remove();
}

}
159 changes: 95 additions & 64 deletions core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import org.apache.struts2.config.StrutsJavaConfiguration;
import org.apache.struts2.config.StrutsJavaConfigurationProvider;
import org.apache.struts2.config.StrutsXmlConfigurationProvider;
import org.apache.struts2.dispatcher.mapper.ActionMapper;
import org.apache.struts2.dispatcher.mapper.ActionMapping;
import org.apache.struts2.dispatcher.multipart.MultiPartRequest;
import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
Expand Down Expand Up @@ -119,6 +120,12 @@ public class Dispatcher {
*/
private static final List<DispatcherListener> dispatcherListeners = new CopyOnWriteArrayList<>();

/**
* This field exists so {@link #getContainer()} can determine whether to (re-)inject this instance in the case of
* a {@link ConfigurationManager} reload.
*/
private Container injectedContainer;

/**
* Store state of StrutsConstants.STRUTS_DEVMODE setting.
*/
Expand All @@ -144,11 +151,6 @@ public class Dispatcher {
*/
private String multipartSaveDir;

/**
* Stores the value of {@link StrutsConstants#STRUTS_MULTIPART_PARSER} setting
*/
private String multipartHandlerName;

/**
* Stores the value of {@link StrutsConstants#STRUTS_MULTIPART_ENABLED}
*/
Expand Down Expand Up @@ -192,6 +194,11 @@ public class Dispatcher {
* Store ConfigurationManager instance, set on init.
*/
protected ConfigurationManager configurationManager;
private ObjectFactory objectFactory;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should save some computation by injecting these beans once when the Dispatcher is initialised instead of constantly retrieving them from the container

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe they should be moved into constructor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the Dispatcher itself is not container-managed, we can't use constructor injection for it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't have to be, just using container.inject(Dispatcher.class) will create a new instance and injects all the dependencies. Anyway this would be a bit out of scope for this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep would require a major refactor, I've got some ideas but not a high priority right now, as long as everything works

private ActionProxyFactory actionProxyFactory;
private LocaleProviderFactory localeProviderFactory;
private StaticContentLoader staticContentLoader;
private ActionMapper actionMapper;

/**
* Provide the dispatcher instance for the current thread.
Expand All @@ -211,6 +218,13 @@ public static void setInstance(Dispatcher instance) {
Dispatcher.instance.set(instance);
}

/**
* Removes the dispatcher instance for this thread.
*/
public static void clearInstance() {
Dispatcher.instance.remove();
}

/**
* Add a dispatcher lifecycle listener.
*
Expand Down Expand Up @@ -306,9 +320,12 @@ public void setMultipartSaveDir(String val) {
multipartSaveDir = val;
}

@Inject(StrutsConstants.STRUTS_MULTIPART_PARSER)
/**
* @deprecated since 6.4.0, no replacement.
*/
@Deprecated
public void setMultipartHandler(String val) {
multipartHandlerName = val;
// no-op
}

@Inject(value = StrutsConstants.STRUTS_MULTIPART_ENABLED, required = false)
Expand All @@ -326,11 +343,21 @@ public void setValueStackFactory(ValueStackFactory valueStackFactory) {
this.valueStackFactory = valueStackFactory;
}

public ValueStackFactory getValueStackFactory() {
return valueStackFactory;
}

@Inject(StrutsConstants.STRUTS_HANDLE_EXCEPTION)
public void setHandleException(String handleException) {
this.handleException = Boolean.parseBoolean(handleException);
}

@Inject(StrutsConstants.STRUTS_DISPATCHER_PARAMETERSWORKAROUND)
public void setDispatchersParametersWorkaround(String dispatchersParametersWorkaround) {
this.paramsWorkaroundEnabled = Boolean.parseBoolean(dispatchersParametersWorkaround)
|| (servletContext != null && StringUtils.contains(servletContext.getServerInfo(), "WebLogic"));
}

public boolean isHandleException() {
return handleException;
}
Expand All @@ -340,12 +367,48 @@ public void setDispatcherErrorHandler(DispatcherErrorHandler errorHandler) {
this.errorHandler = errorHandler;
}

@Inject
public void setObjectFactory(ObjectFactory objectFactory) {
this.objectFactory = objectFactory;
}

@Inject
public void setActionProxyFactory(ActionProxyFactory actionProxyFactory) {
this.actionProxyFactory = actionProxyFactory;
}

public ActionProxyFactory getActionProxyFactory() {
return actionProxyFactory;
}

@Inject
public void setLocaleProviderFactory(LocaleProviderFactory localeProviderFactory) {
this.localeProviderFactory = localeProviderFactory;
}

@Inject
public void setStaticContentLoader(StaticContentLoader staticContentLoader) {
this.staticContentLoader = staticContentLoader;
}

public StaticContentLoader getStaticContentLoader() {
return staticContentLoader;
}

@Inject
public void setActionMapper(ActionMapper actionMapper) {
this.actionMapper = actionMapper;
}

public ActionMapper getActionMapper() {
return actionMapper;
}

/**
* Releases all instances bound to this dispatcher instance.
*/
public void cleanup() {
// clean up ObjectFactory
ObjectFactory objectFactory = getContainer().getInstance(ObjectFactory.class);
if (objectFactory == null) {
LOG.warn("Object Factory is null, something is seriously wrong, no clean up will be performed");
}
Expand All @@ -359,7 +422,7 @@ public void cleanup() {
}

// clean up Dispatcher itself for this thread
instance.set(null);
instance.remove();
servletContext.setAttribute(StrutsStatics.SERVLET_DISPATCHER, null);

// clean up DispatcherListeners
Expand Down Expand Up @@ -532,21 +595,6 @@ private void init_DeferredXmlConfigurations() {
loadConfigPaths("struts-deferred.xml");
}

private Container init_PreloadConfiguration() {
return getContainer();
}

private void init_CheckWebLogicWorkaround(Container container) {
// test whether param-access workaround needs to be enabled
if (servletContext != null && StringUtils.contains(servletContext.getServerInfo(), "WebLogic")) {
LOG.info("WebLogic server detected. Enabling Struts parameter access work-around.");
paramsWorkaroundEnabled = true;
} else {
paramsWorkaroundEnabled = "true".equals(container.getInstance(String.class,
StrutsConstants.STRUTS_DISPATCHER_PARAMETERSWORKAROUND));
}
}

/**
* Load configurations, including both XML and zero-configuration strategies,
* and update optional settings, including whether to reload configurations and resource files.
Expand All @@ -567,9 +615,7 @@ public void init() {
init_AliasStandardObjects(); // [7]
init_DeferredXmlConfigurations();

Container container = init_PreloadConfiguration();
container.inject(this);
init_CheckWebLogicWorkaround(container);
getContainer(); // Inject this instance

if (!dispatcherListeners.isEmpty()) {
for (DispatcherListener l : dispatcherListeners) {
Expand Down Expand Up @@ -689,7 +735,6 @@ protected ActionProxy prepareActionProxy(Map<String, Object> extraContext, Strin
}

protected ActionProxy createActionProxy(String namespace, String name, String method, Map<String, Object> extraContext) {
ActionProxyFactory actionProxyFactory = getContainer().getInstance(ActionProxyFactory.class);
return actionProxyFactory.createActionProxy(namespace, name, method, extraContext, true, false);
}

Expand Down Expand Up @@ -865,6 +910,7 @@ protected String getSaveDir() {
* @param response The response
*/
public void prepare(HttpServletRequest request, HttpServletResponse response) {
getContainer(); // Init ContainerHolder and reinject this instance IF ConfigurationManager was reloaded
String encoding = null;
if (defaultEncoding != null) {
encoding = defaultEncoding;
Expand Down Expand Up @@ -937,15 +983,12 @@ public HttpServletRequest wrapRequest(HttpServletRequest request) throws IOExcep
}

if (isMultipartSupportEnabled(request) && isMultipartRequest(request)) {
MultiPartRequest multiPartRequest = getMultiPartRequest();
LocaleProviderFactory localeProviderFactory = getContainer().getInstance(LocaleProviderFactory.class);

request = new MultiPartRequestWrapper(
multiPartRequest,
request,
getSaveDir(),
localeProviderFactory.createLocaleProvider(),
disableRequestAttributeValueStackLookup
getMultiPartRequest(),
request,
getSaveDir(),
localeProviderFactory.createLocaleProvider(),
disableRequestAttributeValueStackLookup
);
} else {
request = new StrutsRequestWrapper(request, disableRequestAttributeValueStackLookup);
Expand Down Expand Up @@ -988,18 +1031,7 @@ protected boolean isMultipartRequest(HttpServletRequest request) {
* @return a multi part request object
*/
protected MultiPartRequest getMultiPartRequest() {
MultiPartRequest mpr = null;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remnants from before the bean aliasing functionality existed I presume?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... good question, no idea :)

//check for alternate implementations of MultiPartRequest
Set<String> multiNames = getContainer().getInstanceNames(MultiPartRequest.class);
for (String multiName : multiNames) {
if (multiName.equals(multipartHandlerName)) {
mpr = getContainer().getInstance(MultiPartRequest.class, multiName);
}
}
if (mpr == null) {
mpr = getContainer().getInstance(MultiPartRequest.class);
}
return mpr;
return getContainer().getInstance(MultiPartRequest.class);
lukaszlenart marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -1063,27 +1095,26 @@ public ConfigurationManager getConfigurationManager() {
}

/**
* Expose the dependency injection container.
* Exposes a thread-cached reference of the dependency injection container. If the container is found to have
* changed since the last time it was cached, this Dispatcher instance is re-injected to ensure no stale
* configuration/dependencies persist.
* <p>
* A non-cached reference can be obtained by calling {@link #getConfigurationManager()}.
*
* @return Our dependency injection container
*/
public Container getContainer() {
if (ContainerHolder.get() != null) {
return ContainerHolder.get();
}
ConfigurationManager mgr = getConfigurationManager();
if (mgr == null) {
throw new IllegalStateException("The configuration manager shouldn't be null");
} else {
Configuration config = mgr.getConfiguration();
if (config == null) {
throw new IllegalStateException("Unable to load configuration");
} else {
Container container = config.getContainer();
ContainerHolder.store(container);
return container;
if (ContainerHolder.get() == null) {
try {
ContainerHolder.store(getConfigurationManager().getConfiguration().getContainer());
} catch (NullPointerException e) {
throw new IllegalStateException("ConfigurationManager and/or Configuration should not be null", e);
}
}
if (injectedContainer != ContainerHolder.get()) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the core change of this PR - we compare the retrieved container instance against the one we last injected this with, and if it has changed, reinject

injectedContainer = ContainerHolder.get();
injectedContainer.inject(this);
}
return ContainerHolder.get();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
*/
package org.apache.struts2.dispatcher;

import org.apache.struts2.dispatcher.mapper.ActionMapping;
import org.apache.struts2.RequestUtils;
import org.apache.struts2.dispatcher.mapper.ActionMapping;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -54,7 +54,7 @@ public boolean executeStaticResourceRequest(HttpServletRequest request, HttpServ
resourcePath = request.getPathInfo();
}

StaticContentLoader staticResourceLoader = dispatcher.getContainer().getInstance(StaticContentLoader.class);
StaticContentLoader staticResourceLoader = dispatcher.getStaticContentLoader();
if (staticResourceLoader.canHandle(resourcePath)) {
staticResourceLoader.findStaticResource(resourcePath, request, response);
// The framework did its job here
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public Dispatcher initDispatcher(HostConfig filterConfig) {
* @return the static content loader
*/
public StaticContentLoader initStaticContentLoader(HostConfig filterConfig, Dispatcher dispatcher) {
StaticContentLoader loader = dispatcher.getContainer().getInstance(StaticContentLoader.class);
StaticContentLoader loader = dispatcher.getStaticContentLoader();
loader.setHostConfig(filterConfig);
return loader;
}
Expand Down
Loading