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.reactive; 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.lang.Nullable; 030import org.springframework.messaging.Message; 031import org.springframework.messaging.handler.annotation.ValueConstants; 032import org.springframework.messaging.handler.invocation.reactive.SyncHandlerMethodArgumentResolver; 033import org.springframework.util.ClassUtils; 034 035/** 036 * Abstract base class to resolve method arguments from a named value, e.g. 037 * message headers or destination variables. Named values could have one or more 038 * of a name, a required flag, and a default value. 039 * 040 * <p>Subclasses only need to define specific steps such as how to obtain named 041 * value details from a method parameter, how to resolve to argument values, or 042 * how to handle missing values. 043 * 044 * <p>A default value string can contain ${...} placeholders and Spring 045 * Expression Language {@code #{...}} expressions which will be resolved if a 046 * {@link ConfigurableBeanFactory} is supplied to the class constructor. 047 * 048 * <p>A {@link ConversionService} is used to convert a resolved String argument 049 * value to the expected target method parameter type. 050 * 051 * @author Rossen Stoyanchev 052 * @since 5.2 053 */ 054public abstract class AbstractNamedValueMethodArgumentResolver implements SyncHandlerMethodArgumentResolver { 055 056 private final ConversionService conversionService; 057 058 @Nullable 059 private final ConfigurableBeanFactory configurableBeanFactory; 060 061 @Nullable 062 private final BeanExpressionContext expressionContext; 063 064 private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256); 065 066 067 /** 068 * Constructor with a {@link ConversionService} and a {@link BeanFactory}. 069 * @param conversionService conversion service for converting String values 070 * to the target method parameter type 071 * @param beanFactory a bean factory for resolving {@code ${...}} 072 * placeholders and {@code #{...}} SpEL expressions in default values 073 */ 074 protected AbstractNamedValueMethodArgumentResolver(ConversionService conversionService, 075 @Nullable ConfigurableBeanFactory beanFactory) { 076 077 this.conversionService = conversionService; 078 this.configurableBeanFactory = beanFactory; 079 this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, null) : null); 080 } 081 082 083 @Override 084 public Object resolveArgumentValue(MethodParameter parameter, Message<?> message) { 085 NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); 086 MethodParameter nestedParameter = parameter.nestedIfOptional(); 087 088 Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name); 089 if (resolvedName == null) { 090 throw new IllegalArgumentException( 091 "Specified name must not resolve to null: [" + namedValueInfo.name + "]"); 092 } 093 094 Object arg = resolveArgumentInternal(nestedParameter, message, resolvedName.toString()); 095 if (arg == null) { 096 if (namedValueInfo.defaultValue != null) { 097 arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); 098 } 099 else if (namedValueInfo.required && !nestedParameter.isOptional()) { 100 handleMissingValue(namedValueInfo.name, nestedParameter, message); 101 } 102 arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); 103 } 104 else if ("".equals(arg) && namedValueInfo.defaultValue != null) { 105 arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); 106 } 107 108 if (parameter != nestedParameter || !ClassUtils.isAssignableValue(parameter.getParameterType(), arg)) { 109 arg = this.conversionService.convert(arg, TypeDescriptor.forObject(arg), new TypeDescriptor(parameter)); 110 } 111 112 return arg; 113 } 114 115 /** 116 * Obtain the named value for the given method parameter. 117 */ 118 private NamedValueInfo getNamedValueInfo(MethodParameter parameter) { 119 NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter); 120 if (namedValueInfo == null) { 121 namedValueInfo = createNamedValueInfo(parameter); 122 namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo); 123 this.namedValueInfoCache.put(parameter, namedValueInfo); 124 } 125 return namedValueInfo; 126 } 127 128 /** 129 * Create the {@link NamedValueInfo} object for the given method parameter. 130 * Implementations typically retrieve the method annotation by means of 131 * {@link MethodParameter#getParameterAnnotation(Class)}. 132 * @param parameter the method parameter 133 * @return the named value information 134 */ 135 protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter); 136 137 /** 138 * Fall back on the parameter name from the class file if necessary and 139 * replace {@link ValueConstants#DEFAULT_NONE} with null. 140 */ 141 private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) { 142 String name = info.name; 143 if (info.name.isEmpty()) { 144 name = parameter.getParameterName(); 145 if (name == null) { 146 throw new IllegalArgumentException( 147 "Name for argument of type [" + parameter.getNestedParameterType().getName() + 148 "] not specified, and parameter name information not found in class file either."); 149 } 150 } 151 return new NamedValueInfo(name, info.required, 152 ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue); 153 } 154 155 /** 156 * Resolve the given annotation-specified value, 157 * potentially containing placeholders and expressions. 158 */ 159 @Nullable 160 private Object resolveEmbeddedValuesAndExpressions(String value) { 161 if (this.configurableBeanFactory == null || this.expressionContext == null) { 162 return value; 163 } 164 String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value); 165 BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver(); 166 if (exprResolver == null) { 167 return value; 168 } 169 return exprResolver.evaluate(placeholdersResolved, this.expressionContext); 170 } 171 172 /** 173 * Resolves the given parameter type and value name into an argument value. 174 * @param parameter the method parameter to resolve to an argument value 175 * @param message the current request 176 * @param name the name of the value being resolved 177 * @return the resolved argument. May be {@code null} 178 */ 179 @Nullable 180 protected abstract Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name); 181 182 /** 183 * Invoked when a value is required, but {@link #resolveArgumentInternal} 184 * returned {@code null} and there is no default value. Sub-classes can 185 * throw an appropriate exception for this case. 186 * @param name the name for the value 187 * @param parameter the target method parameter 188 * @param message the message being processed 189 */ 190 protected abstract void handleMissingValue(String name, MethodParameter parameter, Message<?> message); 191 192 /** 193 * One last chance to handle a possible null value. 194 * Specifically for booleans method parameters, use {@link Boolean#FALSE}. 195 * Also raise an ISE for primitive types. 196 */ 197 @Nullable 198 private Object handleNullValue(String name, @Nullable Object value, Class<?> paramType) { 199 if (value == null) { 200 if (Boolean.TYPE.equals(paramType)) { 201 return Boolean.FALSE; 202 } 203 else if (paramType.isPrimitive()) { 204 throw new IllegalStateException("Optional " + paramType + " parameter '" + name + 205 "' is present but cannot be translated into a null value due to being " + 206 "declared as a primitive type. Consider declaring it as object wrapper " + 207 "for the corresponding primitive type."); 208 } 209 } 210 return value; 211 } 212 213 214 /** 215 * Represents a named value declaration. 216 */ 217 protected static class NamedValueInfo { 218 219 private final String name; 220 221 private final boolean required; 222 223 @Nullable 224 private final String defaultValue; 225 226 protected NamedValueInfo(String name, boolean required, @Nullable String defaultValue) { 227 this.name = name; 228 this.required = required; 229 this.defaultValue = defaultValue; 230 } 231 } 232 233}