I am new to spring-cloud-gateway and I can't answer the question if I solved my problem the intended way. I hope someone can point me into the right direction, give advice or provide some exemplary sample code.
Requirement:
An incoming request at my spring-cloud-gateway service shall be forwarded to the correct backend service (there are X of such backend services, each responsible for a specific task).
The request itself does not contain enough information to decide which backend service to route to.
An additional REST-service exists that maps an arbitrary URL-to-responsible-backend-service-name. The response format is some small JSON, containing the name of the backend-service to forward to.
What would be the easiest/best/smartest/intended solution to implement this functionality with spring-cloud-gateway?
I tried implementing a GatewayFilter that first calls the mapping-service and depending on the result set GATEWAY_REQUEST_URL_ATTRfor the exchange.
This works ok. But I have additional questions.
Is it possible to omit the .uri("not://needed")) part in the route setup?
Why does the order value need to be higher than 9999? (see code example)
#SpringBootApplication
#RestController
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
return builder.routes().route(r -> r
.alwaysTrue()
.filters(f -> f.filter(new CustomRequestFilter()))
.uri("not://needed")) // how to omit ?
.build();
}
public static class CustomRequestFilter implements GatewayFilter, Ordered {
#Override
public int getOrder() {
return 10000; // why order > 9999 ? (below 10000 does not work)
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return getBackendServiceMappingResult(exchange.getRequest().getURI().toString()) //async call to REST service mapping URL->backend service name
.flatMap(mappingResult -> {
URI uri = mapServiceNameToBackendService(mappingResult.getServiceName());
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, uri);
return chain.filter(exchange);
});
}
private URI mapServiceNameToBackendService(String serviceName) {
try {
switch (serviceName) {
case "serviceA": return new URI("http://httpbin.org:80/get");
case "serviceB": return new URI("https://someotherhost:443/");
}
} catch (URISyntaxException e) {
//ignore
}
return null;
}
}
static class MappingResult {
String serviceName;
public String getServiceName() {
return serviceName;
}
}
static Mono<MappingResult> getBackendServiceMappingResult(String uri) {
WebClient client = WebClient.create("http://localhost:8080");
return client.get().uri(uriBuilder -> uriBuilder.path("/mapping").queryParam("uri", uri).build()).retrieve().bodyToMono(MappingResult.class);
}
}
Is there a better approach (with spring-cloud-gateway) to solve the requirement?
You can use this
#Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
return builder.routes().route(r ->
r.alwaysTrue()
.filters(f-> {
f.changeRequestUri(serverWebExchange -> buildRequestUri(serverWebExchange, CustomRequestFilter));
return f;
})
.uri(env.getProperty("birdeye.platform.url")))
.build();
private Optional buildRequestUri(ServerWebExchange exchange,CustomRequestFilter customRequestFilter ){
boolean updateuri = true;
if(updateuri) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().pathWithinApplication().value();
try {
uriBiulder = UriComponentsBuilder.fromUri(new URI(backendUri)).path(backendPath)
} catch (URISyntaxException e) {
e.printStackTrace();
}
}else {
uriBiulder = new URI("https://paytm.com:8080/category/create")
}
return Optional.of(uriBiulder);
}
JPA 2.2 should support #Inject in AttributeConverter, as described in the specification:
Attribute converter classes in Java EE environments support dependency injection through the Contexts
and Dependency Injection API (CDI) [ 7 ] when CDI is enabled[51]. An attribute converter class that
makes use of CDI injection may also define lifecycle callback methods annotated with the PostConstruct
and PreDestroy annotations. These methods will be invoked after injection has taken
place and before the attribute converter instance is destroyed respectively.
But when I convert my JPA 2.1 conveter sample to JPA 2.2, it does not work.
The original Converter worked both Glassfish v4 and v5:
#Converter
public class ListToStringConveter implements AttributeConverter<List<String>, String> {
//#Inject Logger log;
#Override
public String convertToDatabaseColumn(List<String> attribute) {
if (attribute == null || attribute.isEmpty()) {
return "";
}
return StringUtils.join(attribute, ",");
}
#Override
public List<String> convertToEntityAttribute(String dbData) {
if (dbData == null || dbData.trim().length() == 0) {
return new ArrayList<String>();
}
String[] data = dbData.split(",");
return Arrays.asList(data);
}
}
To taste the injection support of AttributeConverter in JPA 2.2, I extracted the conversion logic to anther CDI bean. And tried to run the codes in Glassfish v5(Java EE 8 Reference implementation).
#Converter(autoApply = false)
public class TagsConverter implements AttributeConverter<List<String>, String> {
// private static final Logger LOG = Logger.getLogger(TagsConverter.class.getName());
//
// #Override
// public String convertToDatabaseColumn(List<String> attribute) {
// if (attribute == null || attribute.isEmpty()) {
// return "";
// }
// return String.join( ",", attribute);
// }
//
// #Override
// public List<String> convertToEntityAttribute(String dbData) {
// if (dbData == null || dbData.trim().length() == 0) {
// return new ArrayList<>();
// }
//
// String[] data = dbData.split(",");
// return Arrays.asList(data);
// }
#Inject
Logger LOG;
#Inject
ConverterUtils utils;
#PostConstruct
public void postConstruct(){
LOG.log(Level.INFO, "calling #PostConstruct");
}
#PreDestroy
public void preDestroy(){
LOG.log(Level.INFO, "calling #PreDestroy");
}
#Override
public String convertToDatabaseColumn(List<String> attribute) {
LOG.log(Level.FINEST, "utils injected: {0}", utils != null);
if (attribute == null || attribute.isEmpty()) {
return "";
}
return utils.listToString(attribute);
}
#Override
public List<String> convertToEntityAttribute(String dbData) {
if (dbData == null || dbData.trim().length() == 0) {
return Collections.<String>emptyList();
}
return utils.stringToList(dbData);
}
}
And ConverterUtils class.
#ApplicationScoped
public class ConverterUtils {
public String listToString(List<String> tags) {
return join(",", tags);
}
public List stringToList(String str) {
return Arrays.asList(str.split(","));
}
}
In the TagsConverter, the expected ConverterUtils are not injected, and always get null when called it, a NPE threw.
The complete codes can be found here.
Update: I found I had created an issue on EclipseLink bugzilla 4 years ago.
We have migrated MongoDB drivers from v1.9.3 to v2.4.0. we have used BsonBaseSerializer, which does not exists in v2.4.0. what is the replacement of BsonBaseSerializer in v2.4.0?
There's not really enough of a question to give a full answer to, but the change you're looking for is documented under serialization in the mongo docs.
http://mongodb.github.io/mongo-csharp-driver/2.4/reference/bson/serialization/#implementation-1
The biggest change being that they take a type now on the base class.
So
V1 Driver code
public class IntegerCoercion : BsonBaseSerializer
{
public override object Deserialize(BsonReader bsonReader, Type nominalType, Type actualType, IBsonSerializationOptions options)
{
if (bsonReader.CurrentBsonType == BsonType.Int32)
{
return bsonReader.ReadInt32();
}
if (bsonReader.CurrentBsonType == BsonType.String)
{
var value = bsonReader.ReadString();
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return Convert.ToInt32(value);
}
bsonReader.SkipValue();
return null;
}
public override void Serialize(BsonWriter bsonWriter, Type nominalType, object value, IBsonSerializationOptions options)
{
if (value == null)
{
bsonWriter.WriteNull();
return;
}
bsonWriter.WriteInt32(Convert.ToInt32(value));
}
}
V2 Driver Code
public class IntegerCoercion : SerializerBase<object>
{
public override object Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
if (context.Reader.CurrentBsonType == BsonType.Int32)
{
return context.Reader.ReadInt32();
}
if (context.Reader.CurrentBsonType == BsonType.String)
{
var value = context.Reader.ReadString();
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return Convert.ToInt32(value);
}
context.Reader.SkipValue();
return null;
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value)
{
if (value == null)
{
context.Writer.WriteNull();
return;
}
context.Writer.WriteInt32(Convert.ToInt32(value));
}
}
Not a huge difference but as with most of the driver changes they're minimal but breaking.
I have the below code snippet for dynamic sorting using JPA Criteria API
Root<Employee> root = criteriaQuery.from(Employee);
Join<Employee, Project> joinProject =
root.join(Employee_.projectList, JoinType.LEFT);
if (sortDirection.equals("asc")) {
criteriaQuery.orderBy(cb.asc(root.get(sortField)));
If I am passing an attribute of Employee entity to order by statement, it works without any hitch, however if an attribute of Project entity is passed to order by statement, exception is thrown stating that
The attribute [projectName] is not present in the managed type
because projectName is an attribute of Projectentity which is joined with Employee using joinProject. In order by statement I am using root.get(sortField). if it is joinProject.get(sortField), it would work fine when attributes of Project are being passed to order by statement.
My questions are
How could I modify my Order By statement in order to cater all the attributes which being passed?
Do I need to conditionally check which attribute and accordingly use if conditions or are there better ways of doing this?
Appreciate insight into this.
A specific approach for a simple scenario (predetermined one-level only joins) may be something like this:
Root<Employee> root = criteriaQuery.from(Employee.class);
Join<Employee, Project> joinProject = root.join(Employee_.projectList, JoinType.LEFT);
Class<?> baseClass = fieldTypeMap.get(sortField);
From<?, ?> from;
if(baseClass == Employee.class)
{
from = root;
}
else if(baseClass == Project.class)
{
from = joinTeam;
}
else ...
Expression<?> expr = from.get(sortField);
if(sortDirection.equals("asc"))
{
criteriaQuery.orderBy(cb.asc(expr));
}
...
where fieldTypeMap is something like:
private final static Map<String, Class<?>> fieldTypeMap = new HashMap<>();
static {
fieldTypeMap.put("employeeName", Employee.class);
fieldTypeMap.put("projectName", Project.class);
...
}
However, this is quick and dirty, ugly and unmaintainable.
If you want a generic approach, things may get a bit complex.
Personally, I'm using my own classes built on top of EntityManager, CriteriaBuilder and Metamodel, which provides dynamic filtering and multi-sorting features.
But something like this should be meaningful enough:
protected static List<Order> buildOrderBy(CriteriaBuilder builder, Root<?> root, List<SortMeta> sortList)
{
List<Order> orderList = new LinkedList<>();
for(SortMeta sortMeta : sortList)
{
String sortField = sortMeta.getSortField();
SortOrder sortOrder = sortMeta.getSortOrder();
if(sortField == null || sortField.isEmpty() || sortOrder == null)
{
continue;
}
Expression<?> expr = getExpression(root, sortField);
if(sortOrder == SortOrder.ASCENDING)
{
orderList.add(builder.asc(expr));
}
else if(sortOrder == SortOrder.DESCENDING)
{
orderList.add(builder.desc(expr));
}
}
return orderList;
}
protected static Expression<?> getExpression(Root<?> root, String sortField)
{
ManagedType<?> managedType = root.getModel();
From<?, Object> from = (From<?, Object>) root;
String[] elements = sortField.split("\\.");
for(String element : elements)
{
Attribute<?, ?> attribute = managedType.getAttribute(element);
if(attribute.getPersistentAttributeType() == PersistentAttributeType.BASIC)
{
return from.get(element);
}
from = from.join(element, JoinType.LEFT);
managedType = EntityUtils.getManagedType(from.getJavaType());
}
return from;
}
This way the join is based on sort field, which is now a dotted-path-expr like "projectList.name" or "office.responsible.age"
public static <X> ManagedType<X> getManagedType(Class<X> clazz)
{
try
{
return getMetamodel().managedType(clazz);
}
catch(IllegalArgumentException e)
{
return null;
}
}
public static Metamodel getMetamodel()
{
return getEntityManagerFactory().getMetamodel();
}
public static EntityManagerFactory getEntityManagerFactory()
{
try
{
return InitialContext.doLookup("java:module/persistence/EntityManagerFactory");
}
catch(NamingException e)
{
throw new RuntimeException(e.getMessage(), e);
}
}
to make it work on a webapp, you have to declare contextual references on web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>my_app_name</display-name>
...
<persistence-context-ref>
<persistence-context-ref-name>java:module/persistence/EntityManager</persistence-context-ref-name>
<persistence-unit-name>my_pu_name</persistence-unit-name>
</persistence-context-ref>
<persistence-unit-ref>
<persistence-unit-ref-name>java:module/persistence/EntityManagerFactory</persistence-unit-ref-name>
<persistence-unit-name>my_pu_name</persistence-unit-name>
</persistence-unit-ref>
</web-app>
update
I don't know about how EclipseLink handles grouping, but also Hibernate does not perform joins (actually, conditions on joins) correctly in some case.
In example, when all these conditions are met:
querying an entity (as Root/From) that is part of a class hierarchy (and not a leaf) based on SINGLE_TABLE
joining/getting a property of a subclass
joining/getting by property name (String) instead of Attribute
To workaround the issue I always resolve property name to an Attribute and reuse joins already "walked" (did I say things may get complicated?):
public class MetaDescriptor extends BusinessObject implements Serializable, MemberEx, ColumnDescriptor
{
private static final long serialVersionUID = 1L;
#BusinessKey
protected final Attribute<?, ?> attribute;
#BusinessKey
protected final MetaDescriptor parent;
protected List<MetaDescriptor> childList;
protected final Type<?> elementType;
...
protected MetaDescriptor(Attribute<?, ?> attribute, MetaDescriptor parent)
{
this.attribute = attribute;
this.parent = parent;
if(attribute instanceof SingularAttribute)
{
SingularAttribute<?, ?> singularAttribute = (SingularAttribute<?, ?>) attribute;
elementType = singularAttribute.getType();
}
else if(attribute instanceof PluralAttribute)
{
PluralAttribute<?, ?, ?> pluralAttribute = (PluralAttribute<?, ?, ?>) attribute;
elementType = pluralAttribute.getElementType();
}
else
{
elementType = null;
}
}
public static MetaDescriptor getDescriptor(ManagedType<?> managedType, String path)
{
return getDescriptor(managedType, path, null);
}
public static MetaDescriptor getDescriptor(From<?, ?> from, String path)
{
if(from instanceof Root)
{
return getDescriptor(((Root<?>) from).getModel(), path);
}
return getDescriptor(from.getJavaType(), path);
}
public static MetaDescriptor getDescriptor(Class<?> clazz, String path)
{
ManagedType<?> managedType = EntityUtils.getManagedType(clazz);
if(managedType == null)
{
return null;
}
return getDescriptor(managedType, path);
}
private static MetaDescriptor getDescriptor(ManagedType<?> managedType, String path, MetaDescriptor parent)
{
if(path == null)
{
return null;
}
Entry<String, String> slice = StringUtilsEx.sliceBefore(path, '.');
String attributeName = slice.getKey();
Attribute<?, ?> attribute;
if("class".equals(attributeName))
{
attribute = new ClassAttribute<>(managedType);
}
else
{
try
{
attribute = managedType.getAttribute(attributeName);
}
catch(IllegalArgumentException e)
{
Class<?> managedClass = managedType.getJavaType();
// take only if it is unique
attribute = StreamEx.of(EntityUtils.getMetamodel().getManagedTypes())
.filter(x -> managedClass.isAssignableFrom(x.getJavaType()))
.flatCollection(ManagedType::getDeclaredAttributes)
.filterBy(Attribute::getName, attributeName)
.limit(2)
.collect(Collectors.reducing((a, b) -> null))
.orElse(null);
if(attribute == null)
{
return null;
}
}
}
MetaDescriptor descriptor = new MetaDescriptor(attribute, parent);
String remainingPath = slice.getValue();
if(remainingPath.isEmpty())
{
return descriptor;
}
Type<?> elementType = descriptor.getElementType();
if(elementType instanceof ManagedType)
{
return getDescriptor((ManagedType<?>) elementType, remainingPath, descriptor);
}
throw new IllegalArgumentException();
}
#Override
public <T> Expression<T> getExpression(CriteriaBuilder builder, From<?, ?> from)
{
From<?, Object> parentFrom = getParentFrom(from);
if(attribute instanceof ClassAttribute)
{
return (Expression<T>) parentFrom.type();
}
if(isSingular())
{
return parentFrom.get((SingularAttribute<Object, T>) attribute);
}
return getJoin(parentFrom, JoinType.LEFT);
}
private <X, T> From<X, T> getParentFrom(From<?, ?> from)
{
return OptionalEx.of(parent)
.map(x -> x.getJoin(from, JoinType.LEFT))
.select(From.class)
.orElse(from);
}
public <X, T> Join<X, T> getJoin(From<?, ?> from, JoinType joinType)
{
From<?, X> parentFrom = getParentFrom(from);
Join<X, T> join = (Join<X, T>) StreamEx.of(parentFrom.getJoins())
.findAny(x -> Objects.equals(x.getAttribute(), attribute))
.orElseGet(() -> buildJoin(parentFrom, joinType));
return join;
}
private <X, T> Join<X, T> buildJoin(From<?, X> from, JoinType joinType)
{
if(isSingular())
{
return from.join((SingularAttribute<X, T>) attribute, joinType);
}
if(isMap())
{
return from.join((MapAttribute<X, ?, T>) attribute, joinType);
}
if(isSet())
{
return from.join((SetAttribute<X, T>) attribute, joinType);
}
if(isList())
{
return from.join((ListAttribute<X, T>) attribute, joinType);
}
if(isCollection())
{
return from.join((CollectionAttribute<X, T>) attribute, joinType);
}
throw new ImpossibleException();
}
public Order buildOrder(CriteriaBuilderEx builder, From<?, ?> from, SortOrder direction)
{
if(direction == null)
{
return null;
}
Expression<?> expr = getExpression(builder, from);
return direction == SortOrder.ASCENDING ? builder.asc(expr) : builder.desc(expr);
}
}
with this construct, I can now safely:
public static List<Order> buildOrderList(CriteriaBuilderEx builder, From<?, ? extends Object> from, List<SortMeta> list)
{
return StreamEx.of(list)
.nonNull()
.map(x -> buildOrder(builder, from, x.getSortField(), x.getSortOrder()))
.nonNull()
.toList();
}
public static Order buildOrder(CriteriaBuilderEx builder, From<?, ? extends Object> from, String path, SortOrder direction)
{
if(path == null || path.isEmpty() || direction == null)
{
return null;
}
MetaDescriptor descriptor = MetaDescriptor.getDescriptor(from, path);
if(descriptor == null)
{
return null;
}
return descriptor.buildOrder(builder, from, direction);
}
If sortField exists only in one entity:
try{
path = root.get(sortField);
}catch (IllegalArgumentException e){
path = joinProject.get(sortField);
}
criteriaQuery.orderBy(cb.asc(path));
I am trying to inject dependencies into a Web Api Controller.
I created an own IHttpControllerActivator class and replaced the default one in lobalConfiguration.
public class SimpleASPWebAPIContainer : IHttpControllerActivator
{
private readonly CompositionContainer container;
public SimpleASPWebAPIContainer(CompositionContainer compositionContainer)
{
container = compositionContainer;
}
public IHttpController Create(System.Net.Http.HttpRequestMessage request, System.Web.Http.Controllers.HttpControllerDescriptor controllerDescriptor, Type controllerType)
{
if (controllerType != null)
{
var export = container.GetExports(controllerType, null, null).FirstOrDefault();
IHttpController result = null;
if (null != export)
{
result = export.Value as IHttpController;
}
else
{
//result = base.GetControllerInstance(requestContext, controllerType);
//container.ComposeParts(result);
}
return result;
}
else
{
return null;
}
}
public void Dispose()
{
if (container != null)
container.Dispose();
}
}
var apiSimpleContainer = new SimpleASPWebAPIContainer(container);
System.Web.Http.GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerActivator), apiSimpleContainer);
But when the client app is calling a controller method the IHttpControllerActivation Create method is not invoked.
Anybody can help me?
It was a very silly mistake.
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
ConfigureOAuth(app);
MefConfig.RegisterMef(config);
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
AutoMapperConfig.InitAutoMapper();
}
I should have to used the new HttoConfiguration instance to replace default IHttpControllerActivator instead of System.Web.Http.GlobalConfiguration.Configuration.