How to make a bean configuration trigger only when two profiles are active - spring

I have a bean that only should get created iff the two profiles "demo" and "local" both are active. What is the best way to achieve this in a java based Spring Configuration.
What I came up with so far is, creating beans like the following:
#Profile("demo")
#Bean("isDemoActive")
public Boolean isDemoActive(){ return true;}
And get those injected in the bean creating method and do a if condition on those beans.
Is there a nicer/easier way to do this kind of stuff?

Here's my suggestion, as per my comment above:
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class DoubleProfilesCondition implements Condition {
public boolean matches(ConditionContext context,AnnotatedTypeMetadata metadata) {
String[] activeProfiles = context.getEnvironment().getActiveProfiles();
int counter = 0;
for (int i = 0; i < activeProfiles.length; i++) {
String profile = activeProfiles[i];
if (profile.equals("profile1") || profile.equals("profile2")) {
counter++;
}
}
if (counter == 2)
return true;
return false;
}
}
And the class that dictates which beans are created:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
#Configuration
#Conditional(DoubleProfilesCondition.class)
public class MyConfig {
public #Bean
ExampleService service() {
ExampleService service = new ExampleService();
service.setMessage("hello, success!");
return service;
}
}

I coded a #ProfileAll annotation which is very like the standard #Profile annotation, but annotated Configurations, Methods, ... are only processed by spring if ALL of the given profiles are currently active:
ProfileAll.java (Annotation)
import org.springframework.context.annotation.Conditional;
#Retention(RetentionPolicy.RUNTIME)
#Target({ ElementType.TYPE, ElementType.METHOD })
#Documented
#Conditional(ProfileAllCondition.class)
public #interface ProfileAll {
/** The set of profiles for which the annotated component should be registered. */
String[] value();
}
ProfileAllCondition.java (Condition)
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.MultiValueMap;
class ProfileAllCondition implements Condition {
#Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
if (context.getEnvironment() != null) {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(ProfileAll.class.getName());
if (attrs != null) {
LinkedList<String> profilesActive = new LinkedList<>();
LinkedList<String> profilesNotActive = new LinkedList<>();
List<Object> values = attrs.get("value");
int count = 0;
for (Object value : values) {
for (String profile : ((String[]) value)) {
count++;
if (context.getEnvironment().acceptsProfiles(profile)) {
profilesActive.add(profile);
} else {
profilesNotActive.add(profile);
}
}
}
if (profilesActive.size() == count) {
return true;
} else {
return false;
}
}
}
return true;
}
}
use it as follows, e.g.:
#Profile({ "mysql", "cloud", "special" })
#Configuration
public class MySqlConfig {
...
MySqlConfig will then only be processed if all three profiles are currently active on startup.

Related

A custom Codec or PojoCodec may need to be explicitly configured and registered to handle this type

UserProfileModel has two embedded models. I am giving here concisely:
#Document(collection = "USER_PROFILE")
public class UserProfileModel extends BaseModel<UserModel> {
public static final String FIELD_USER_ID = "userId";
public static final String FIELD_BASIC_INFO = "basicInfo";
public static final String FIELD_CONTACT_INFO = "contactInfo";
private String userId;
private BasicInfo basicInfo;
private ContactInfo contactInfo;
}
public class BasicInfo {
private String gender;
private LocalDate birthDate;
private String[] langs;
private String religiousView;
private String politicalView;
}
public class ContactInfo {
private String[] mobilePhones;
private Address address;
private SocialLink[] socialLinks;
private String[] websites;
private String[] emails;
}
Writing converter class of UserProfileModel:
#Component
#WritingConverter
public class UserProfileModelConverter implements Converter<UserProfileModel, Document> {
#Override
public Document convert(UserProfileModel s) {
Document doc = new Document();
if (null != s.getUserId())
doc.put(UserProfileModel.FIELD_USER_ID, s.getUserId());
if (null != s.getBasicInfo())
doc.put(UserProfileModel.FIELD_BASIC_INFO, s.getBasicInfo());
if (null != s.getContactInfo())
doc.put(UserProfileModel.FIELD_CONTACT_INFO, s.getContactInfo());
}
}
Trying to save an object of UserProfileModel like this:
#Autowired
private UserProfileRepository repo;
UserProfileModel profileModel = generateUserProfileModel();
repo.save(profileModel);
Exception:
org.bson.codecs.configuration.CodecConfigurationException: An exception occurred when encoding using the AutomaticPojoCodec.
Encoding a BasicInfo: 'BasicInfo(gender=Male, birthDate=2020-05-05, langs=[Lang 1, Lang 2], religiousView=Islam (Sunni), politicalView=N/A)' failed with the following exception:
Failed to encode 'BasicInfo'. Encoding 'langs' errored with: Can't find a codec for class [Ljava.lang.String;.
A custom Codec or PojoCodec may need to be explicitly configured and registered to handle this type.
at org.bson.codecs.pojo.AutomaticPojoCodec.encode(AutomaticPojoCodec.java:53)
at org.bson.codecs.EncoderContext.encodeWithChildContext(EncoderContext.java:91)
at org.bson.codecs.DocumentCodec.writeValue(DocumentCodec.java:185)
at org.bson.codecs.DocumentCodec.writeMap(DocumentCodec.java:199)
at org.bson.codecs.DocumentCodec.encode(DocumentCodec.java:141)
at org.bson.codecs.DocumentCodec.encode(DocumentCodec.java:45)
at org.bson.codecs.BsonDocumentWrapperCodec.encode(BsonDocumentWrapperCodec.java:63)
at org.bson.codecs.BsonDocumentWrapperCodec.encode(BsonDocumentWrapperCodec.java:29)
If I don't use converter for UserProfileModel by adding in Mongo Config, then this exception doesn't appear and everything works well.
But I am trying to use the Converter class for some reason.
So is it something wrong or modification needed in converter class?
A Jira ticket already raised on this issue here.
Can't find a codec for class [Ljava.lang.String;
And their reply
The Document class currently supports only List, not native Java
array, so just replace with:
List codes = Arrays.asList("1112233", "2223344");
So, String array is not supported. Use List<String> instead of String[] in models.
Two ways to solve this issue:
Either change String[] to List<String> as mentioned here
Write your custom codec (if you do not have control over a class)
Following is solution 2:
Create a custom Code StringArrayCodec
Register it along with the existing codec
Start/restart application which uses mongo-java driver
Following is Kotlin code but can be converted to java as well.
StringArrayCodec.kt
import java.util.*
import org.bson.BsonReader
import org.bson.BsonType
import org.bson.BsonWriter
import org.bson.codecs.Codec
import org.bson.codecs.DecoderContext
import org.bson.codecs.EncoderContext
class StringArrayCodec : Codec<Array<String>> {
/**
* Encode an instance of the type parameter `T` into a BSON value.
* #param writer the BSON writer to encode into
* #param value the value to encode
* #param encoderContext the encoder context
*/
override fun encode(writer: BsonWriter?, value: Array<String>?, encoderContext: EncoderContext?) {
writer?.writeStartArray()
val isNonNull = value != null
if (isNonNull) {
writer?.writeBoolean(isNonNull)
value?.size?.let { writer?.writeInt32(it) }
for (i in value!!) {
writeValue(writer, i, encoderContext)
}
} else {
writer?.writeBoolean(!isNonNull)
}
writer?.writeEndArray()
}
private fun writeValue(writer: BsonWriter?, s: String, encoderContext: EncoderContext?) {
if (s == null) {
writer?.writeNull()
} else {
writer?.writeString(s)
}
}
/**
* Returns the Class instance that this encodes. This is necessary because Java does not reify generic types.
*
* #return the Class instance that this encodes.
*/
override fun getEncoderClass(): Class<Array<String>>? {
return Array<String>::class.java
}
/**
* Decodes a BSON value from the given reader into an instance of the type parameter `T`.
*
* #param reader the BSON reader
* #param decoderContext the decoder context
* #return an instance of the type parameter `T`.
*/
override fun decode(reader: BsonReader?, decoderContext: DecoderContext?): Array<String>? {
reader?.readStartArray()
val isNonNull = reader?.readBoolean()
val tempArray: Array<String?>?
if (isNonNull == true) {
val size = reader.readInt32()
tempArray = arrayOfNulls(size)
for (i in 0 until size) {
tempArray[i] = readValue(reader, decoderContext)
}
} else {
tempArray = null
}
val array: Array<String>? = if (isNonNull == true) {
Arrays.stream(tempArray)
.filter { s ->
s != null
}
.toArray() as Array<String>
} else {
null
}
reader?.readEndArray()
return array
}
private fun readValue(reader: BsonReader, decoderContext: DecoderContext?): String? {
val bsonType: BsonType = reader.currentBsonType
return if (bsonType == BsonType.NULL) {
reader.readNull()
null
} else {
reader.readString()
}
}
}
Register custom codec as CodecRegistries.fromCodecs(StringArrayCodec())
MongodbConfig.kt
import com.mongodb.ConnectionString
import com.mongodb.MongoClientSettings
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import com.mongodb.client.MongoCollection
import com.mongodb.client.MongoDatabase
import org.bson.Document
import org.bson.codecs.configuration.CodecRegistries
import org.bson.codecs.configuration.CodecRegistries.fromProviders
import org.bson.codecs.configuration.CodecRegistries.fromRegistries
import org.bson.codecs.configuration.CodecRegistry
import org.bson.codecs.pojo.PojoCodecProvider
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.stereotype.Component
#EnableConfigurationProperties
#ConfigurationProperties(prefix = "mongodb")
#ConditionalOnProperty(name = ["mongodb.enable"], havingValue = "true", matchIfMissing = false)
#Component
class MongodbConfig
(
#Value("\${mongodb.uri}")
val connectionUri: String,
#Value("\${mongodb.database}")
val database: String,
#Value("\${mongodb.collection-name}")
val collectionName: String
) {
#Bean
fun mongoClient(): MongoClient {
return MongoClients.create(mongodbSettings())
}
#Bean
fun mongoDatabase(mongoClient: MongoClient): MongoDatabase {
return mongoClient.getDatabase(database)
}
#Bean
fun mongodbCollection(mongoDatabase: MongoDatabase): MongoCollection<Document> {
return mongoDatabase.getCollection(collectionName)
}
fun mongodbSettings(): MongoClientSettings {
val pojoCodecRegistry: CodecRegistry = fromRegistries(
CodecRegistries.fromCodecs(StringArrayCodec()), // <---- this is the custom codec
MongoClientSettings.getDefaultCodecRegistry(),
fromProviders(PojoCodecProvider.builder().automatic(true).build())
)
val connectionString = ConnectionString(connectionUri)
return MongoClientSettings.builder()
.codecRegistry(pojoCodecRegistry)
.applyConnectionString(connectionString)
.build()
}
}
Dependency and driver version
implementation("org.mongodb:mongodb-driver-sync:4.2.0") {
because("To connect mongodb instance")
}
The answer from dkb re StringArrayCodec was helpful to me.
If you wish to avoid kotlin and use java instead then here is a non-kotlin-spam-java version:
import org.bson.BsonReader;
import org.bson.BsonWriter;
import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
public final class MongoCodecStringArray implements Codec {
public void encode(BsonWriter writer, String[] value, EncoderContext encoderContext) {
if (writer == null)
return;
writer.writeStartArray();
boolean isNonNull = value != null;
writer.writeBoolean(isNonNull);
if (isNonNull) {
writer.writeInt32(value.length);
for (int i = 0; i < value.length; ++i) {
writer.writeString(value[i]);
}
}
writer.writeEndArray();
}
public void encode(BsonWriter var1, Object var2, EncoderContext var3) {
this.encode(var1, (String[]) var2, var3);
}
public Class getEncoderClass() {
return String[].class;
}
public String[] decodeImpl(BsonReader reader, DecoderContext decoderContext) {
if (reader == null)
return null;
reader.readStartArray();
Boolean isNonNull = reader.readBoolean();
String[] ret = null;
if (isNonNull) {
int size = reader.readInt32();
ret = new String[size];
for (int i = 0; i < size; ++i) {
ret[i] = reader.readString();
}
}
reader.readEndArray();
return ret;
}
public Object decode(BsonReader var1, DecoderContext var2) {
return decodeImpl(var1, var2);
}
}
.

Create a custom RestControllerAnotation to execute a requestMapping

Good afternoon,
I have a restController and I want to create an annotation that allows or not to execute a method based on an a custom header value.
If custom header tag equals something then the method must execute, if the custom header dont match, the method musth not execute
I have followed several articles but I have not been able.
I attached the code I created:
Annotation Code:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface ApiVersion {
int[] value();
}
ApiVersionRequestMappingHandlerMapping
public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private final String prefix;
public ApiVersionRequestMappingHandlerMapping(String prefix) {
this.prefix = prefix;
}
#Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
if(info == null) return null;
ApiVersion methodAnnotation = AnnotationUtils.findAnnotation(method, ApiVersion.class);
if(methodAnnotation != null) {
RequestCondition<?> methodCondition = getCustomMethodCondition(method);
// Concatenate our ApiVersion with the usual request mapping
info = createApiVersionInfo(methodAnnotation, methodCondition).combine(info);
} else {
ApiVersion typeAnnotation = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
if(typeAnnotation != null) {
RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
// Concatenate our ApiVersion with the usual request mapping
info = createApiVersionInfo(typeAnnotation, typeCondition).combine(info);
}
}
return info;
}
private RequestMappingInfo createApiVersionInfo(ApiVersion annotation, RequestCondition<?> customCondition) {
int[] values = annotation.value();
String[] patterns = new String[values.length];
for(int i=0; i<values.length; i++) {
// Build the URL prefix
patterns[i] = prefix+values[i];
}
return new RequestMappingInfo(
new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(), useSuffixPatternMatch(), useTrailingSlashMatch(), getFileExtensions()),
new RequestMethodsRequestCondition(),
new ParamsRequestCondition(),
new HeadersRequestCondition(),
new ConsumesRequestCondition(),
new ProducesRequestCondition(),
customCondition);
}
}
Rest Controller
#RestController
#RequiredArgsConstructor
#RequestMapping("/api/example")
public class ExampleController {
private final UserService userService;
#ApiVersion (1)
#GetMapping("/myMethod")
public String myMethod(#AuthenticationPrincipal UserAuthenticatedDetails userAuthenticated) {
return userAuthenticated.getUsername();
}
}
ApiConfig
package xx.package.sample;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
#ComponentScan("xx.package")
#Configuration
#EnableTransactionManagement
#EntityScan("xx.package.domain.entity")
#EnableJpaRepositories("xx.package.domain.repository")
#EnableAutoConfiguration
public class ApiConfig {
}
I know I'm missing something but I can't see what.
Regards, and thank you very much!
You could use #GetMapping(path = "/myMethod", headers = "My-Header=myValue").
a sequence of "My-Header=myValue" style expressions, with a request
only mapped if each such header is found to have the given value
see https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/GetMapping.html#headers--

