Why does Spring #Cacheable not pass the annotated method's result type to its deserializer? - spring

This is sample code with Kotlin.
#Configuration
#Bean("cacheManager1hour")
fun cacheManager1hour(#Qualifier("cacheConfig") cacheConfiguration: RedisCacheConfiguration, redisConnectionFactory: RedisConnectionFactory): CacheManager {
cacheConfiguration.entryTtl(Duration.ofSeconds(60 * 60))
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(cacheConfiguration)
.build()
}
#Bean("cacheConfig")
fun cacheConfig(objectMapper:ObjectMapper): RedisCacheConfiguration {
return RedisCacheConfiguration.defaultCacheConfig()
.computePrefixWith { cacheName -> "yaya:$cacheName:" }
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer()))
}
#RestController
#Cacheable(value = "book", key = "#root.methodName", cacheManager = "cacheManager1hour")
fun getBook(): Book {
return Book()
}
class Book {
var asdasd:String? = "TEST"
var expires_in = 123
}
The GenericJackson2JsonRedisSerializer cannot process the "kotlin class" and we need to add '#class as property' to the Redis cache entry.
Anyway, why do we need the #class? The Spring context is aware of the result's type, why doesn't it get passed? We would have two benefits:
less memory
easy for the Serializer, i.e. objectMapper.readValue(str, T)
Annotated Spring code for illustration
// org.springframework.cache.interceptor.CacheAspectSupport
#Nullable
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
for (Cache cache : context.getCaches()) {
// --> maybe we can pass the context.method.returnType to doGet
Cache.ValueWrapper wrapper = doGet(cache, key);
if (wrapper != null) {
if (logger.isTraceEnabled()) {
logger.trace("Cache entry for key '" + key + "' found in cache '" +
cache.getName() + "'");
}
return wrapper;
}
}
return null;
}
// org.springframework.data.redis.cache.RedisCache
#Override
protected Object lookup(Object key) {
// -> there will get the deserialized type can pass to Jackson
byte[] value = cacheWriter.get(name, createAndConvertCacheKey(key));
if (value == null) {
return null;
}
return deserializeCacheValue(value);
}

Your return type could be:
some abstract class
some interface
In those cases your return type is almost useless to deserialize the object. Encoding the actual class always works .

Related

Object Mapper generic deserialize

I have the problem with Object Mapper deserialize.
I use hibernate + spring boot and needed to deserialize json array for entity field.
This is my hibernate AttributeConverter:
abstract class JsonConverter<T>: AttributeConverter<Collection<T>, String> {
abstract val typeReference: TypeReference<out Collection<T>>
override fun convertToDatabaseColumn(attribute: Collection<T>?): String? {
if (attribute.isNullOrEmpty())
return null
return ObjectMapper.writeValueAsString(attribute)
}
}
implementation for collection
abstract class ListConverter<T>: JsonConverter<T>() {
override val typeReference = object : TypeReference<List<T>>() {}
override fun convertToEntityAttribute(dbData: String?): Collection<T> {
if (dbData == null)
return emptyList()
return ObjectMapper.readValue(dbData, typeReference)
}
}
abstract class SetConverter<T>: JsonConverter<T>() {
override val typeReference = object : TypeReference<Set<T>>() {}
override fun convertToEntityAttribute(dbData: String?): Collection<T> {
if (dbData == null)
return emptySet()
return ObjectMapper.readValue(dbData, typeReference)
}
}
Hibernate entity field
#Column(name = "product_ids")
#Convert(converter = LongSetConverter::class)
var productIds: Set<Long>,
implementation AttributeConverter for this entity field
class LongSetConverter: SetConverter<Long>()
The problem is that Long Generic not working and Object Mapper always return Set of Int
Long (or another type) just ingnore

more than one 'primary' service instance suppliers found during load balancing (spring boot/cloud)

