I have created a java service to get the details from dynamodb it is working fine but my challenge is i hardcoded the table name to the class file #DynamoDbTable annotation in order to work with different environments i feel it is not the right way. I have given my code below could someone help me to resolve the issue.
Code sample
public class DynamodbService {
private DynamoDB client;
private DynamoDBMapper objectMapper;
/**
*
* #param client
* #param objectMapper
*/
#Autowired
public DynamodbService(DynamoDB client, DynamoDBMapper objectMapper) {
this.client = client;
this.objectMapper = objectMapper;
}
public List<Dynamodb> findAll() throws Exception {
DynamoDBMapperConfig mapperConfig = new DynamoDBMapperConfig.Builder()
.withTableNameOverride(DynamoDBMapperConfig.TableNameOverride.withTableNameReplacement(""))
.build();
DynamoDBMapper mapper = new DynamoDBMapper(client, mapperConfig);
DynamoDBScanExpression scanExpression = new DynamoDBScanExpression();
List<Dynamodb> scanResult = objectMapper.scan(Dynamodb.class, scanExpression);
return scanResult;
}
}
My DynamoDB config
#Configuration
public class DynamoDBconfig {
/**
*
*/
#Value("${amazon.dynamodb.accesskey}")
private String awsAccessKey;
/**
*
*/
#Value("${amazon.dynamodb.secretkey}")
private String awsSecretKey;
#Bean
public AWSCredentials amazonAWSCredentials() {
return new BasicAWSCredentials(aes.getDecryptedvalue(awsAccessKey), aes.getDecryptedvalue(awsSecretKey));
}
/**
*
* #return
*/
#Bean
public DynamoDBMapperConfig dynamoDBMapperConfig() {
return DynamoDBMapperConfig.DEFAULT;
}
/**
*
* #param amazonDynamoDB
* #param config
* #return
*/
#Bean
public DynamoDBMapper dynamoDBMapper(AmazonDynamoDB amazonDynamoDB, DynamoDBMapperConfig config) {
return new DynamoDBMapper(amazonDynamoDB, config);
}
/**
*
* #return
*/
#Bean
public AmazonDynamoDB amazonDynamoDB() {
return AmazonDynamoDBClientBuilder.standard().withCredentials(amazonAWSCredentialsProvider())
.withRegion(Regions.US_EAST_2).build();
}
/**
*
* #return
*/
#Bean
public DynamoDB dynamoDB() {
return new DynamoDB(amazonDynamoDB());
}
/**
*
* #return
*/
public AWSCredentialsProvider amazonAWSCredentialsProvider() {
return new AWSStaticCredentialsProvider(amazonAWSCredentials());
}
}
ERROR in my dynamodb service
The constructor DynamoDBMapper(DynamoDB, DynamoDBMapperConfig) is undefined
I am unable find out what is the issue. If i use table name in class file it's working fine if i try to replace the table name using code it ends up with error.
DynamoDBMapper expects any implementation of AmazonDynamoDB but not DynamoDB class.
public DynamoDBMapper(
final AmazonDynamoDB dynamoDB,
final DynamoDBMapperConfig config) {
You need to inject only the DynamoDBMapper in your service.
#Bean
public DynamoDBMapper dynamoDBMapper(AmazonDynamoDB amazonDynamoDB) {
DynamoDBMapperConfig config = new DynamoDBMapperConfig.Builder().withTableNameOverride(TableNameOverride.withTableNameReplacement(tableName))
.build();
return new DynamoDBMapper(amazonDynamoDB, config);
}
Other option could be using com.amazonaws.services.dynamodbv2.document.DynamoDB class but there you could not use DynamoDBScanExpression and you need to rewrite your code.
String desiredTabledName = "table-name";
Table table = dynamoDB.getTable(desiredTabledName);
You have multiple options.
- table.scan() and table query.
- Index index = table.getIndex(indexName);
index.scan() or index.query()
- You could pass `QuerySpec` and `ScanSpec` to all above.
Since SpringBoot 2.2.2, the custom pagination serializer with Jackson (2.10.1) doesn't work and isn't executed when serializing.
/**
* This class allows to specify configuration related to the Web MVC part.
*/
#Configuration
public class WebConfiguration implements WebMvcConfigurer {
private static final String JSON_DATA_PROPERTY = "data";
/**
* Allows to configure a {#link JsonSerializer} for pagination.
*
* #return an instance of {#link Module}.
*/
#SuppressWarnings("rawtypes")
#Bean
public Module springDataPageModule() {
return new SimpleModule().addSerializer(Page.class, new JsonSerializer<Page>() {
#Override
public void serialize(final Page page, final JsonGenerator jsonGenerator,
final SerializerProvider serializers) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeObjectField(JSON_DATA_PROPERTY, page.getContent());
jsonGenerator.writeObjectFieldStart("paging");
jsonGenerator.writeNumberField("page", page.getNumber() + 1);
jsonGenerator.writeNumberField("totalPages", page.getTotalPages());
jsonGenerator.writeNumberField("totalElements", page.getTotalElements());
jsonGenerator.writeNumberField("perPage", page.getSize());
jsonGenerator.writeEndObject();
jsonGenerator.writeEndObject();
}
});
}
...
}
With SpringBoot 2.2.1, this custom pagination serializer is applied and works.
Can you see this problem ?
The behavior has changed since SpringBoot 2.2.2.
You have to go through the registration of the module
/**
* This class allows to specify configuration related to the Web MVC part.
*/
#Configuration
public class WebConfiguration implements WebMvcConfigurer {
private static final String JSON_DATA_PROPERTY = "data";
/**
* Allows to configure a {#link JsonSerializer} for pagination.
*
* #return an instance of {#link Module}.
*/
private Module preparePageModule() {
return new SimpleModule().addSerializer(Page.class, new JsonSerializer<>() {
#Override
public void serialize(#SuppressWarnings("rawtypes") final Page page, final JsonGenerator jsonGenerator,
final SerializerProvider serializers) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeObjectField(JSON_DATA_PROPERTY, page.getContent());
jsonGenerator.writeObjectFieldStart("paging");
jsonGenerator.writeNumberField("page", page.getNumber() + 1);
jsonGenerator.writeNumberField("totalPages", page.getTotalPages());
jsonGenerator.writeNumberField("totalElements", page.getTotalElements());
jsonGenerator.writeNumberField("perPage", page.getSize());
jsonGenerator.writeEndObject();
jsonGenerator.writeEndObject();
}
});
}
/**
* Allows to configure the Jackson object mapper.
*
* #param objectMapper
* an instance of {#link ObjectMapper}.
*/
#Autowired
public void configureJacksonObjectMapper(final ObjectMapper objectMapper) {
...
objectMapper.registerModule(preparePageModule());
}
...
}
`
I am trying to create a a custom method for use in Pre/Post Authorize calls like this:
public class CustomLSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler{
public CustomSecurityExpressionHandler(){
super();
}
#Override
protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, MethodInvocation invocation){
CustomSecurityExpressionRoot root = new CustomSecurityExpressionRoot(authentication);
root.setThis(invocation.getThis());
root.setPermissionEvaluator(getPermissionEvaluator());
return root;
}
}
and
public class CustomSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
private Object filterObject;
private Object returnObject;
private Object target;
public CustomSecurityExpressionRoot(Authentication a) {
super(a);
}
public boolean testDecision(String test){
System.out.println("Printing:"+test+"\n");
return true;
}
public void setFilterObject(Object filterObject) {
this.filterObject = filterObject;
}
public Object getFilterObject() {
return filterObject;
}
public void setReturnObject(Object returnObject) {
this.returnObject = returnObject;
}
public Object getReturnObject() {
return returnObject;
}
void setThis(Object target) {
this.target = target;
}
public Object getThis() {
return target;
}
public boolean hasPermission(Object permission) {
try {
return super.hasPermission(null, null, permission);
} catch (AccessDeniedException e) {
return false;
}
}
public boolean checkPermission(Object permission) {
return super.hasPermission(null, null, permission);
}
#Override
public boolean hasPermission(Object targetId, String targetType, Object permission) {
try {
return super.hasPermission(targetId, targetType, permission);
} catch (AccessDeniedException e) {
return false;
}
}
public boolean checkPermission(Object targetId, String targetType, Object permission) {
return super.hasPermission(targetId, targetType, permission);
}
#Override
public boolean hasPermission(Object target, Object permission) {
try {
return super.hasPermission(target, permission);
} catch (AccessDeniedException e) {
return false;
}
}
public boolean checkPermission(Object target, Object permission) {
return super.hasPermission(target, permission);
}
}
As seen above I have added the new method testDecision(String), which I can successfully use in my preAuthorize call as below:
#PreAuthorize("testDecision('TestString')")
Event getEvent(int eventId);
But when I call it in the context of a PostAuthorize as:
#PostAuthorize("testDecision('TestString')")
Event getEvent(int eventId);
I get a ClassCastException:
SEVERE: Servlet.service() for servlet [Spring MVC Dispatcher Servlet] in context with path [/myapp] threw exception [Request processing failed; nested exception is java.lang.ClassCastException: com.example.CustomSecurityExpressionRoot cannot be cast to org.springframework.security.access.expression.method.MethodSecurityExpressionRoot] with root cause
java.lang.ClassCastException: com.example.CustomSecurityExpressionRoot cannot be cast to org.springframework.security.access.expression.method.MethodSecurityExpressionRoot
at org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler.setReturnObject(DefaultMethodSecurityExpressionHandler.java:156)
at org.springframework.security.access.expression.method.ExpressionBasedPostInvocationAdvice.after(ExpressionBasedPostInvocationAdvice.java:49)
at org.springframework.security.access.prepost.PostInvocationAdviceProvider.decide(PostInvocationAdviceProvider.java:38)
at org.springframework.security.access.intercept.AfterInvocationProviderManager.decide(AfterInvocationProviderManager.java:73)
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.afterInvocation(AbstractSecurityInterceptor.java:282)
at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:68)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
at com.sun.proxy.$Proxy15.getEvent(Unknown Source)
(..truncated..)
Anyone can help me figure out what I am doing wrong?
It seems you are on an older version of Spring Security. As of Spring Security 3.1.5+ SEC-2245 is fixed & you can create your own expression root and implement MethodSecurityExpressionOperations.
The class CustomSecurityExpressionRoot must extends MethodSecurityExpressionRoot!
(implementing MethodSecurityExpressionOperations) is not enough.
Unfortunately MethodSecurityExpressionRoot is a package protected class.
Therfore you need to put CustomSecurityExpressionRoot in the same package (org.springframework.security.access.expression.method)
or you use the following class as super class for your CustomSecurityExpressionRoot (that is what I do in my projects)
ExtensibleMethodSecurityExpressionRoot:
package org.springframework.security.access.expression.method;
import org.springframework.security.core.Authentication;
/** Makes the class {#link MethodSecurityExpressionRoot} public to other packages. */
public class ExtensibleMethodSecurityExpressionRoot extends MethodSecurityExpressionRoot {
/**
* Instantiates a new extensible method security expression root.
* #param a the Authentication
*/
public ExtensibleMethodSecurityExpressionRoot(final Authentication a) {
super(a);
}
}
My complete way is this:
ExtensibleMethodSecurityExpressionHandler to change the evaluation root context:
package org.springframework.security.access.expression.method;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.DenyAllPermissionEvaluator;
import org.springframework.security.access.expression.ExpressionUtils;
import org.springframework.security.access.expression.method.defaultexpression.DefaultMethodSecuritiyExpressionRootFactory;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
/**
* This class is the same like {#link MethodSecurityExpressionHandler} but its evaluation
* root context can be exchanged.
* To use an other evaluation root context, set an other {#link #methodSecurityExpRootFactory}.
*
*/
public class ExtensibleMethodSecurityExpressionHandler implements MethodSecurityExpressionHandler {
/** The parameter name discoverer. */
private ParameterNameDiscoverer parameterNameDiscoverer;
/** The permission evaluator. */
private PermissionEvaluator permissionEvaluator;
/** The trust resolver. */
private AuthenticationTrustResolver trustResolver;
/** The expression parser. */
private ExpressionParser expressionParser;
/** The method security expression root factory. */
private MethodSecurityExpressionRootFactory<?> methodSecurityExpRootFactory;
/** The role hierarchy. */
private RoleHierarchy roleHierarchy;
/**
* Instantiates a new extensible method security expression handler.
*/
public ExtensibleMethodSecurityExpressionHandler() {
this.parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
this.permissionEvaluator = new DenyAllPermissionEvaluator();
this.trustResolver = new AuthenticationTrustResolverImpl();
this.expressionParser = new SpelExpressionParser();
this.methodSecurityExpRootFactory = new DefaultMethodSecuritiyExpressionRootFactory();
}
/**
* Uses a {#link MethodSecurityEvaluationContext} as the <tt>EvaluationContext</tt> implementation and
* configures it with a {#link MethodSecurityExpressionRoot} instance as the expression root object.
*
* #param auth the auth
* #param mi the mi
* #return the evaluation context
*/
#Override
public EvaluationContext createEvaluationContext(final Authentication auth, final MethodInvocation mi) {
MethodSecurityEvaluationContext ctx = new MethodSecurityEvaluationContext(auth,
mi,
this.parameterNameDiscoverer);
MethodSecurityExpressionRoot root = this.methodSecurityExpRootFactory.createMethodSecurityExpressionRoot(auth);
root.setTrustResolver(this.trustResolver);
root.setPermissionEvaluator(this.permissionEvaluator);
root.setRoleHierarchy(this.roleHierarchy);
ctx.setRootObject(root);
return ctx;
}
/*
* (non-Javadoc)
*
* #see
* org.springframework.security.access.expression.method.MethodSecurityExpressionHandler#filter(java.lang.Object,
* org.springframework.expression.Expression, org.springframework.expression.EvaluationContext)
*/
#Override
#SuppressWarnings({ "unchecked", "rawtypes" })
public Object filter(final Object filterTarget, final Expression filterExpression, final EvaluationContext ctx) {
MethodSecurityExpressionRoot rootObject = (MethodSecurityExpressionRoot) ctx.getRootObject().getValue();
List retainList;
if (filterTarget instanceof Collection) {
Collection collection = (Collection) filterTarget;
retainList = new ArrayList(collection.size());
for (Object filterObject : (Collection) filterTarget) {
rootObject.setFilterObject(filterObject);
if (ExpressionUtils.evaluateAsBoolean(filterExpression, ctx)) {
retainList.add(filterObject);
}
}
collection.clear();
collection.addAll(retainList);
return filterTarget;
}
if (filterTarget.getClass().isArray()) {
Object[] array = (Object[]) filterTarget;
retainList = new ArrayList(array.length);
for (Object element : array) {
rootObject.setFilterObject(element);
if (ExpressionUtils.evaluateAsBoolean(filterExpression, ctx)) {
retainList.add(element);
}
}
Object[] filtered = (Object[]) Array.newInstance(filterTarget.getClass().getComponentType(),
retainList.size());
for (int i = 0; i < retainList.size(); i++) {
filtered[i] = retainList.get(i);
}
return filtered;
}
throw new IllegalArgumentException("Filter target must be a collection or array type, but was " + filterTarget);
}
/*
* (non-Javadoc)
*
* #see org.springframework.security.access.expression.method.MethodSecurityExpressionHandler#getExpressionParser()
*/
#Override
public ExpressionParser getExpressionParser() {
return this.expressionParser;
}
/**
* Sets the parameter name discoverer.
*
* #param parameterNameDiscoverer the new parameter name discoverer
*/
public void setParameterNameDiscoverer(final ParameterNameDiscoverer parameterNameDiscoverer) {
this.parameterNameDiscoverer = parameterNameDiscoverer;
}
/**
* Sets the permission evaluator.
*
* #param permissionEvaluator the new permission evaluator
*/
public void setPermissionEvaluator(final PermissionEvaluator permissionEvaluator) {
this.permissionEvaluator = permissionEvaluator;
}
/**
* Sets the trust resolver.
*
* #param trustResolver the new trust resolver
*/
public void setTrustResolver(final AuthenticationTrustResolver trustResolver) {
this.trustResolver = trustResolver;
}
/*
* (non-Javadoc)
*
* #see
* org.springframework.security.access.expression.method.MethodSecurityExpressionHandler#setReturnObject(java.lang
* .Object, org.springframework.expression.EvaluationContext)
*/
#Override
public void setReturnObject(final Object returnObject, final EvaluationContext ctx) {
((MethodSecurityExpressionRoot) ctx.getRootObject().getValue()).setReturnObject(returnObject);
}
/**
* Sets the role hierarchy.
*
* #param roleHierarchy the new role hierarchy
*/
public void setRoleHierarchy(final RoleHierarchy roleHierarchy) {
this.roleHierarchy = roleHierarchy;
}
/**
* Gets the method security expression root factory.
*
* #return the method security expression root factory
*/
public MethodSecurityExpressionRootFactory<?> getMethodSecurityExpressionRootFactory() {
return this.methodSecurityExpRootFactory;
}
/**
* Sets the method security expression root factory.
*
* #param methodSecurityExpressionRootFactory the new method security expression root factory
*/
public void setMethodSecurityExpressionRootFactory(
final MethodSecurityExpressionRootFactory<?> methodSecurityExpressionRootFactory) {
this.methodSecurityExpRootFactory = methodSecurityExpressionRootFactory;
}
/**
* Gets the parameter name discoverer.
*
* #return the parameter name discoverer
*/
public ParameterNameDiscoverer getParameterNameDiscoverer() {
return this.parameterNameDiscoverer;
}
/**
* Gets the permission evaluator.
*
* #return the permission evaluator
*/
public PermissionEvaluator getPermissionEvaluator() {
return this.permissionEvaluator;
}
/**
* Gets the trust resolver.
*
* #return the trust resolver
*/
public AuthenticationTrustResolver getTrustResolver() {
return this.trustResolver;
}
/**
* Gets the role hierarchy.
*
* #return the role hierarchy
*/
public RoleHierarchy getRoleHierarchy() {
return this.roleHierarchy;
}
/**
* Sets the expression parser.
*
* #param expressionParser the new expression parser
*/
public void setExpressionParser(final ExpressionParser expressionParser) {
this.expressionParser = expressionParser;
}
}
MethodSecurityExpressionRootFactory:
package org.springframework.security.access.expression.method;
import org.springframework.security.core.Authentication;
/**
* Factory Class/Template Class-Pattern: Template Class interface to create different expression root objects.
*
* #param <T> the {#link ExtensibleMethodSecurityExpressionRoot} created by this factory.
*/
public interface MethodSecurityExpressionRootFactory<T extends ExtensibleMethodSecurityExpressionRoot> {
/**
* Creates a new MethodSecurityExpressionRoot object.
*
* #param authentication the authentication
* #return the extensible method security expression root
*/
T createMethodSecurityExpressionRoot(final Authentication authentication);
}
DefaultMethodSecuritiyExpressionRootFactory: only needed if one want to use the ExtensibleMethodSecurityExpression handler without own extension
package org.springframework.security.access.expression.method.defaultexpression;
import org.springframework.security.access.expression.method.ExtensibleMethodSecurityExpressionRoot;
import org.springframework.security.access.expression.method.MethodSecurityExpressionRootFactory;
import org.springframework.security.core.Authentication;
/**
* Create the default {#link ExtensibleMethodSecurityExpressionRoot} expression root.
*/
public class DefaultMethodSecuritiyExpressionRootFactory implements
MethodSecurityExpressionRootFactory<ExtensibleMethodSecurityExpressionRoot> {
#Override
public ExtensibleMethodSecurityExpressionRoot createMethodSecurityExpressionRoot(final Authentication auth) {
return new ExtensibleMethodSecurityExpressionRoot(auth);
}
}
Example Customized Method Expression Root
package com.queomedia.vwcotool.infrastructure.security.spring;
import org.springframework.security.access.expression.method.ExtensibleMethodSecurityExpressionRoot;
import org.springframework.security.core.Authentication;
public class VwCoToolMethodSecurityExpressionRoot extends ExtensibleMethodSecurityExpressionRoot {
private Authentication a;
public MyMethodSecurityExpressionRoot(final Authentication a) {
super(a);
this.a = a;
}
public isXXX(final DomainObject x){
return x.getCreator().getName().equals(a.getPrincipal());
}
}
I have a command object:
public class Job {
private String jobType;
private String location;
}
Which is bound by spring-mvc:
#RequestMapping("/foo")
public String doSomethingWithJob(Job job) {
...
}
Which works fine for http://example.com/foo?jobType=permanent&location=Stockholm. But now I need to make it work for the following url instead:
http://example.com/foo?jt=permanent&loc=Stockholm
Obviously, I don't want to change my command object, because the field names have to remain long (as they are used in the code). How can I customize that? Is there an option to do something like this:
public class Job {
#RequestParam("jt")
private String jobType;
#RequestParam("loc")
private String location;
}
This doesn't work (#RequestParam can't be applied to fields).
The thing I'm thinking about is a custom message converter similar to FormHttpMessageConverter and read a custom annotation on the target object
This solution more concise but requires using RequestMappingHandlerAdapter, which Spring use when <mvc:annotation-driven /> enabled.
Hope it will help somebody.
The idea is to extend ServletRequestDataBinder like this:
/**
* ServletRequestDataBinder which supports fields renaming using {#link ParamName}
*
* #author jkee
*/
public class ParamNameDataBinder extends ExtendedServletRequestDataBinder {
private final Map<String, String> renameMapping;
public ParamNameDataBinder(Object target, String objectName, Map<String, String> renameMapping) {
super(target, objectName);
this.renameMapping = renameMapping;
}
#Override
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
super.addBindValues(mpvs, request);
for (Map.Entry<String, String> entry : renameMapping.entrySet()) {
String from = entry.getKey();
String to = entry.getValue();
if (mpvs.contains(from)) {
mpvs.add(to, mpvs.getPropertyValue(from).getValue());
}
}
}
}
Appropriate processor:
/**
* Method processor supports {#link ParamName} parameters renaming
*
* #author jkee
*/
public class RenamingProcessor extends ServletModelAttributeMethodProcessor {
#Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
//Rename cache
private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<Class<?>, Map<String, String>>();
public RenamingProcessor(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Class<?> targetClass = target.getClass();
if (!replaceMap.containsKey(targetClass)) {
Map<String, String> mapping = analyzeClass(targetClass);
replaceMap.put(targetClass, mapping);
}
Map<String, String> mapping = replaceMap.get(targetClass);
ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), mapping);
requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder, nativeWebRequest);
super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
}
private static Map<String, String> analyzeClass(Class<?> targetClass) {
Field[] fields = targetClass.getDeclaredFields();
Map<String, String> renameMap = new HashMap<String, String>();
for (Field field : fields) {
ParamName paramNameAnnotation = field.getAnnotation(ParamName.class);
if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) {
renameMap.put(paramNameAnnotation.value(), field.getName());
}
}
if (renameMap.isEmpty()) return Collections.emptyMap();
return renameMap;
}
}
Annotation:
/**
* Overrides parameter name
* #author jkee
*/
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface ParamName {
/**
* The name of the request parameter to bind to.
*/
String value();
}
Spring config:
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean class="ru.yandex.metrika.util.params.RenamingProcessor">
<constructor-arg name="annotationNotRequired" value="true"/>
</bean>
</mvc:argument-resolvers>
</mvc:annotation-driven>
And finally, usage (like Bozho solution):
public class Job {
#ParamName("job-type")
private String jobType;
#ParamName("loc")
private String location;
}
Here's what I got working:
First, a parameter resolver:
/**
* This resolver handles command objects annotated with #SupportsAnnotationParameterResolution
* that are passed as parameters to controller methods.
*
* It parses #CommandPerameter annotations on command objects to
* populate the Binder with the appropriate values (that is, the filed names
* corresponding to the GET parameters)
*
* In order to achieve this, small pieces of code are copied from spring-mvc
* classes (indicated in-place). The alternative to the copied lines would be to
* have a decorator around the Binder, but that would be more tedious, and still
* some methods would need to be copied.
*
* #author bozho
*
*/
public class AnnotationServletModelAttributeResolver extends ServletModelAttributeMethodProcessor {
/**
* A map caching annotation definitions of command objects (#CommandParameter-to-fieldname mappings)
*/
private ConcurrentMap<Class<?>, Map<String, String>> definitionsCache = Maps.newConcurrentMap();
public AnnotationServletModelAttributeResolver(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.getParameterType().isAnnotationPresent(SupportsAnnotationParameterResolution.class)) {
return true;
}
return false;
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
bind(servletRequest, servletBinder);
}
#SuppressWarnings("unchecked")
public void bind(ServletRequest request, ServletRequestDataBinder binder) {
Map<String, ?> propertyValues = parsePropertyValues(request, binder);
MutablePropertyValues mpvs = new MutablePropertyValues(propertyValues);
MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
if (multipartRequest != null) {
bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
}
// two lines copied from ExtendedServletRequestDataBinder
String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
mpvs.addPropertyValues((Map<String, String>) request.getAttribute(attr));
binder.bind(mpvs);
}
private Map<String, ?> parsePropertyValues(ServletRequest request, ServletRequestDataBinder binder) {
// similar to WebUtils.getParametersStartingWith(..) (prefixes not supported)
Map<String, Object> params = Maps.newTreeMap();
Assert.notNull(request, "Request must not be null");
Enumeration<?> paramNames = request.getParameterNames();
Map<String, String> parameterMappings = getParameterMappings(binder);
while (paramNames != null && paramNames.hasMoreElements()) {
String paramName = (String) paramNames.nextElement();
String[] values = request.getParameterValues(paramName);
String fieldName = parameterMappings.get(paramName);
// no annotation exists, use the default - the param name=field name
if (fieldName == null) {
fieldName = paramName;
}
if (values == null || values.length == 0) {
// Do nothing, no values found at all.
} else if (values.length > 1) {
params.put(fieldName, values);
} else {
params.put(fieldName, values[0]);
}
}
return params;
}
/**
* Gets a mapping between request parameter names and field names.
* If no annotation is specified, no entry is added
* #return
*/
private Map<String, String> getParameterMappings(ServletRequestDataBinder binder) {
Class<?> targetClass = binder.getTarget().getClass();
Map<String, String> map = definitionsCache.get(targetClass);
if (map == null) {
Field[] fields = targetClass.getDeclaredFields();
map = Maps.newHashMapWithExpectedSize(fields.length);
for (Field field : fields) {
CommandParameter annotation = field.getAnnotation(CommandParameter.class);
if (annotation != null && !annotation.value().isEmpty()) {
map.put(annotation.value(), field.getName());
}
}
definitionsCache.putIfAbsent(targetClass, map);
return map;
} else {
return map;
}
}
/**
* Copied from WebDataBinder.
*
* #param multipartFiles
* #param mpvs
*/
protected void bindMultipart(Map<String, List<MultipartFile>> multipartFiles, MutablePropertyValues mpvs) {
for (Map.Entry<String, List<MultipartFile>> entry : multipartFiles.entrySet()) {
String key = entry.getKey();
List<MultipartFile> values = entry.getValue();
if (values.size() == 1) {
MultipartFile value = values.get(0);
if (!value.isEmpty()) {
mpvs.add(key, value);
}
} else {
mpvs.add(key, values);
}
}
}
}
And then registering the parameter resolver using a post-processor. It should be registered as a <bean>:
/**
* Post-processor to be used if any modifications to the handler adapter need to be made
*
* #author bozho
*
*/
public class AnnotationHandlerMappingPostProcessor implements BeanPostProcessor {
#Override
public Object postProcessAfterInitialization(Object bean, String arg1)
throws BeansException {
return bean;
}
#Override
public Object postProcessBeforeInitialization(Object bean, String arg1)
throws BeansException {
if (bean instanceof RequestMappingHandlerAdapter) {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
List<HandlerMethodArgumentResolver> resolvers = adapter.getCustomArgumentResolvers();
if (resolvers == null) {
resolvers = Lists.newArrayList();
}
resolvers.add(new AnnotationServletModelAttributeResolver(false));
adapter.setCustomArgumentResolvers(resolvers);
}
return bean;
}
}
In Spring 3.1, ServletRequestDataBinder provides a hook for additional bind values:
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
}
The ExtendedServletRequestDataBinder subclass uses it to add URI template variables as binding values. You could extend it further to make it possible to add command-specific field aliases.
You can override RequestMappingHandlerAdapter.createDataBinderFactory(..) to provide a custom WebDataBinder instance. From a controller's perspective it could look like this:
#InitBinder
public void initBinder(MyWebDataBinder binder) {
binder.addFieldAlias("jobType", "jt");
// ...
}
Thanks the answer of #jkee .
Here is my solution.
First, a custom annotation:
#Inherited
#Documented
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
public #interface ParamName {
/**
* The name of the request parameter to bind to.
*/
String value();
}
A customer DataBinder:
public class ParamNameDataBinder extends ExtendedServletRequestDataBinder {
private final Map<String, String> paramMappings;
public ParamNameDataBinder(Object target, String objectName, Map<String, String> paramMappings) {
super(target, objectName);
this.paramMappings = paramMappings;
}
#Override
protected void addBindValues(MutablePropertyValues mutablePropertyValues, ServletRequest request) {
super.addBindValues(mutablePropertyValues, request);
for (Map.Entry<String, String> entry : paramMappings.entrySet()) {
String paramName = entry.getKey();
String fieldName = entry.getValue();
if (mutablePropertyValues.contains(paramName)) {
mutablePropertyValues.add(fieldName, mutablePropertyValues.getPropertyValue(paramName).getValue());
}
}
}
}
A parameter resolver:
public class ParamNameProcessor extends ServletModelAttributeMethodProcessor {
#Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
private static final Map<Class<?>, Map<String, String>> PARAM_MAPPINGS_CACHE = new ConcurrentHashMap<>(256);
public ParamNameProcessor() {
super(false);
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestParam.class)
&& !BeanUtils.isSimpleProperty(parameter.getParameterType())
&& Arrays.stream(parameter.getParameterType().getDeclaredFields())
.anyMatch(field -> field.getAnnotation(ParamName.class) != null);
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Map<String, String> paramMappings = this.getParamMappings(target.getClass());
ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), paramMappings);
requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder, nativeWebRequest);
super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
}
/**
* Get param mappings.
* Cache param mappings in memory.
*
* #param targetClass
* #return {#link Map<String, String>}
*/
private Map<String, String> getParamMappings(Class<?> targetClass) {
if (PARAM_MAPPINGS_CACHE.containsKey(targetClass)) {
return PARAM_MAPPINGS_CACHE.get(targetClass);
}
Field[] fields = targetClass.getDeclaredFields();
Map<String, String> paramMappings = new HashMap<>(32);
for (Field field : fields) {
ParamName paramName = field.getAnnotation(ParamName.class);
if (paramName != null && !paramName.value().isEmpty()) {
paramMappings.put(paramName.value(), field.getName());
}
}
PARAM_MAPPINGS_CACHE.put(targetClass, paramMappings);
return paramMappings;
}
}
Finally, a bean configuration for adding ParamNameProcessor into the first of argument resolvers:
#Configuration
public class WebConfig {
/**
* Processor for annotation {#link ParamName}.
*
* #return ParamNameProcessor
*/
#Bean
protected ParamNameProcessor paramNameProcessor() {
return new ParamNameProcessor();
}
/**
* Custom {#link BeanPostProcessor} for adding {#link ParamNameProcessor} into the first of
* {#link RequestMappingHandlerAdapter#argumentResolvers}.
*
* #return BeanPostProcessor
*/
#Bean
public BeanPostProcessor beanPostProcessor() {
return new BeanPostProcessor() {
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof RequestMappingHandlerAdapter) {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>(adapter.getArgumentResolvers());
argumentResolvers.add(0, paramNameProcessor());
adapter.setArgumentResolvers(argumentResolvers);
}
return bean;
}
};
}
}
Param pojo:
#Data
public class Foo {
private Integer id;
#ParamName("first_name")
private String firstName;
#ParamName("last_name")
private String lastName;
#ParamName("created_at")
#DateTimeFormat(pattern = "yyyy-MM-dd")
private Date createdAt;
}
Controller method:
#GetMapping("/foos")
public ResponseEntity<List<Foo>> listFoos(#RequestParam Foo foo, #PageableDefault(sort = "id") Pageable pageable) {
List<Foo> foos = fooService.listFoos(foo, pageable);
return ResponseEntity.ok(foos);
}
That's all.
There is a simple way, you can just add one more setter method, like "setLoc,setJt".
there is no nice built in way to do it, you can only choose which workaround you apply. The difference between handling
#RequestMapping("/foo")
public String doSomethingWithJob(Job job)
and
#RequestMapping("/foo")
public String doSomethingWithJob(String stringjob)
is that job is a bean and stringjob isn't (no surprise so far). The real difference is that beans are resolved with the standard Spring bean resolver mechanism, while string params are resolved by spring MVC that knows the concept of the #RequestParam annotation. To make the long story short there is no way in the standard spring bean resolution (that is using classes like PropertyValues, PropertyValue, GenericTypeAwarePropertyDescriptor) to resolve "jt" to a property called "jobType" or at least I dont know about it.
The workarounds coud be as others suggested to add a custom PropertyEditor or a filter, but I think it just messes up the code. In my opinion the cleanest solution would be to declare a class like this :
public class JobParam extends Job {
public String getJt() {
return super.job;
}
public void setJt(String jt) {
super.job = jt;
}
}
then use that in your controller
#RequestMapping("/foo")
public String doSomethingWithJob(JobParam job) {
...
}
UPDATE :
A slightly simpler option is to not to extend, just add the extra getters, setters to the original class
public class Job {
private String jobType;
private String location;
public String getJt() {
return jobType;
}
public void setJt(String jt) {
jobType = jt;
}
}
You can use Jackson com.fasterxml.jackson.databind.ObjectMapper to convert any map to your DTO/POJO class with nested props. You need annotate your POJOs with #JsonUnwrapped on nested object. Like this:
public class MyRequest {
#JsonUnwrapped
private NestedObject nested;
public NestedObject getNested() {
return nested;
}
}
And than use it like this:
#RequestMapping(method = RequestMethod.GET, value = "/myMethod")
#ResponseBody
public Object myMethod(#RequestParam Map<String, Object> allRequestParams) {
MyRequest request = new ObjectMapper().convertValue(allRequestParams, MyRequest.class);
...
}
That's all. A little coding. Also, you can give any names to your props usign #JsonProperty.
I would like to point you to another direction. But I do not know if it works.
I would try to manipulate the binding itself.
It is done by WebDataBinder and will be invoked from HandlerMethodInvoker method Object[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception
I have no deep look in Spring 3.1, but what I have seen, is that this part of Spring has been changed a lot. So it is may possible to exchange the WebDataBinder. In Spring 3.0 it seams not possible without overriding the HandlerMethodInvoker.
Try intercepting request using InterceptorAdaptor, and then using simple checking mechanism decide whether to foward the request to the controller handler. Also wrap HttpServletRequestWrapper around the request, to enable you override the requests getParameter().
This way you can repass the actual parameter name and its value back to the request to be seen by the controller.
Example option:
public class JobInterceptor extends HandlerInterceptorAdapter {
private static final String requestLocations[]={"rt", "jobType"};
private boolean isEmpty(String arg)
{
return (arg !=null && arg.length() > 0);
}
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
//Maybe something like this
if(!isEmpty(request.getParameter(requestLocations[0]))|| !isEmpty(request.getParameter(requestLocations[1]))
{
final String value =
!isEmpty(request.getParameter(requestLocations[0])) ? request.getParameter(requestLocations[0]) : !isEmpty(request
.getParameter(requestLocations[1])) ? request.getParameter(requestLocations[1]) : null;
HttpServletRequest wrapper = new HttpServletRequestWrapper(request)
{
public String getParameter(String name)
{
super.getParameterMap().put("JobType", value);
return super.getParameter(name);
}
};
//Accepted request - Handler should carry on.
return super.preHandle(request, response, handler);
}
//Ignore request if above condition was false
return false;
}
}
Finally wrap the HandlerInterceptorAdaptor around your controller handler as shown below. The SelectedAnnotationHandlerMapping allows you to specify which handler will be interecepted.
<bean id="jobInterceptor" class="mypackage.JobInterceptor"/>
<bean id="publicMapper" class="org.springplugins.web.SelectedAnnotationHandlerMapping">
<property name="urls">
<list>
<value>/foo</value>
</list>
</property>
<property name="interceptors">
<list>
<ref bean="jobInterceptor"/>
</list>
</property>
</bean>
EDITED.
There's a little improvement to jkee's answer.
In order to support inheritance you should also analyze parent classes.
/**
* ServletRequestDataBinder which supports fields renaming using {#link ParamName}
*
* #author jkee
* #author Yauhen Parmon
*/
public class ParamRenamingProcessor extends ServletModelAttributeMethodProcessor {
#Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
//Rename cache
private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<>();
public ParamRenamingProcessor(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Class<?> targetClass = Objects.requireNonNull(target).getClass();
if (!replaceMap.containsKey(targetClass)) {
replaceMap.put(targetClass, analyzeClass(targetClass));
}
Map<String, String> mapping = replaceMap.get(targetClass);
ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), mapping);
Objects.requireNonNull(requestMappingHandlerAdapter.getWebBindingInitializer())
.initBinder(paramNameDataBinder);
super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
}
private Map<String, String> analyzeClass(Class<?> targetClass) {
Map<String, String> renameMap = new HashMap<>();
for (Field field : targetClass.getDeclaredFields()) {
ParamName paramNameAnnotation = field.getAnnotation(ParamName.class);
if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) {
renameMap.put(paramNameAnnotation.value(), field.getName());
}
}
if (targetClass.getSuperclass() != Object.class) {
renameMap.putAll(analyzeClass(targetClass.getSuperclass()));
}
return renameMap;
}
}
This processor will analyze fields of superclasses annotated with #ParamName. It also doesn't use initBinder method with 2 parameters which is deprecated as of Spring 5.0. All the rest in jkee's answer is OK.