Download the necessary .properties file depending on the OS

There are two configuration files for different OS windows.properties and unix.properties.
There is a configuration:
#Configuration
#ConfigurationProperties (prefix = "storage")
public class StorageProperties {
     private String root;
     private String sitesDirName;
     private String avatarsDirName;
     private String screenshotsDirName;
     #PostConstruct
     public void postConstruct () {
     }
}
How to make so that a certain file would be loaded depending on the OS? I ran across #Conditional, but this is one condition. Maybe he will help somehow.
(1) Define an enum for OS .Use system property os.name to determine current OS :
public enum OS {
WINDOWS, UNIX, MAC, UNKNOWN;
public static OS currentOS() {
String OS = System.getProperty("os.name").toLowerCase();
if (OS.indexOf("win") >= 0) {
return WINDOWS;
} else if (OS.indexOf("nix") >= 0 || OS.indexOf("nux") >= 0 || OS.indexOf("aix") > 0) {
return UNIX;
} else if ((OS.indexOf("mac") >= 0)) {
return MAC;
} else {
return UNKNOWN;
}
}
}
(2) Implement ConditionalOnOS :
#Target({ElementType.TYPE, ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Conditional(OsCondition.class)
public #interface ConditionalOnOS {
OS os();
}
public class OsCondition implements Condition {
#Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(ConditionalOnOS.class.getName());
if (attrs != null) {
Object os = attrs.getFirst("os");
if (os != null && os instanceof OS) {
if (OS.currentOS().equals(((OS) os))) {
return true;
}
}
}
return false;
}
}
(3) Configure #ConfigurationProperties for different OS. Use #PropertySource to define the properties file paths for different OS:
#ConfigurationProperties(prefix = "storage")
public static class StorageProperties {
private String root;
private String sitesDirName;
private String avatarsDirName;
private String screenshotsDirName;
#Configuration
#PropertySource("classpath:windows.properties")
#ConditionalOnOS(os = OS.WINDOWS)
public static class WindowsStrogeProperties extends StorageProperties {
}
#Configuration
#PropertySource("classpath:unix.properties")
#ConditionalOnOS(os = OS.UNIX)
public static class UnixStrogeProperties extends StorageProperties {
}
}
(4) Inject StorageProperties to the client
#Conditional would be useful to determine the operating system, whereby you would have to define the conditional classes.
As a shorter approach, you can use good old if statements to determine the operating system. Assuming you have two different files, windows.properties and unix.properties as suggested, create the configuration class to determine the operating system and load the appropriate .properties file.
The code for the configuration class is as shown below.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.io.ClassPathResource;
import com.sun.javafx.PlatformUtil;
#Configuration
public class OSConfiguration {
#Bean
public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
String osName = "";
if (PlatformUtil.isWindows()) {
osName = "windows";
} else if (PlatformUtil.isUnix()) {
osName = "unix";
} else if (PlatformUtil.isMac()) {
osName = "mac";
}
String propertiesFilename = osName + ".properties";
PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
configurer.setLocation(new ClassPathResource(propertiesFilename));
return configurer;
}
}