I'm currently updating from Spring boot 2.2.x to 2.6.x + legacy code, it's a big jump so there were multiple changes. I'm now running into a problem with load balancing through an api-gateway. I'll apologize in advance for the wall of code to come. I will put the point of failure at the bottom.
When I send in an API request, I get the following error:
more than one 'primary' bean found among candidates: [zookeeperDiscoveryClientServiceInstanceListSupplier, serviceInstanceListSupplier, retryAwareDiscoveryClientServiceInstanceListSupplier]
it seems that the zookeeperDiscovery and retryAware suppliers are loaded through the default serviceInsatnceListSupplier, which has #Primary over it. I thought would take precedence over the other ones. I assume I must be doing something wrong due changes in the newer version, here are the relevant code in question:
#Configuration
#LoadBalancerClients(defaultConfiguration = ClientConfiguration.class)
public class WebClientConfiguration {
#Bean
#Qualifier("microserviceWebClient")
#ConditionalOnMissingBean(name = "microserviceWebClient")
public WebClient microserviceWebClient(#Qualifier("microserviceWebClientBuilder") WebClient.Builder builder) {
return builder.build();
}
#Bean
#Qualifier("microserviceWebClientBuilder")
#ConditionalOnMissingBean(name = "microserviceWebClientBuilder")
#LoadBalanced
public WebClient.Builder microserviceWebClientBuilder() {
return WebClient.builder();
}
#Bean
#Primary
public ReactorLoadBalancerExchangeFilterFunction reactorLoadBalancerExchangeFilterFunction(
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory) {
//the transformer is currently null, there wasn't a transformer before the upgrade
return new CustomExchangeFilterFunction(loadBalancerFactory, transformer);
}
}
There are also some Feign Client related configs here which I will omit, since it's not (or shouldn't be) playing a role in this problem:
public class ClientConfiguration {
/**
* The property key within the feign clients configuration context for the feign client name.
*/
public static final String FEIGN_CLIENT_NAME_PROPERTY = "feign.client.name";
public ClientConfiguration() {
}
//Creates a new BiPredicate for shouldClose. This will be used to determine if HTTP Connections should be automatically closed or not.
#Bean
#ConditionalOnMissingBean
public BiPredicate<Response, Type> shouldClose() {
return (Response response, Type type) -> {
if(type instanceof Class) {
Class<?> currentClass = (Class<?>) type;
return (null == AnnotationUtils.getAnnotation(currentClass, EnableResponseStream.class));
}
return true;
};
}
//Creates a Custom Decoder
#Bean
public Decoder createCustomDecoder(
ObjectFactory<HttpMessageConverters> converters, BiPredicate<Response, Type> shouldClose
) {
return new CustomDecoder(converters, shouldClose);
}
#Bean
#Qualifier("loadBalancerName")
public String loadBalancerName(PropertyResolver propertyResolver) {
String name = propertyResolver.getProperty(FEIGN_CLIENT_NAME_PROPERTY);
if(StringUtils.hasText(name)) {
// we are in a feign context
return name;
}
// we are in a LoadBalancerClientFactory context
name = propertyResolver.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
Assert.notNull(name, "Could not find a load balancer name within the configuration context!");
return name;
}
#Bean
public ReactorServiceInstanceLoadBalancer reactorServiceInstanceLoadBalancer(
BeanFactory beanFactory, #Qualifier("loadBalancerName") String loadBalancerName
) {
return new CustomRoundRobinLoadBalancer(
beanFactory.getBeanProvider(ServiceInstanceListSupplier.class),
loadBalancerName
);
}
#Bean
#Primary
public ServiceInstanceListSupplier serviceInstanceListSupplier(
#Qualifier(
"filter"
) Predicate<ServiceInstance> filter, DiscoveryClient discoveryClient, Environment environment, #Qualifier(
"loadBalancerName"
) String loadBalancerName
) {
// add service name to environment if necessary
if(environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME) == null) {
StandardEnvironment wrapped = new StandardEnvironment();
if(environment instanceof ConfigurableEnvironment) {
((ConfigurableEnvironment) environment).getPropertySources()
.forEach(s -> wrapped.getPropertySources().addLast(s));
}
Map<String, Object> additionalProperties = new HashMap<>();
additionalProperties.put(LoadBalancerClientFactory.PROPERTY_NAME, loadBalancerName);
wrapped.getPropertySources().addLast(new MapPropertySource(loadBalancerName, additionalProperties));
environment = wrapped;
}
return new FilteringInstanceListSupplier(filter, discoveryClient, environment);
}
}
There was a change in the ExchangeFilter constructor, but as far as I can tell, it accepts that empty transformer,I don't know if it's supposed to:
public class CustomExchangeFilterFunction extends ReactorLoadBalancerExchangeFilterFunction {
private static final ThreadLocal<ClientRequest> REQUEST_HOLDER = new ThreadLocal<>();
//I think it's wrong but I don't know what to do here
private static List<LoadBalancerClientRequestTransformer> transformersList;
private final Factory<ServiceInstance> loadBalancerFactory;
public CustomExchangeFilterFunction (Factory<ServiceInstance> loadBalancerFactory) {
this(loadBalancerFactory);
///according to docs, but I don't know where and if I need to use this
#Bean
public LoadBalancerClientRequestTransformer transformer() {
return new LoadBalancerClientRequestTransformer() {
#Override
public ClientRequest transformRequest(ClientRequest request, ServiceInstance instance) {
return ClientRequest.from(request)
.header(instance.getInstanceId())
.build();
}
};
}
public CustomExchangeFilterFunction (Factory<ServiceInstance> loadBalancerFactory, List<LoadBalancerClientRequestTransformer> transformersList) {
super(loadBalancerFactory, transformersList); //the changed constructor
this.loadBalancerFactory = loadBalancerFactory;;
}
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
// put the current request into the thread context - ugly, but couldn't find a better way to access the request within
// the choose method without reimplementing nearly everything
REQUEST_HOLDER.set(request);
try {
return super.filter(request, next);
} finally {
REQUEST_HOLDER.remove();
}
}
//used to be an override, but the function has changed
//code execution doesn't even get this far yet
protected Mono<Response<ServiceInstance>> choose(String serviceId) {
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerFactory.getInstance(serviceId);
if(loadBalancer == null) {
return Mono.just(new EmptyResponse());
}
ClientRequest request = REQUEST_HOLDER.get();
// this might be null, if the underlying implementation changed and this method is no longer executed in the same
// thread
// as the filter method
Assert.notNull(request, "request must not be null, underlying implementation seems to have changed");
return choose(loadBalancer, filter);
}
protected Mono<Response<ServiceInstance>> choose(
ReactiveLoadBalancer<ServiceInstance> loadBalancer,
Predicate<ServiceInstance> filter
) {
return Mono.from(loadBalancer.choose(new DefaultRequest<>(filter)));
}
}
There were pretty big changes in the CustomExchangeFilterFunction, but the current execution doesn't even get there. It fails here, in .getIfAvailable(...):
public class CustomRoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final int DEFAULT_SEED_POSITION = 1000;
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final String serviceId;
private final int seedPosition;
private final AtomicInteger position;
private final Map<String, AtomicInteger> positionsForVersions = new HashMap<>();
public CustomRoundRobinLoadBalancer (
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId
) {
this(serviceInstanceListSupplierProvider, serviceId, new Random().nextInt(DEFAULT_SEED_POSITION));
}
public CustomRoundRobinLoadBalancer (
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId,
int seedPosition
) {
Assert.notNull(serviceInstanceListSupplierProvider, "serviceInstanceListSupplierProvider must not be null");
Assert.notNull(serviceId, "serviceId must not be null");
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.serviceId = serviceId;
this.seedPosition = seedPosition;
this.position = new AtomicInteger(seedPosition);
}
#Override
// we have no choice but to use the raw type Request here, because this method overrides another one with this signature
public Mono<Response<ServiceInstance>> choose(#SuppressWarnings("rawtypes") Request request) {
//fails here!
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get().next().map((List<ServiceInstance> instances) -> getInstanceResponse(instances, request));
}
}
Edit: after some deeper stacktracing, it seems that it does go into the CustomFilterFunction and invokes the constructor with super(loadBalancerFactory, transformer)
I found the problem or a workaround. I was using #LoadBalancerClients because I thought it would just set the same config for all clients that way (even if I technically only have one atm). I changed it to ##LoadBalancerClient and it suddenly worked. I don't quite understand why this made a difference but it did!

