From 1614e5d6fde4780689263f60faf860af35a39b92 Mon Sep 17 00:00:00 2001 From: "Lincoln Baxter, III" Date: Tue, 21 Mar 2023 17:23:38 -0400 Subject: [PATCH] feat(spring): support thread-safe Spring Boot WebApplicationContext initialization and lookup --- .../rewrite/el/TypeBasedExpression.java | 56 +++++++----- .../spring/SpringBeanNameResolver.java | 25 +++--- .../SpringExpressionLanguageProvider.java | 8 ++ .../rewrite/spring/SpringServiceEnricher.java | 23 ++++- .../rewrite/spring/SpringServiceLocator.java | 10 +-- .../spring/SpringServletContextLoader.java | 86 +++++++++++++++++++ ...cpsoft.rewrite.servlet.spi.ContextListener | 1 + ...cpsoft.rewrite.servlet.spi.RequestListener | 1 + 8 files changed, 169 insertions(+), 41 deletions(-) create mode 100644 integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringServletContextLoader.java create mode 100644 integration-spring/src/main/resources/META-INF/services/org.ocpsoft.rewrite.servlet.spi.ContextListener create mode 100644 integration-spring/src/main/resources/META-INF/services/org.ocpsoft.rewrite.servlet.spi.RequestListener diff --git a/api-el/src/main/java/org/ocpsoft/rewrite/el/TypeBasedExpression.java b/api-el/src/main/java/org/ocpsoft/rewrite/el/TypeBasedExpression.java index 9c35621b3..d091d6344 100644 --- a/api-el/src/main/java/org/ocpsoft/rewrite/el/TypeBasedExpression.java +++ b/api-el/src/main/java/org/ocpsoft/rewrite/el/TypeBasedExpression.java @@ -15,7 +15,9 @@ */ package org.ocpsoft.rewrite.el; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; import org.ocpsoft.common.services.ServiceLoader; import org.ocpsoft.logging.Logger; @@ -62,43 +64,55 @@ public String getExpression() @SuppressWarnings("unchecked") private String lookupBeanName() { - // load the available SPI implementations Iterator iterator = ServiceLoader.load(BeanNameResolver.class).iterator(); + + List deferred = new ArrayList<>(); while (iterator.hasNext()) { BeanNameResolver resolver = iterator.next(); - // check if this implementation is able to tell the name - String beanName = resolver.getBeanName(clazz); - - if (log.isTraceEnabled()) { - log.trace("Service provider [{}] returned [{}] for class [{}]", new Object[] { - resolver.getClass().getSimpleName(), beanName, clazz.getName() - }); - } - - // the first result is accepted - if (beanName != null) { - - // create the complete EL expression including the component - String el = new StringBuilder() - .append(beanName).append('.').append(component) - .toString(); + try { + // check if this implementation is able to tell the name + String beanName = resolver.getBeanName(clazz); if (log.isTraceEnabled()) { - log.debug("Creation of EL expression for component [{}] of class [{}] successful: {}", new Object[] { - component, clazz.getName(), el + log.trace("Service provider [{}] returned [{}] for class [{}]", new Object[] { + resolver.getClass().getSimpleName(), beanName, clazz.getName() }); } - return el; + // the first result is accepted + if (beanName != null) { + + // create the complete EL expression including the component + String el = new StringBuilder() + .append(beanName).append('.').append(component) + .toString(); + + if (log.isTraceEnabled()) { + log.debug("Creation of EL expression for component [{}] of class [{}] successful: {}", new Object[] { + component, clazz.getName(), el + }); + } + + return el; + } + } + catch (Exception e) { + log.debug("Failed to resolve bean names using [" + resolver.getClass().getName() + "]", e); + deferred.add(e); } } + if (deferred.size() > 1) { + for (Exception e : deferred) { + log.error("Failed to resolve bean names.", e); + } + } throw new IllegalStateException("Unable to obtain EL name for bean of type [" + clazz.getName() + "] from any of the SPI implementations. You should conside placing a @" - + ELBeanName.class.getSimpleName() + " on the class."); + + ELBeanName.class.getSimpleName() + " on the class.", (deferred.size() == 1 ? deferred.get(0) : null)); } diff --git a/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringBeanNameResolver.java b/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringBeanNameResolver.java index 418b72a9b..1103ed355 100644 --- a/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringBeanNameResolver.java +++ b/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringBeanNameResolver.java @@ -16,12 +16,12 @@ package org.ocpsoft.rewrite.spring; import java.util.HashSet; -import java.util.Map; import java.util.Set; import org.ocpsoft.logging.Logger; import org.ocpsoft.rewrite.el.spi.BeanNameResolver; import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; @@ -29,24 +29,28 @@ * {@link BeanNameResolver} implementation for Spring. * * @author Christian Kaltepoth + * @author Lincoln Baxter, III */ public class SpringBeanNameResolver implements BeanNameResolver { private final Logger log = Logger.getLogger(SpringBeanNameResolver.class); + @Autowired + private WebApplicationContext applicationContext; + @Override public String getBeanName(Class clazz) { - - // try to obtain the WebApplicationContext using ContextLoader - WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext(); - if (context == null) { - throw new IllegalStateException("Unable to get current WebApplicationContext"); + if (applicationContext == null) { + applicationContext = ContextLoader.getCurrentWebApplicationContext(); + if (applicationContext == null) { + throw new IllegalStateException("Unable to get current WebApplicationContext"); + } } // obtain a map of bean names - Set beanNames = resolveBeanNames(context, clazz); + Set beanNames = resolveBeanNames(applicationContext, clazz); // no beans of that type, nothing we can do if (beanNames == null || beanNames.size() == 0) { @@ -76,15 +80,14 @@ private Set resolveBeanNames(ListableBeanFactory beanFactory, Class c final Set result = new HashSet(); - Map beanMap = beanFactory.getBeansOfType(clazz); - if (beanMap != null) { - for (String name : beanMap.keySet()) { + String[] names = beanFactory.getBeanNamesForType(clazz); + if (names != null) { + for (String name : names) { if (name != null && !name.startsWith("scopedTarget.")) { result.add(name); } } } - return result; } diff --git a/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringExpressionLanguageProvider.java b/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringExpressionLanguageProvider.java index 0fa5916db..0c27ee045 100644 --- a/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringExpressionLanguageProvider.java +++ b/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringExpressionLanguageProvider.java @@ -33,6 +33,7 @@ import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardTypeConverter; import org.springframework.expression.spel.support.StandardTypeLocator; +import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; /** @@ -136,6 +137,13 @@ public EvaluationContext getEvaluationContext() // we need a ConfigurableBeanFactory to build the BeanExpressionContext ConfigurableBeanFactory beanFactory = null; + if (applicationContext == null) { + applicationContext = ContextLoader.getCurrentWebApplicationContext(); + if (applicationContext == null) { + throw new IllegalStateException("Unable to get current WebApplicationContext"); + } + } + // the WebApplicationContext MAY implement ConfigurableBeanFactory if (applicationContext instanceof ConfigurableBeanFactory) { beanFactory = (ConfigurableBeanFactory) applicationContext; diff --git a/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringServiceEnricher.java b/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringServiceEnricher.java index 0c29acaf6..c61a4632d 100644 --- a/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringServiceEnricher.java +++ b/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringServiceEnricher.java @@ -17,9 +17,13 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.stream.Collectors; + +import javax.servlet.ServletContext; import org.ocpsoft.common.spi.ServiceEnricher; import org.ocpsoft.logging.Logger; +import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.SpringBeanAutowiringSupport; /** @@ -35,7 +39,13 @@ public class SpringServiceEnricher implements ServiceEnricher @Override public void enrich(final T service) { - SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(service); + ServletContext context = SpringServletContextLoader.findCurrentServletContext(); + if (context != null) { + SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(service, context); + } + else { + SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(service); + } if (log.isDebugEnabled()) log.debug("Enriched instance of service [" + service.getClass().getName() + "]"); @@ -44,8 +54,15 @@ public void enrich(final T service) @Override public Collection produce(final Class type) { - // TODO implement - return new ArrayList(); + WebApplicationContext webApplicationContext = SpringServletContextLoader.findCurrentApplicationContext(); + + if(webApplicationContext == null) { + return new ArrayList<>(); + } + + return webApplicationContext.getBeanProvider(type) + .stream() + .collect(Collectors.toList()); } } diff --git a/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringServiceLocator.java b/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringServiceLocator.java index 62f43652b..c76bd267b 100644 --- a/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringServiceLocator.java +++ b/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringServiceLocator.java @@ -21,31 +21,29 @@ import java.util.Set; import org.ocpsoft.common.spi.ServiceLocator; -import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; /** * {@link ServiceLocator} implementation for Spring. * * @author Christian Kaltepoth + * @author Lincoln Baxter, III */ public class SpringServiceLocator implements ServiceLocator { - @Override @SuppressWarnings("unchecked") public Collection> locate(Class clazz) { Set> result = new LinkedHashSet>(); - // use the Spring API to obtain the WebApplicationContext - WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext(); + WebApplicationContext applicationContext = SpringServletContextLoader.findCurrentApplicationContext(); // may be null if Spring hasn't started yet - if (context != null) { + if (applicationContext != null) { // ask spring about SPI implementations - Map beans = context.getBeansOfType(clazz); + Map beans = applicationContext.getBeansOfType(clazz); // add the implementations Class objects to the result set for (T type : beans.values()) { diff --git a/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringServletContextLoader.java b/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringServletContextLoader.java new file mode 100644 index 000000000..2dc717959 --- /dev/null +++ b/integration-spring/src/main/java/org/ocpsoft/rewrite/spring/SpringServletContextLoader.java @@ -0,0 +1,86 @@ +/* + * Copyright 2011 Lincoln Baxter, III + * + * Licensed 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.ocpsoft.rewrite.spring; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; + +import org.ocpsoft.rewrite.servlet.spi.ContextListener; +import org.springframework.web.context.ContextLoader; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.context.support.WebApplicationContextUtils; + +/** + * Thread-safe {@link ServletContext} loader implementation for Spring. + * + * @author Lincoln Baxter, III + */ +public class SpringServletContextLoader implements ContextListener { + private static final Map contextMap = new ConcurrentHashMap<>(1); + + @Override + public void contextInitialized(ServletContextEvent event) + { + ServletContext servletContext = event.getServletContext(); + contextMap.put(Thread.currentThread().getContextClassLoader(), servletContext); + contextMap.put(servletContext.getClassLoader(), servletContext); + } + + @Override + public void contextDestroyed(ServletContextEvent event) + { + ServletContext context = event.getServletContext(); + contextMap.entrySet().removeIf(entry -> entry.getValue() == context); + } + + public static ServletContext findCurrentServletContext() + { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + + if (requestAttributes instanceof ServletRequestAttributes) { + return ((ServletRequestAttributes) requestAttributes).getRequest().getServletContext(); + } + + return contextMap.get(Thread.currentThread().getContextClassLoader()); + } + + public static WebApplicationContext findCurrentApplicationContext() + { + ServletContext currentServletContext = findCurrentServletContext(); + + if (currentServletContext != null) { + WebApplicationContext webApplicationContext = WebApplicationContextUtils.findWebApplicationContext(currentServletContext); + if (webApplicationContext != null) { + return webApplicationContext; + } + } + + return ContextLoader.getCurrentWebApplicationContext(); + } + + @Override + public int priority() + { + return 0; + } + +} diff --git a/integration-spring/src/main/resources/META-INF/services/org.ocpsoft.rewrite.servlet.spi.ContextListener b/integration-spring/src/main/resources/META-INF/services/org.ocpsoft.rewrite.servlet.spi.ContextListener new file mode 100644 index 000000000..8e610f7fe --- /dev/null +++ b/integration-spring/src/main/resources/META-INF/services/org.ocpsoft.rewrite.servlet.spi.ContextListener @@ -0,0 +1 @@ +org.ocpsoft.rewrite.spring.SpringServletContextLoader \ No newline at end of file diff --git a/integration-spring/src/main/resources/META-INF/services/org.ocpsoft.rewrite.servlet.spi.RequestListener b/integration-spring/src/main/resources/META-INF/services/org.ocpsoft.rewrite.servlet.spi.RequestListener new file mode 100644 index 000000000..8e610f7fe --- /dev/null +++ b/integration-spring/src/main/resources/META-INF/services/org.ocpsoft.rewrite.servlet.spi.RequestListener @@ -0,0 +1 @@ +org.ocpsoft.rewrite.spring.SpringServletContextLoader \ No newline at end of file