All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.context.annotation.CommonAnnotationBeanPostProcessor Maven / Gradle / Ivy

There is a newer version: 6.1.13
Show newest version
/*
 * Copyright 2002-2022 the original author or authors.
 *
 * 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
 *
 *      https://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.springframework.context.annotation;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import jakarta.ejb.EJB;

import org.springframework.aop.TargetSource;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor;
import org.springframework.beans.factory.annotation.InjectionMetadata;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.config.EmbeddedValueResolver;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;

/**
 * {@link org.springframework.beans.factory.config.BeanPostProcessor} implementation
 * that supports common Java annotations out of the box, in particular the common
 * annotations in the {@code jakarta.annotation} package. These common Java
 * annotations are supported in many Jakarta EE technologies (e.g. JSF and JAX-RS).
 *
 * 

This post-processor includes support for the {@link jakarta.annotation.PostConstruct} * and {@link jakarta.annotation.PreDestroy} annotations - as init annotation * and destroy annotation, respectively - through inheriting from * {@link InitDestroyAnnotationBeanPostProcessor} with pre-configured annotation types. * *

The central element is the {@link jakarta.annotation.Resource} annotation * for annotation-driven injection of named beans, by default from the containing * Spring BeanFactory, with only {@code mappedName} references resolved in JNDI. * The {@link #setAlwaysUseJndiLookup "alwaysUseJndiLookup" flag} enforces JNDI lookups * equivalent to standard Jakarta EE resource injection for {@code name} references * and default names as well. The target beans can be simple POJOs, with no special * requirements other than the type having to match. * *

This post-processor also supports the EJB 3 {@link jakarta.ejb.EJB} annotation, * analogous to {@link jakarta.annotation.Resource}, with the capability to * specify both a local bean name and a global JNDI name for fallback retrieval. * The target beans can be plain POJOs as well as EJB 3 Session Beans in this case. * *

For default usage, resolving resource names as Spring bean names, * simply define the following in your application context: * *

 * <bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor"/>
* * For direct JNDI access, resolving resource names as JNDI resource references * within the Jakarta EE application's "java:comp/env/" namespace, use the following: * *
 * <bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor">
 *   <property name="alwaysUseJndiLookup" value="true"/>
 * </bean>
* * {@code mappedName} references will always be resolved in JNDI, * allowing for global JNDI names (including "java:" prefix) as well. The * "alwaysUseJndiLookup" flag just affects {@code name} references and * default names (inferred from the field name / property name). * *

NOTE: A default CommonAnnotationBeanPostProcessor will be registered * by the "context:annotation-config" and "context:component-scan" XML tags. * Remove or turn off the default annotation configuration there if you intend * to specify a custom CommonAnnotationBeanPostProcessor bean definition! *

NOTE: Annotation injection will be performed before XML injection; thus * the latter configuration will override the former for properties wired through * both approaches. * * @author Juergen Hoeller * @author Sam Brannen * @since 2.5 * @see #setAlwaysUseJndiLookup * @see #setResourceFactory * @see org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor * @see org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor */ @SuppressWarnings("serial") public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBeanPostProcessor implements InstantiationAwareBeanPostProcessor, BeanFactoryAware, Serializable { // Defensive reference to JNDI API for JDK 9+ (optional java.naming module) private static final boolean jndiPresent = ClassUtils.isPresent( "javax.naming.InitialContext", CommonAnnotationBeanPostProcessor.class.getClassLoader()); private static final Set> resourceAnnotationTypes = new LinkedHashSet<>(4); @Nullable private static final Class ejbClass; static { resourceAnnotationTypes.add(Resource.class); ejbClass = loadAnnotationType("jakarta.ejb.EJB"); if (ejbClass != null) { resourceAnnotationTypes.add(ejbClass); } } private final Set ignoredResourceTypes = new HashSet<>(1); private boolean fallbackToDefaultTypeMatch = true; private boolean alwaysUseJndiLookup = false; @Nullable private transient BeanFactory jndiFactory; @Nullable private transient BeanFactory resourceFactory; @Nullable private transient BeanFactory beanFactory; @Nullable private transient StringValueResolver embeddedValueResolver; private final transient Map injectionMetadataCache = new ConcurrentHashMap<>(256); /** * Create a new CommonAnnotationBeanPostProcessor, * with the init and destroy annotation types set to * {@link jakarta.annotation.PostConstruct} and {@link jakarta.annotation.PreDestroy}, * respectively. */ public CommonAnnotationBeanPostProcessor() { setOrder(Ordered.LOWEST_PRECEDENCE - 3); setInitAnnotationType(PostConstruct.class); setDestroyAnnotationType(PreDestroy.class); // java.naming module present on JDK 9+? if (jndiPresent) { this.jndiFactory = new SimpleJndiBeanFactory(); } } /** * Ignore the given resource type when resolving {@code @Resource} annotations. * @param resourceType the resource type to ignore */ public void ignoreResourceType(String resourceType) { Assert.notNull(resourceType, "Ignored resource type must not be null"); this.ignoredResourceTypes.add(resourceType); } /** * Set whether to allow a fallback to a type match if no explicit name has been * specified. The default name (i.e. the field name or bean property name) will * still be checked first; if a bean of that name exists, it will be taken. * However, if no bean of that name exists, a by-type resolution of the * dependency will be attempted if this flag is "true". *

Default is "true". Switch this flag to "false" in order to enforce a * by-name lookup in all cases, throwing an exception in case of no name match. * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#resolveDependency */ public void setFallbackToDefaultTypeMatch(boolean fallbackToDefaultTypeMatch) { this.fallbackToDefaultTypeMatch = fallbackToDefaultTypeMatch; } /** * Set whether to always use JNDI lookups equivalent to standard Jakarta EE resource * injection, even for {@code name} attributes and default names. *

Default is "false": Resource names are used for Spring bean lookups in the * containing BeanFactory; only {@code mappedName} attributes point directly * into JNDI. Switch this flag to "true" for enforcing Jakarta EE style JNDI lookups * in any case, even for {@code name} attributes and default names. * @see #setJndiFactory * @see #setResourceFactory */ public void setAlwaysUseJndiLookup(boolean alwaysUseJndiLookup) { this.alwaysUseJndiLookup = alwaysUseJndiLookup; } /** * Specify the factory for objects to be injected into {@code @Resource} / * {@code @EJB} annotated fields and setter methods, * for {@code mappedName} attributes that point directly into JNDI. * This factory will also be used if "alwaysUseJndiLookup" is set to "true" in order * to enforce JNDI lookups even for {@code name} attributes and default names. *

The default is a {@link org.springframework.jndi.support.SimpleJndiBeanFactory} * for JNDI lookup behavior equivalent to standard Jakarta EE resource injection. * @see #setResourceFactory * @see #setAlwaysUseJndiLookup */ public void setJndiFactory(BeanFactory jndiFactory) { Assert.notNull(jndiFactory, "BeanFactory must not be null"); this.jndiFactory = jndiFactory; } /** * Specify the factory for objects to be injected into {@code @Resource} / * {@code @EJB} annotated fields and setter methods, * for {@code name} attributes and default names. *

The default is the BeanFactory that this post-processor is defined in, * if any, looking up resource names as Spring bean names. Specify the resource * factory explicitly for programmatic usage of this post-processor. *

Specifying Spring's {@link org.springframework.jndi.support.SimpleJndiBeanFactory} * leads to JNDI lookup behavior equivalent to standard Jakarta EE resource injection, * even for {@code name} attributes and default names. This is the same behavior * that the "alwaysUseJndiLookup" flag enables. * @see #setAlwaysUseJndiLookup */ public void setResourceFactory(BeanFactory resourceFactory) { Assert.notNull(resourceFactory, "BeanFactory must not be null"); this.resourceFactory = resourceFactory; } @Override public void setBeanFactory(BeanFactory beanFactory) { Assert.notNull(beanFactory, "BeanFactory must not be null"); this.beanFactory = beanFactory; if (this.resourceFactory == null) { this.resourceFactory = beanFactory; } if (beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory) { this.embeddedValueResolver = new EmbeddedValueResolver(configurableBeanFactory); } } @Override public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { super.postProcessMergedBeanDefinition(beanDefinition, beanType, beanName); InjectionMetadata metadata = findResourceMetadata(beanName, beanType, null); metadata.checkConfigMembers(beanDefinition); } @Override public void resetBeanDefinition(String beanName) { this.injectionMetadataCache.remove(beanName); } @Override public Object postProcessBeforeInstantiation(Class beanClass, String beanName) { return null; } @Override public boolean postProcessAfterInstantiation(Object bean, String beanName) { return true; } @Override public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { InjectionMetadata metadata = findResourceMetadata(beanName, bean.getClass(), pvs); try { metadata.inject(bean, beanName, pvs); } catch (Throwable ex) { throw new BeanCreationException(beanName, "Injection of resource dependencies failed", ex); } return pvs; } private InjectionMetadata findResourceMetadata(String beanName, Class clazz, @Nullable PropertyValues pvs) { // Fall back to class name as cache key, for backwards compatibility with custom callers. String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName()); // Quick check on the concurrent map first, with minimal locking. InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey); if (InjectionMetadata.needsRefresh(metadata, clazz)) { synchronized (this.injectionMetadataCache) { metadata = this.injectionMetadataCache.get(cacheKey); if (InjectionMetadata.needsRefresh(metadata, clazz)) { if (metadata != null) { metadata.clear(pvs); } metadata = buildResourceMetadata(clazz); this.injectionMetadataCache.put(cacheKey, metadata); } } } return metadata; } private InjectionMetadata buildResourceMetadata(Class clazz) { if (!AnnotationUtils.isCandidateClass(clazz, resourceAnnotationTypes)) { return InjectionMetadata.EMPTY; } List elements = new ArrayList<>(); Class targetClass = clazz; do { final List currElements = new ArrayList<>(); ReflectionUtils.doWithLocalFields(targetClass, field -> { if (ejbClass != null && field.isAnnotationPresent(ejbClass)) { if (Modifier.isStatic(field.getModifiers())) { throw new IllegalStateException("@EJB annotation is not supported on static fields"); } currElements.add(new EjbRefElement(field, field, null)); } else if (field.isAnnotationPresent(Resource.class)) { if (Modifier.isStatic(field.getModifiers())) { throw new IllegalStateException("@Resource annotation is not supported on static fields"); } if (!this.ignoredResourceTypes.contains(field.getType().getName())) { currElements.add(new ResourceElement(field, field, null)); } } }); ReflectionUtils.doWithLocalMethods(targetClass, method -> { Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { return; } if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (ejbClass != null && bridgedMethod.isAnnotationPresent(ejbClass)) { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalStateException("@EJB annotation is not supported on static methods"); } if (method.getParameterCount() != 1) { throw new IllegalStateException("@EJB annotation requires a single-arg method: " + method); } PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); currElements.add(new EjbRefElement(method, bridgedMethod, pd)); } else if (bridgedMethod.isAnnotationPresent(Resource.class)) { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalStateException("@Resource annotation is not supported on static methods"); } Class[] paramTypes = method.getParameterTypes(); if (paramTypes.length != 1) { throw new IllegalStateException("@Resource annotation requires a single-arg method: " + method); } if (!this.ignoredResourceTypes.contains(paramTypes[0].getName())) { PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); currElements.add(new ResourceElement(method, bridgedMethod, pd)); } } } }); elements.addAll(0, currElements); targetClass = targetClass.getSuperclass(); } while (targetClass != null && targetClass != Object.class); return InjectionMetadata.forElements(elements, clazz); } /** * Obtain a lazily resolving resource proxy for the given name and type, * delegating to {@link #getResource} on demand once a method call comes in. * @param element the descriptor for the annotated field/method * @param requestingBeanName the name of the requesting bean * @return the resource object (never {@code null}) * @since 4.2 * @see #getResource * @see Lazy */ protected Object buildLazyResourceProxy(final LookupElement element, final @Nullable String requestingBeanName) { TargetSource ts = new TargetSource() { @Override public Class getTargetClass() { return element.lookupType; } @Override public boolean isStatic() { return false; } @Override public Object getTarget() { return getResource(element, requestingBeanName); } @Override public void releaseTarget(Object target) { } }; ProxyFactory pf = new ProxyFactory(); pf.setTargetSource(ts); if (element.lookupType.isInterface()) { pf.addInterface(element.lookupType); } ClassLoader classLoader = (this.beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory ? configurableBeanFactory.getBeanClassLoader() : null); return pf.getProxy(classLoader); } /** * Obtain the resource object for the given name and type. * @param element the descriptor for the annotated field/method * @param requestingBeanName the name of the requesting bean * @return the resource object (never {@code null}) * @throws NoSuchBeanDefinitionException if no corresponding target resource found */ protected Object getResource(LookupElement element, @Nullable String requestingBeanName) throws NoSuchBeanDefinitionException { // JNDI lookup to perform? String jndiName = null; if (StringUtils.hasLength(element.mappedName)) { jndiName = element.mappedName; } else if (this.alwaysUseJndiLookup) { jndiName = element.name; } if (jndiName != null) { if (this.jndiFactory == null) { throw new NoSuchBeanDefinitionException(element.lookupType, "No JNDI factory configured - specify the 'jndiFactory' property"); } return this.jndiFactory.getBean(jndiName, element.lookupType); } // Regular resource autowiring if (this.resourceFactory == null) { throw new NoSuchBeanDefinitionException(element.lookupType, "No resource factory configured - specify the 'resourceFactory' property"); } return autowireResource(this.resourceFactory, element, requestingBeanName); } /** * Obtain a resource object for the given name and type through autowiring * based on the given factory. * @param factory the factory to autowire against * @param element the descriptor for the annotated field/method * @param requestingBeanName the name of the requesting bean * @return the resource object (never {@code null}) * @throws NoSuchBeanDefinitionException if no corresponding target resource found */ protected Object autowireResource(BeanFactory factory, LookupElement element, @Nullable String requestingBeanName) throws NoSuchBeanDefinitionException { Object resource; Set autowiredBeanNames; String name = element.name; if (factory instanceof AutowireCapableBeanFactory autowireCapableBeanFactory) { DependencyDescriptor descriptor = element.getDependencyDescriptor(); if (this.fallbackToDefaultTypeMatch && element.isDefaultName && !factory.containsBean(name)) { autowiredBeanNames = new LinkedHashSet<>(); resource = autowireCapableBeanFactory.resolveDependency(descriptor, requestingBeanName, autowiredBeanNames, null); if (resource == null) { throw new NoSuchBeanDefinitionException(element.getLookupType(), "No resolvable resource object"); } } else { resource = autowireCapableBeanFactory.resolveBeanByName(name, descriptor); autowiredBeanNames = Collections.singleton(name); } } else { resource = factory.getBean(name, element.lookupType); autowiredBeanNames = Collections.singleton(name); } if (factory instanceof ConfigurableBeanFactory configurableBeanFactory) { for (String autowiredBeanName : autowiredBeanNames) { if (requestingBeanName != null && configurableBeanFactory.containsBean(autowiredBeanName)) { configurableBeanFactory.registerDependentBean(autowiredBeanName, requestingBeanName); } } } return resource; } @SuppressWarnings("unchecked") @Nullable private static Class loadAnnotationType(String name) { try { return (Class) ClassUtils.forName(name, CommonAnnotationBeanPostProcessor.class.getClassLoader()); } catch (ClassNotFoundException ex) { return null; } } /** * Class representing generic injection information about an annotated field * or setter method, supporting @Resource and related annotations. */ protected abstract static class LookupElement extends InjectionMetadata.InjectedElement { protected String name = ""; protected boolean isDefaultName = false; protected Class lookupType = Object.class; @Nullable protected String mappedName; public LookupElement(Member member, @Nullable PropertyDescriptor pd) { super(member, pd); } /** * Return the resource name for the lookup. */ public final String getName() { return this.name; } /** * Return the desired type for the lookup. */ public final Class getLookupType() { return this.lookupType; } /** * Build a DependencyDescriptor for the underlying field/method. */ public final DependencyDescriptor getDependencyDescriptor() { if (this.isField) { return new LookupDependencyDescriptor((Field) this.member, this.lookupType); } else { return new LookupDependencyDescriptor((Method) this.member, this.lookupType); } } } /** * Class representing injection information about an annotated field * or setter method, supporting the @Resource annotation. */ private class ResourceElement extends LookupElement { private final boolean lazyLookup; public ResourceElement(Member member, AnnotatedElement ae, @Nullable PropertyDescriptor pd) { super(member, pd); Resource resource = ae.getAnnotation(Resource.class); String resourceName = resource.name(); Class resourceType = resource.type(); this.isDefaultName = !StringUtils.hasLength(resourceName); if (this.isDefaultName) { resourceName = this.member.getName(); if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) { resourceName = StringUtils.uncapitalizeAsProperty(resourceName.substring(3)); } } else if (embeddedValueResolver != null) { resourceName = embeddedValueResolver.resolveStringValue(resourceName); } if (Object.class != resourceType) { checkResourceType(resourceType); } else { // No resource type specified... check field/method. resourceType = getResourceType(); } this.name = (resourceName != null ? resourceName : ""); this.lookupType = resourceType; String lookupValue = resource.lookup(); this.mappedName = (StringUtils.hasLength(lookupValue) ? lookupValue : resource.mappedName()); Lazy lazy = ae.getAnnotation(Lazy.class); this.lazyLookup = (lazy != null && lazy.value()); } @Override protected Object getResourceToInject(Object target, @Nullable String requestingBeanName) { return (this.lazyLookup ? buildLazyResourceProxy(this, requestingBeanName) : getResource(this, requestingBeanName)); } } /** * Class representing injection information about an annotated field * or setter method, supporting the @EJB annotation. */ private class EjbRefElement extends LookupElement { private final String beanName; public EjbRefElement(Member member, AnnotatedElement ae, @Nullable PropertyDescriptor pd) { super(member, pd); EJB resource = ae.getAnnotation(EJB.class); String resourceBeanName = resource.beanName(); String resourceName = resource.name(); this.isDefaultName = !StringUtils.hasLength(resourceName); if (this.isDefaultName) { resourceName = this.member.getName(); if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) { resourceName = StringUtils.uncapitalizeAsProperty(resourceName.substring(3)); } } Class resourceType = resource.beanInterface(); if (Object.class != resourceType) { checkResourceType(resourceType); } else { // No resource type specified... check field/method. resourceType = getResourceType(); } this.beanName = resourceBeanName; this.name = resourceName; this.lookupType = resourceType; this.mappedName = resource.mappedName(); } @Override protected Object getResourceToInject(Object target, @Nullable String requestingBeanName) { if (StringUtils.hasLength(this.beanName)) { if (beanFactory != null && beanFactory.containsBean(this.beanName)) { // Local match found for explicitly specified local bean name. Object bean = beanFactory.getBean(this.beanName, this.lookupType); if (requestingBeanName != null && beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory) { configurableBeanFactory.registerDependentBean(this.beanName, requestingBeanName); } return bean; } else if (this.isDefaultName && !StringUtils.hasLength(this.mappedName)) { throw new NoSuchBeanDefinitionException(this.beanName, "Cannot resolve 'beanName' in local BeanFactory. Consider specifying a general 'name' value instead."); } } // JNDI name lookup - may still go to a local BeanFactory. return getResource(this, requestingBeanName); } } /** * Extension of the DependencyDescriptor class, * overriding the dependency type with the specified resource type. */ private static class LookupDependencyDescriptor extends DependencyDescriptor { private final Class lookupType; public LookupDependencyDescriptor(Field field, Class lookupType) { super(field, true); this.lookupType = lookupType; } public LookupDependencyDescriptor(Method method, Class lookupType) { super(new MethodParameter(method, 0), true); this.lookupType = lookupType; } @Override public Class getDependencyType() { return this.lookupType; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy