001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.messaging.handler.annotation.support; 018 019import java.util.Map; 020import java.util.concurrent.ConcurrentHashMap; 021 022import org.springframework.beans.factory.BeanFactory; 023import org.springframework.beans.factory.config.BeanExpressionContext; 024import org.springframework.beans.factory.config.BeanExpressionResolver; 025import org.springframework.beans.factory.config.ConfigurableBeanFactory; 026import org.springframework.core.MethodParameter; 027import org.springframework.core.convert.ConversionService; 028import org.springframework.core.convert.TypeDescriptor; 029import org.springframework.core.convert.support.DefaultConversionService; 030import org.springframework.messaging.Message; 031import org.springframework.messaging.handler.annotation.ValueConstants; 032import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; 033import org.springframework.util.ClassUtils; 034 035/** 036 * Abstract base class for resolving method arguments from a named value. Message headers, 037 * and path variables are examples of named values. Each may have a name, a required flag, 038 * and a default value. 039 * 040 * <p>Subclasses define how to do the following: 041 * <ul> 042 * <li>Obtain named value information for a method parameter 043 * <li>Resolve names into argument values 044 * <li>Handle missing argument values when argument values are required 045 * <li>Optionally handle a resolved value 046 * </ul> 047 * 048 * <p>A default value string can contain ${...} placeholders and Spring Expression 049 * Language {@code #{...}} expressions. For this to work a {@link ConfigurableBeanFactory} 050 * must be supplied to the class constructor. 051 * 052 * <p>A {@link ConversionService} may be used to apply type conversion to the resolved 053 * argument value if it doesn't match the method parameter type. 054 * 055 * @author Rossen Stoyanchev 056 * @author Juergen Hoeller 057 * @since 4.0 058 */ 059public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver { 060 061 private final ConversionService conversionService; 062 063 private final ConfigurableBeanFactory configurableBeanFactory; 064 065 private final BeanExpressionContext expressionContext; 066 067 private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = 068 new ConcurrentHashMap<MethodParameter, NamedValueInfo>(256); 069 070 071 /** 072 * Constructor with a {@link ConversionService} and a {@link BeanFactory}. 073 * @param cs conversion service for converting values to match the 074 * target method parameter type 075 * @param beanFactory a bean factory to use for resolving {@code ${...}} placeholder 076 * and {@code #{...}} SpEL expressions in default values, or {@code null} if default 077 * values are not expected to contain expressions 078 */ 079 protected AbstractNamedValueMethodArgumentResolver(ConversionService cs, ConfigurableBeanFactory beanFactory) { 080 this.conversionService = (cs != null ? cs : DefaultConversionService.getSharedInstance()); 081 this.configurableBeanFactory = beanFactory; 082 this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, null) : null); 083 } 084 085 086 @Override 087 public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception { 088 NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); 089 MethodParameter nestedParameter = parameter.nestedIfOptional(); 090 091 Object resolvedName = resolveStringValue(namedValueInfo.name); 092 if (resolvedName == null) { 093 throw new IllegalArgumentException( 094 "Specified name must not resolve to null: [" + namedValueInfo.name + "]"); 095 } 096 097 Object arg = resolveArgumentInternal(nestedParameter, message, resolvedName.toString()); 098 if (arg == null) { 099 if (namedValueInfo.defaultValue != null) { 100 arg = resolveStringValue(namedValueInfo.defaultValue); 101 } 102 else if (namedValueInfo.required && !nestedParameter.isOptional()) { 103 handleMissingValue(namedValueInfo.name, nestedParameter, message); 104 } 105 arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); 106 } 107 else if ("".equals(arg) && namedValueInfo.defaultValue != null) { 108 arg = resolveStringValue(namedValueInfo.defaultValue); 109 } 110 111 if (parameter != nestedParameter || !ClassUtils.isAssignableValue(parameter.getParameterType(), arg)) { 112 arg = this.conversionService.convert(arg, TypeDescriptor.forObject(arg), new TypeDescriptor(parameter)); 113 } 114 115 handleResolvedValue(arg, namedValueInfo.name, parameter, message); 116 117 return arg; 118 } 119 120 /** 121 * Obtain the named value for the given method parameter. 122 */ 123 private NamedValueInfo getNamedValueInfo(MethodParameter parameter) { 124 NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter); 125 if (namedValueInfo == null) { 126 namedValueInfo = createNamedValueInfo(parameter); 127 namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo); 128 this.namedValueInfoCache.put(parameter, namedValueInfo); 129 } 130 return namedValueInfo; 131 } 132 133 /** 134 * Create the {@link NamedValueInfo} object for the given method parameter. Implementations typically 135 * retrieve the method annotation by means of {@link MethodParameter#getParameterAnnotation(Class)}. 136 * @param parameter the method parameter 137 * @return the named value information 138 */ 139 protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter); 140 141 /** 142 * Create a new NamedValueInfo based on the given NamedValueInfo with sanitized values. 143 */ 144 private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) { 145 String name = info.name; 146 if (info.name.isEmpty()) { 147 name = parameter.getParameterName(); 148 if (name == null) { 149 throw new IllegalArgumentException( 150 "Name for argument of type [" + parameter.getNestedParameterType().getName() + 151 "] not specified, and parameter name information not found in class file either."); 152 } 153 } 154 String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue); 155 return new NamedValueInfo(name, info.required, defaultValue); 156 } 157 158 /** 159 * Resolve the given annotation-specified value, 160 * potentially containing placeholders and expressions. 161 */ 162 private Object resolveStringValue(String value) { 163 if (this.configurableBeanFactory == null) { 164 return value; 165 } 166 String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value); 167 BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver(); 168 if (exprResolver == null) { 169 return value; 170 } 171 return exprResolver.evaluate(placeholdersResolved, this.expressionContext); 172 } 173 174 /** 175 * Resolves the given parameter type and value name into an argument value. 176 * @param parameter the method parameter to resolve to an argument value 177 * @param message the current request 178 * @param name the name of the value being resolved 179 * @return the resolved argument. May be {@code null} 180 * @throws Exception in case of errors 181 */ 182 protected abstract Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name) 183 throws Exception; 184 185 /** 186 * Invoked when a named value is required, but 187 * {@link #resolveArgumentInternal(MethodParameter, Message, String)} returned {@code null} and 188 * there is no default value. Subclasses typically throw an exception in this case. 189 * @param name the name for the value 190 * @param parameter the method parameter 191 * @param message the message being processed 192 */ 193 protected abstract void handleMissingValue(String name, MethodParameter parameter, Message<?> message); 194 195 /** 196 * A {@code null} results in a {@code false} value for {@code boolean}s or an 197 * exception for other primitives. 198 */ 199 private Object handleNullValue(String name, Object value, Class<?> paramType) { 200 if (value == null) { 201 if (Boolean.TYPE.equals(paramType)) { 202 return Boolean.FALSE; 203 } 204 else if (paramType.isPrimitive()) { 205 throw new IllegalStateException("Optional " + paramType + " parameter '" + name + 206 "' is present but cannot be translated into a null value due to being " + 207 "declared as a primitive type. Consider declaring it as object wrapper " + 208 "for the corresponding primitive type."); 209 } 210 } 211 return value; 212 } 213 214 /** 215 * Invoked after a value is resolved. 216 * @param arg the resolved argument value 217 * @param name the argument name 218 * @param parameter the argument parameter type 219 * @param message the message 220 */ 221 protected void handleResolvedValue(Object arg, String name, MethodParameter parameter, Message<?> message) { 222 } 223 224 225 /** 226 * Represents the information about a named value, including name, whether it's 227 * required and a default value. 228 */ 229 protected static class NamedValueInfo { 230 231 private final String name; 232 233 private final boolean required; 234 235 private final String defaultValue; 236 237 protected NamedValueInfo(String name, boolean required, String defaultValue) { 238 this.name = name; 239 this.required = required; 240 this.defaultValue = defaultValue; 241 } 242 } 243 244}