serializing annotations as well as fields to JSON

I have a spring boot app, and I want to send DTO validation constraints as well as field value to the client.
Having DTO
class PetDTO {
#Length(min=5, max=15)
String name;
}
where name happens to be 'Leviathan', should result in this JSON being sent to client:
{
name: 'Leviathan'
name_constraint: { type: 'length', min:5, max: 15},
}
Reasoning is to have single source of truth for validations. Can this be done with reasonable amount of work?
To extend Frederik's answer I'll show a little sample code that convers an object to map and serializes it.
So here is the User pojo:
import org.hibernate.validator.constraints.Length;
public class User {
private String name;
public User(String name) {
this.name = name;
}
#Length(min = 5, max = 15)
public String getName() {
return name;
}
}
Then the actual serializer:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.util.ReflectionUtils;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toMap;
public class UserSerializer extends StdSerializer<User> {
public UserSerializer(){
this(User.class);
}
private UserSerializer(Class t) {
super(t);
}
#Override
public void serialize(User bean, JsonGenerator gen, SerializerProvider provider) throws IOException {
Map<String, Object> properties = beanProperties(bean);
gen.writeStartObject();
for (Map.Entry<String, Object> entry : properties.entrySet()) {
gen.writeObjectField(entry.getKey(), entry.getValue());
}
gen.writeEndObject();
}
private static Map<String, Object> beanProperties(Object bean) {
try {
return Arrays.stream(Introspector.getBeanInfo(bean.getClass(), Object.class).getPropertyDescriptors())
.filter(descriptor -> Objects.nonNull(descriptor.getReadMethod()))
.flatMap(descriptor -> {
String name = descriptor.getName();
Method getter = descriptor.getReadMethod();
Object value = ReflectionUtils.invokeMethod(getter, bean);
Property originalProperty = new Property(name, value);
Stream<Property> constraintProperties = Stream.of(getter.getAnnotations())
.map(anno -> new Property(name + "_constraint", annotationProperties(anno)));
return Stream.concat(Stream.of(originalProperty), constraintProperties);
})
.collect(toMap(Property::getName, Property::getValue));
} catch (Exception e) {
return Collections.emptyMap();
}
}
// Methods from Annotation.class
private static List<String> EXCLUDED_ANNO_NAMES = Arrays.asList("toString", "equals", "hashCode", "annotationType");
private static Map<String, Object> annotationProperties(Annotation anno) {
try {
Stream<Property> annoProps = Arrays.stream(Introspector.getBeanInfo(anno.getClass(), Proxy.class).getMethodDescriptors())
.filter(descriptor -> !EXCLUDED_ANNO_NAMES.contains(descriptor.getName()))
.map(descriptor -> {
String name = descriptor.getName();
Method method = descriptor.getMethod();
Object value = ReflectionUtils.invokeMethod(method, anno);
return new Property(name, value);
});
Stream<Property> type = Stream.of(new Property("type", anno.annotationType().getName()));
return Stream.concat(type, annoProps).collect(toMap(Property::getName, Property::getValue));
} catch (IntrospectionException e) {
return Collections.emptyMap();
}
}
private static class Property {
private String name;
private Object value;
public Property(String name, Object value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public Object getValue() {
return value;
}
}
}
And finally we need to register this serializer to be used by Jackson:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#SpringBootApplication(scanBasePackages = "sample.spring.serialization")
public class SerializationApp {
#Bean
public Jackson2ObjectMapperBuilder mapperBuilder(){
Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder = new Jackson2ObjectMapperBuilder();
jackson2ObjectMapperBuilder.serializers(new UserSerializer());
return jackson2ObjectMapperBuilder;
}
public static void main(String[] args) {
SpringApplication.run(SerializationApp.class, args);
}
}
#RestController
class SerializationController {
#GetMapping("/user")
public User user() {
return new User("sample");
}
}
The Json that will be emitted:
{
"name_constraint":{
"min":5,
"max":15,
"payload":[],
"groups":[],
"message":"{org.hibernate.validator.constraints.Length.message}",
"type":"org.hibernate.validator.constraints.Length"
},
"name":"sample"
}
Hope this helps. Good luck.
You can always use a custom Jackson Serializer for this. Plenty of docs to do this can be found on the internet, might look something like this:
public void serialize(PetDTO value, JsonGenerator jgen, ...) {
jgen.writeStartObject();
jgen.writeNumberField("name", value.name);
jgen.writeObjectField("name_consteaint", getConstraintValue(value));
}
public ConstaintDTO getConstraintValue(PetDTO value) {
// Use reflection to check if the name field on the PetDTO is annotated
// and extract the min, max and type values from the annotation
return new ConstaintDTO().withMaxValue(...).withMinValue(...).ofType(...);
}
You may want to create a base-DTO class for which the converter kicks in so you don't have to create a custom converter for all your domain objects that need to expose the constraints.
By combining reflection and smart use of writing fields, you can get close. Downside is you can't take advantage of the #JsonXXX annotations on your domain objects, since you're writing the JSON yourself.
More ideal solution whould be to have Jackson convert, but have some kind of post-conversion-call to add additional XX_condtion properties to the object. Maybe start by overriding the default object-serializer (if possible)?

How to custom #FeignClient Expander to convert param?

Feign default expander to convert param:
final class ToStringExpander implements Expander {
#Override
public String expand(Object value) {
return value.toString();
}
}
I want custom it to convert user to support GET param, like this
#FeignClient("xx")
interface UserService{
#RequestMapping(value="/users",method=GET)
public List<User> findBy(#ModelAttribute User user);
}
userService.findBy(user);
What can i do?
First,you must write a expander like ToJsonExpander:
public class ToJsonExpander implements Param.Expander {
private static ObjectMapper objectMapper = new ObjectMapper();
public String expand(Object value) {
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
throw new ExpanderException(e);
}
}
}
Second, write a AnnotatedParameterProcessor like JsonArgumentParameterProcessor to add expander for your processor.
public class JsonArgumentParameterProcessor implements AnnotatedParameterProcessor {
private static final Class<JsonArgument> ANNOTATION = JsonArgument.class;
public Class<? extends Annotation> getAnnotationType() {
return ANNOTATION;
}
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation) {
MethodMetadata data = context.getMethodMetadata();
String name = ANNOTATION.cast(annotation).value();
String method = data.template().method();
Util.checkState(Util.emptyToNull(name) != null,
"JsonArgument.value() was empty on parameter %s", context.getParameterIndex());
context.setParameterName(name);
if (method != null && (HttpMethod.POST.matches(method) || HttpMethod.PUT.matches(method) || HttpMethod.DELETE.matches(method))) {
data.formParams().add(name);
} else {
`data.indexToExpanderClass().put(context.getParameterIndex(), ToJsonExpander.class);`
Collection<String> query = context.setTemplateParameter(name, data.template().queries().get(name));
data.template().query(name, query);
}
return true;
}
}
Third,add it to Feign configuration.
#Bean
public Contract feignContract(){
List<AnnotatedParameterProcessor> processors = new ArrayList<>();
processors.add(new JsonArgumentParameterProcessor());
processors.add(new PathVariableParameterProcessor());
processors.add(new RequestHeaderParameterProcessor());
processors.add(new RequestParamParameterProcessor());
return new SpringMvcContract(processors);
}
Now, you can use #JsonArgument to send model argument like:
public void saveV10(#JsonArgument("session") Session session);
I don't know what #ModelAttribute does but I was looking for a way to convert #RequestParam values so I did this:
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import org.springframework.cloud.netflix.feign.FeignFormatterRegistrar;
import org.springframework.format.FormatterRegistry;
import org.springframework.stereotype.Component;
import static com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat.E164;
#Component
public class PhoneNumberFeignFormatterRegistrar implements FeignFormatterRegistrar {
private final PhoneNumberUtil phoneNumberUtil;
public PhoneNumberFeignFormatterRegistrar(PhoneNumberUtil phoneNumberUtil) {
this.phoneNumberUtil = phoneNumberUtil;
}
#Override
public void registerFormatters(FormatterRegistry registry) {
registry.addConverter(Phonenumber.PhoneNumber.class, String.class, source -> phoneNumberUtil.format(source, E164));
}
}
Now stuff like the following works
import com.google.i18n.phonenumbers.Phonenumber;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.hateoas.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
#FeignClient("data-service")
public interface DataClient {
#RequestMapping(method = RequestMethod.GET, value = "/phoneNumbers/search/findByPhoneNumber")
Resource<PhoneNumberRecord> getPhoneNumber(#RequestParam("phoneNumber") Phonenumber.PhoneNumber phoneNumber);
}
As the open feign issue and spring doc say:
The OpenFeign #QueryMap annotation provides support for POJOs to be used as GET parameter maps.
Spring Cloud OpenFeign provides an equivalent #SpringQueryMap annotation, which is used to annotate a POJO or Map parameter as a query parameter map since 2.1.0.
You can use it like this:
#GetMapping("user")
String getUser(#SpringQueryMap User user);
public class User {
private String name;
private int age;
...
}

Resources