SpringBoot rest validation does not fail on wrong enum input

I have a SpringBoot rest POST endpoint where in body I POST an enum value. This call does not fail on wrong value input. I would like the rest call to fail instead of returning null for a value which can not be deserialised.
I have tried with the following custom ObjectMapper configuration, but any wrong input i put as enum deserialises to null.
#Bean
#Primary
public ObjectMapper customJsonObjectMapper() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
ObjectMapper objectMapper = builder.build();
objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, false);
SimpleModule module = new SimpleModule();
objectMapper.registerModule(module);
return objectMapper;
}
For example if i have the enum:
public enum CouponOddType {
BACK("back"),
LAY("lay");
private String value;
CouponOddType(String value) {
this.value = value;
}
#Override
#JsonValue
public String toString() {
return String.valueOf(value);
}
#JsonCreator
public static CouponOddType fromValue(String text) {
for (CouponOddType b : CouponOddType.values()) {
if (String.valueOf(b.value).equals(text)) {
return b;
}
}
return null;
}
}
the dto where the request is mapped to:
#ApiModel(description = "Filter used to query coupons. Filter properties are combined with AND operator")
#Validated
#javax.annotation.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2020-07-07T13:12:58.487+02:00[Europe/Ljubljana]")
public class CouponQueryFilter {
#JsonProperty("statuses")
#Valid
private List<CouponStatus> statuses = null;
#JsonProperty("oddTypes")
#Valid
private List<CouponOddType> oddTypes = null;
public CouponQueryFilter statuses(List<CouponStatus> statuses) {
this.statuses = statuses;
return this;
}
public CouponQueryFilter addStatusesItem(CouponStatus statusesItem) {
if (this.statuses == null) {
this.statuses = new ArrayList<>();
}
this.statuses.add(statusesItem);
return this;
}
/**
* Get statuses
* #return statuses
**/
#ApiModelProperty(value = "")
#Valid
public List<CouponStatus> getStatuses() {
return statuses;
}
public void setStatuses(List<CouponStatus> statuses) {
this.statuses = statuses;
}
public CouponQueryFilter oddTypes(List<CouponOddType> oddTypes) {
this.oddTypes = oddTypes;
return this;
}
public CouponQueryFilter addOddTypesItem(CouponOddType oddTypesItem) {
if (this.oddTypes == null) {
this.oddTypes = new ArrayList<>();
}
this.oddTypes.add(oddTypesItem);
return this;
}
/**
* Get oddTypes
* #return oddTypes
**/
#ApiModelProperty(value = "")
#Valid
public List<CouponOddType> getOddTypes() {
return oddTypes;
}
public void setOddTypes(List<CouponOddType> oddTypes) {
this.oddTypes = oddTypes;
}
}
and in the POST request i put the enum value in json array:
{
"statuses": [
"wrong value"
],
"oddTypes": [
"wrong value"
]
}
I would like that this type of request results in an HTTP 404 error, instead of deserialising into null.
In this case, Jackson is actually behaving as intended and there is an issue in your deserialization logic. Ultimately, you want bad enum values to throw an error and return that error to the user. This is infact the default behaviour of spring and jackso, and will result in a HTTP 400 BAD REQUEST error. IMO This is the appropriate error to return (not 404) since the user has supplied bad input.
Unless there is a specific reason for you to implement a custom #JsonCreator in your enum class, I would get rid of it. What is happening here is that Jackson is being told to use this method for converting a string into an enum value instead from the defualt method. When a text is passed that is not a valid value of your enum, you are returning null which results into that values deserializing to null.
A quick fix, would be to delete the JsonCreator and allow jackson to use its default behaviour for handling enums. The extra properties methods you have added are unnecessary in most cases
ublic enum CouponOddType {
BACK("back"),
LAY("lay");
private String value;
CouponOddType(String value) {
this.value = value;
}
}
If you need to perserve the creator for some other reason, then you will need to add business logic to determine if any of the enum values in the arrays evaluated to null.
private Response someSpringRestEndpoint(#RequestBody CouponQueryFilter filter){
if (filter.getOddTypes() != null && filter.getOddTypes().contains(null){
throw new CustomException()
}
if (filter.getStatuses() != null && filter.getStatuses().contains(null){
throw new CustomException()
}
//... other business logic
}

How to include Method Parameters in #Timed annotation used on arbitrary method name

In my application, I have a use case where I have to monitor a method by the argument value it is supplied. I have to expose the metrics to Prometheus endpoint. However, the function is a common function and is used by many different classes. I am trying to get the value passed in the method parameter to #Timed, so as to distinguish between different behaviors this function would exhibit based on the parameter value passed.
I tried using #Timed annotation but could not get the #Timed annotation expose the function parameter as a metric to Prometheus.
#Timed("getFooContent")
public void getFooContent(Arg1 arg1, Arg2 arg2) {
//some code....
}
I was able to figure this out by creating an annotation #Foo and then adding this annotation to the parameter of my function:
#Timed("getFooContent")
public void getFooContent(#Foo Arg1 arg1, Arg2 arg2) {
//some code....
}
Following is my Timed Configuration class:
#Configuration
#SuppressWarnings("unchecked")
public class TimedConfiguration {
public static final String NOT_AVAILABLE = "N/A";
Function<ProceedingJoinPoint, Iterable<Tag>> tagsBasedOnJoinPoint;
#Bean
public TimedAspect timedAspect(MeterRegistry registry) {
tagsBasedOnJoinPoint = pjp ->
Tags.of("class", pjp.getStaticPart().getSignature().getDeclaringTypeName(),
"method", pjp.getStaticPart().getSignature().getName(),
"parameter_1", getArguments(pjp));
return new TimedAspect(registry, tagsBasedOnJoinPoint);
}
private String getArguments(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
String className = pjp.getStaticPart().getSignature().getDeclaringTypeName();
if(className.contains("com.example.foo")) { //Resstricting to only certain packages starting with com.example.foo
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
Annotation[][] annotations = method.getParameterAnnotations();
int index = -1;
for(int i = 0; i < annotations.length; i++) {
Annotation[] annotationsArr = annotations[i];
for(Annotation annotation: annotationsArr) {
if(annotation.annotationType().getName().equals(Foo.class.getName())) {
index = i;
break;
}
}
}
if(index >= 0) {
List parameterValues = new ArrayList((List)args[index]);
if(CollectionUtils.isNotEmpty(parameterValues) && parameterValues.get(0) instanceof Byte) {
Collections.sort(parameterValues); //Sorting the paratemer values as per my use case
return String.valueOf(parameterValues.stream().collect(Collectors.toSet()));
}
}
}
return NOT_AVAILABLE;
}
I solved it with this TimedAspect configuration that I found in a PoC in a micrometer github issue:
https://github.com/jonatan-ivanov/micrometer-tags/blob/master/src/main/java/com/example/micrometertags/MetricsConfig.java
#Configuration
public class MetricsConfig {
#Bean
public TimedAspect timedAspect(MeterRegistry meterRegistry) {
return new TimedAspect(meterRegistry, this::tagFactory);
}
private Iterable<Tag> tagFactory(ProceedingJoinPoint pjp) {
return Tags.of(
"class", pjp.getStaticPart().getSignature().getDeclaringTypeName(),
"method", pjp.getStaticPart().getSignature().getName()
)
.and(getParameterTags(pjp))
.and(ExtraTagsPropagation.getTagsAndReset());
}
private Iterable<Tag> getParameterTags(ProceedingJoinPoint pjp) {
Set<Tag> tags = new HashSet<>();
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
for (Annotation annotation : parameters[i].getAnnotations()) {
if (annotation instanceof ExtraTag) {
ExtraTag extraTag = (ExtraTag) annotation;
tags.add(Tag.of(extraTag.value(), String.valueOf(pjp.getArgs()[i])));
}
}
}
return tags;
}
}
There isn't a way to include the parameters in the timer's tags using just the annotation. Micrometer provides the annotation for simple use cases, and recommends using the programmatic approach when you need something more complex.
You should use the record method on the timer and wrap your code in that.
registry.timer("myclass.getFooContent", Tags.of("arg1", arg1)).record(() -> {
//some code...
})
Add this below bean in your Configuration class and then try.
#Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
Annotate the configuration class with #EnableAspectJAutoProxy
Please read thru this link http://micrometer.io/docs/concepts#_the_timed_annotation

Spring 4.3.0.RELEASE does not inject Model and Request to #Controller

The code below was working fine with Spring 4.2.6.REALESE. Now, after migration something was changed and I started to get nulls in my #Controller Is it a bug?
#Controller
#RequestMapping("/client")
public class ClientController {
#RequestMapping(value = "test", method = RequestMethod.GET)
public String test(Model model) {
// model is null at this point
return "client/test";
}
}
I share your experience with the model object being null. The code below worked well prior 4.3.0-RELEASE. Maybe we are missing some new configuration?
#Controller
#RequestMapping("/admin/pricelists")
public class PriceListsController {
#RequestMapping(value = "/")
public String index(final Model model) {
// Model is null in 4.3.0-RELEASE.
...
}
}
Short answer: 4.3.0.RELEASE requires Servlet 3.0+
Longer proof:
Model is null because a method to find a proper ArgumentResolver for org.springframework.ui.Model finds the first one on the list which is org.springframework.web.method.annotation.RequestParamMethodArgumentResolver
instead of org.springframework.web.method.annotation.ModelMethodProcessor
That is the invalid part of code form HandlerMethodArgumentResolverComposite
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
if (logger.isTraceEnabled()) {
logger.trace("Testing if argument resolver [" + methodArgumentResolver + "] supports [" +
parameter.getGenericParameterType() + "]");
}
if (methodArgumentResolver.supportsParameter(parameter)) {
result = methodArgumentResolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
RequestParamMethodArgumentResolver is here first on the list and its
RequestParamMethodArgumentResolver.supportsParameter(MethodParameter parameter) returns true because of this line:
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
return true;
}
and then in MultipartResolutionDelegate:
public static boolean isMultipartArgument(MethodParameter parameter) {
Class<?> paramType = parameter.getNestedParameterType();
return (MultipartFile.class == paramType || isMultipartFileCollection(parameter) ||
isMultipartFileArray(parameter) || servletPartClass == paramType ||
isPartCollection(parameter) || isPartArray(parameter));
}
isPartCollection returns true because of this strange comparison where both sides of the equation are nulls !!
private static boolean isPartCollection(MethodParameter methodParam) {
return (servletPartClass == getCollectionParameterType(methodParam));
}
servletPartClass is null because of this
private static Class<?> servletPartClass = null;
static {
try {
servletPartClass = ClassUtils.forName("javax.servlet.http.Part",
MultipartResolutionDelegate.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
// Servlet 3.0 javax.servlet.http.Part type not available -
// Part references simply not supported then.
}
}
This is fixed in 4.3.1
Spring Framework / SPR-14358
Failure to resolve #RequestMapping method arguments in Servlet 2.5 environments

Resources