I'm trying to using generics with the reactive webclient, so far I've create a class that holds the webClient and some methods
public class CustomWebClient<T> {
private final WebClient webClient;
public CustomWebClient(WebClient.Builder builder, ClientProperties properties) {
String rootUrl = properties.getHttp().getRootUrl();
webClient = builder
.baseUrl(rootUrl)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeaders(httpHeaders -> httpHeaders.setAll(properties.getHeaders()))
.build();
}
public Mono<ResponseEntity<T>> setRequest(final String uri,
final HttpMethod httpMethod,
final Class<T> classToMap) {
WebClient.RequestHeadersSpec<?> requestHeadersSpec = webClient.method(httpMethod).uri(uri);
WebClient.ResponseSpec responseSpec = setRetrievingOptions(requestHeadersSpec);
return setEntity(responseSpec, classToMap);
}
private WebClient.ResponseSpec setRetrievingOptions(final WebClient.RequestHeadersSpec<?> requestHeadersSpec) {
return requestHeadersSpec.retrieve()
.onStatus(HttpStatus::is4xxClientError,
clientResponse -> Mono.error(new ResponseStatusException(clientResponse.statusCode(), NOT_FOUND_MSG)))
.onStatus(HttpStatus::is5xxServerError,
clientResponse -> Mono.error(new ResponseStatusException(clientResponse.statusCode(), INTERNAL_SERVER_ERROR_MSG)));
}
private Mono<ResponseEntity<T>> setEntity(final WebClient.ResponseSpec responseSpec, Class<T> classToMap) {
return responseSpec.toEntity(classToMap);
}
}
then I inject the CustomWebClient into another class which is a service
public class ABCServiceImpl implements ABCService {
private final CustomWebClient<? extends BaseResponse> customClient;
private Mono<PageResponse> invokeGetPageFromABC(final String storeId, final String site, final MultiValueMap<String, String> queryParams) {
Mono<ResponseEntity<PageResult>> responseEntityMono = customClient.setRequest(String.format(PAGE_ENDPOINT, site), HttpMethod.GET, queryParams, PageResult.class);
return responseEntityMono.map(content ->
new ContentResponse(content.getStatusCode(), customWebClient.SUCCESS_MSG, createContent(content.getBody())));
}
private ContentDTO createContent(ContentResult contentResult){
return Optional.ofNullable(contentResult)
.filter(c -> Objects.nonNull(c.getContent()))
.map(ContentResult::getContent)
.orElse(new ContentDTO());
}
}
at this point a get a compile time error which is
Required type: Class <capture of ? extends BaseResponse>
Provided: Class <ContentResult>
Here is the object I'm passing
public class ContentResult extends BaseResponse {
private ContentDTO content;
}
Since I'm new with generics probably there is something I'm missing or I didn't fully understand. Any clue about what could be the problem?
Honestly I've expected to work given the fact that I've actually specified the super class while injecting the customWebClient.
Thank you in advance!
I've combed through this, and I think I understand what you're trying to do. I recommend not using the wildcard ("?") in this way. When you call customClient.setRequest(..), the compiler can't figure out what version of customClient you have (i.e. what subclass of BaseResponse), so it throws an error when you try to pass in the parameterized type Class object (which looks to be PageResult.class)
You could fix this by turning ABCServiceImpl into a parameterized class i.e.
ABCServiceImpl<T extends BaseResponse> with a field property of private final CustomWebClient<T extends BaseResponse> customClient;
But I still wouldn't do that either, because it looks like your custom client does not need to be a parameterized type at all!
So I'd recommend this:
Remove the parameterized type from CustomWebClient. (i.e. public class CustomWebClient instead of public class CustomWebClient<T>)
Make the public Mono<ResponseEntity<T>> setRequest(..) method a parameterized method instead: public <T> Mono<ResponseEntity<T>> setRequest(..)
Call the method on customWebClient like so:
customWebClient.<ContentResult>setRequest(ContentResult.class, ...etc);
Related
Have a problem with optimizing search request.
I have search method that accepts parameters in url query like:
http://localhost:8080/api?code.<type>=<value>&name=Test
Example: http://localhost:8080/api?code.phone=9999999999&name=Test
Defined SearchDto:
public class SearchDto {
String name;
List<Code> code;
}
Defined Code class:
public class Code {
String type;
String value;
}
Currently I'm using Map<String,String> as incoming parameter for the method:
#GetMapping("/search")
public ResponseEntity<?> search(final #RequestParam Map<String, String> searchParams) {
return service.search(searchParams);
}
Then manually converting map values for SearchDto class. Is it possible to get rid of Map<String,String> and pass SearchDto directly as argument in controller method?
Passing a json in querystring is actually a bad practice, since it decrease the security and sets limits on the number of parameters you can send to your endpoint.
Technically speaking, you could make everything work by using your DTO as a controller's parameter, then URL encoding the json before you send it to the backend.
The best option, in your case, is to serve an endpoint that listen to a POST request: it is not an error, neither a bad practise, to use POST when performing a search.
you can customize a HandlerMethodArgumentResolver to implement it.
but , if you want a object receive incoming parameter. why not use POST
#Target({ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface Example {
}
public class ExampleArgumentResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
Example requestParam = parameter.getParameterAnnotation(Example.class);
return requestParam != null;
}
#Override
public Object resolveArgument(MethodParameter parameter, #Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, #Nullable WebDataBinderFactory binderFactory) throws Exception {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
Map<String, String[]> parameterMap = webRequest.getParameterMap();
Map<String, String> result = CollectionUtils.newLinkedHashMap(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
result.put(key, values[0]);
}
});
//here will return a map object. then you convert map to your object, I don't know how to convert , but you have achieve it.
return o;
}
}
add to container
#Configuration
#EnableWebMvc
public class ExampleMvcConfiguration implements WebMvcConfigurer {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new ExampleArgumentResolver());
}
}
usage
#RestController
public class TestCtrl {
#GetMapping("api")
public Object gg(#Example SearchDto searchDto) {
System.out.println(searchDto);
return "1";
}
#Data
public static class SearchDto {
String name;
List<Code> code;
}
#Data
public static class Code {
String type;
String value;
}
}
Here is a demo.
I have the following classes:
public class AccountDetail {
private String accountNumber;
private Date effectiveDate;
private String status;
// a bunch of other properties
}
public class AccountDetailWithAlerts extends AccountDetail {
private LowMediumAlerts alerts;
}
public class AccountsAndAlerts {
private List<AccountDetailWithAlerts> accounts;
private HighCustomerAccountAlerts accountAlerts;
// getters and setters
}
public class CustomerAndAccountAlerts {
private List<AlertMessage> customerAlerts;
private List<AccountAlertMessages> accountAlerts;
}
public Class CompanyResponse<T> {
#JsonInclude(JsonInclude.Include.NON_NULL)
private T response;
// other things that aren't relevant
}
I have a controller, AccountsController, that does a #GetMapping and has a ResponseEntity method:
public ResponseEntity<CompanyResponse<AccountsAndAlerts> getAccountDetails {
#RequestParam MultiValueMap<String, String> queryParms,
// some #ApiParams for client-header, end-user-id & accountNumber
String accountId = queryParms.getFirst("accountId");
// setting RestHeaders, contentType
CompanyResponse<AccountsAndAlerts> response = accountDetailService.getAccountsWithAlerts(restHeaders, accountNumber, queryParms, accountId);
return new ResponseEntity<CompanyResponse<AccountsAndAlerts>>(response, headers, HttpStatus.valueOf(response.getStatus()));
}
Here is the method in accountDetailService:
public CompanyResponse<AccountsAndAlerts> getAccountsWithAlerts(RestHeaders restHeaders, String accountNumber, MultiValueMap<String, String> queryParms, String accountId) throws... {
CompanyResponse<AccountsAndAlerts> newResponse = new CompanyResponse<AccountsAndAlerts>();
try {
CompletableFuture<List<AccountDetailWithAlerts>> accountsFuture = accountDetails.getAccounts(newResponse, restHeaders, accountNumber, queryParms);
CompletableFuture<CustomerAndAccountAlerts> alertsFuture = accountDetails.getAlerts(newResponse, restHeaders, accountId);
accountsFuture.thenAcceptBoth(alertsFuture, (s1, s2) -> newResponse.setResponse(getResponse(s1, s2))).get();
} catch {
// catch code
}
return newResponse;
}
Finally, the getAccounts method in AccountDetails:
public CompletableFuture<List<AccountDetailWithAlerts>> getAccounts(CompanyResponse<AccountsAndAlerts> newResponse, RestHeaders restHeaders, String accountNumber, MultiValueMap<String, String> queryParms) throws ... {
// this has the restTemplate and the .supplyAsync()
}
What I need to do is create a new ResponseEntityMethod in the Controller:
public ResponseEntity<CompanyResponse<AccountDetail> getCertainAccountDetails
I have put in a return of that type, and I am attempting to create a new method in the accountDetailService, getCertainAccounts().
The problem is trying to set this all up without creating a whole other CompletableFuture method with an invoke and supplyAsync() and restTemplate and such.
It appears that I still need to call getAccounts(), but then I have to somewhere along this line downcast the AccountDetailWithMessages to AccountDetail. I don't know if I can somehow downcast CompletableFuture<List<AccountDetailWithAlerts>> to CompletableFuture<List<AccountDetail>> or how to do it, or if I really need to downcast CompanyResponse<AccountsAndAlerts> or how to do that.
Can anyone help?
PS. I changed the names of everything to protect my Company's code. If you see errors in methods or names or anything, please be assured that is not an issue and is just the result of my typing things out instead of copying and pasting. The only issue is how to do the downcasting.
Thanks!
PPS. In case it wasn't clear, with my new method and code I do not want to get the alerts. I am trying to get account details only without alerts.
I have to deserialize my Java Object and I can't use static reference i.e TypeReference.
Like,
mentioned in here
So, I am left with is generating the right Javatype using type factory, but somehow I am not able to get the syntax right.
Aforementioned are my Classes and Interfaces.
public interface Request {}
public interface Response {}
public class MyRequest implements Request {
int id;
//Getter //Setter
}
public class MyResponse implements Response {
int id;
//Getter //Setter
}
public class UberObject<S extends Request, T extends Response> implements Serializable {
private S request;
private T response;
//Getter//Setter
}
public class UberObjectWithId<S extends Request, T extends Response> extends UberObject<S, T> {
private int id;
//Getter //Setter
}
UberObjectWithId typereferencedObject =
objectmapperobject.readValue(serialisedUberObjectWithId,
new TypeReference<UberObjectWithId<MyRequest, MyResponse>>() {});
The above approach works but I can't use TypeReference because of the limitation mentioned in the above link.
I tried using the Typefactory to retrieve the JavaType but I am not able to get the syntax right.
JavaType type = mapper.getTypeFactory().constructParametrizedType(UberObjectWithId.class,
UberObject.class, Request.class, Response.class);
but the call fails
objectmapperobject.readValue(serialisedUberObjectWithId, type);
How can I resolve my particular problem?
I fixed it by using the correct syntax to generate the JavaType
JavaType type = mapper.getTypeFactory().constructParametrizedType(UberObjectWithId.class,
UberObject.class, MyRequest.class, MyResponse.class);
It requires concrete classes while deserializing.
Our company is planning to switch our microservice technology to Spring Boot. As an initiative I did some advanced reading and noting down its potential impact and syntax equivalents. I also started porting the smallest service we had as a side project.
One issue that blocked my progress was trying to convert our Json request/response exchange to Spring Boot.
Here's an example of the code: (This is Nutz framework for those who don't recognize this)
#POST
#At // These two lines are equivalent to #PostMapping("/create")
#AdaptBy(type=JsonAdapter.class)
public Object create(#Param("param_1") String param1, #Param("param_2) int param2) {
MyModel1 myModel1 = new MyModel1(param1);
MyModel2 myModel2 = new MyModel2(param2);
myRepository1.create(myMode12);
myRepository2.create(myModel2);
return new MyJsonResponse();
}
On PostMan or any other REST client I simply pass POST:
{
"param_1" : "test",
"param_2" : 1
}
I got as far as doing this in Spring Boot:
#PostMapping("/create")
public Object create(#RequestParam("param_1") String param1, #RequestParam("param_2) int param2) {
MyModel1 myModel1 = new MyModel1(param1);
MyModel2 myModel2 = new MyModel2(param2);
myRepository1.create(myMode12);
myRepository2.create(myModel2);
return new MyJsonResponse();
}
I am not sure how to do something similar as JsonAdapter here. Spring doesn't recognize the data I passed.
I tried this but based on the examples it expects the Json paramters to be of an Entity's form.
#RequestMapping(path="/wallet", consumes="application/json", produces="application/json")
But I only got it to work if I do something like this:
public Object (#RequestBody MyModel1 model1) {}
My issue with this is that MyModel1 may not necessarily contain the fields/parameters that my json data has.
The very useful thing about Nutz is that if I removed JsonAdapter it behaves like a regular form request endpoint in spring.
I couldn't find an answer here in Stack or if possible I'm calling it differently than what existing spring devs call it.
Our bosses expect us (unrealistically) to implement these changes without forcing front-end developers to adjust to these changes. (Autonomy and all that jazz). If this is unavoidable what would be the sensible explanation for this?
In that case you can use Map class to read input json, like
#PostMapping("/create")
public Object create(#RequestBody Map<String, ?> input) {
sout(input.get("param1")) // cast to String, int, ..
}
I actually figured out a more straightforward solution.
Apparently this works:
#PostMapping("/endpoint")
public Object endpoint(#RequestBody MyWebRequestObject request) {
String value1 = request.getValue_1();
String value2 = request.getValue_2();
}
The json payload is this:
{
"value_1" : "hello",
"value_2" : "world"
}
This works if MyRequestObject is mapped like the json request object like so. Example:
public class MyWebRequestObject {
String value_1;
String value_2
}
Unmapped values are ignored. Spring is smart like that.
I know this is right back where I started but since we introduced a service layer for the rest control to interact with, it made sense to create our own request model object (DTOs) that is separate from the persistence model.
You can use #RequestBody Map as a parameter for #PostMapping, #PutMapping and #PatchMapping. For #GetMapping and #DeleteMapping, you can write a class which implements Converter to convert from json-formed request parameters to Map. And you would register that class as a bean with #Component annotation. Then you can bind your parameters to #RequestParameter Map.
Here is an example of Converter below.
#Component
public class StringToMapConverter implements Converter<String, Map<String, Object>> {
private final ObjectMapper objectMapper;
#Autowired
public StringToMapConverter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
#Override
public Map<String, Object> convert(String source) {
try {
return objectMapper.readValue(source, new TypeReference<Map<String, Object>>(){});
} catch (IOException e) {
return new HashMap<>();
}
}
}
If you want to exclude specific field of your MyModel1 class, use #JsonIgnore annotation onto the field like below.
class MyModel1 {
private field1;
#JsonIgnore field2;
}
Then, I guess you can just use what you have done.(I'm not sure.)
public Object (#RequestBody MyModel1 model1) {}
i think that you can use a strategy that involve dto
https://auth0.com/blog/automatically-mapping-dto-to-entity-on-spring-boot-apis/
you send a json to your rest api that is map like a dto object, after you can map like an entity or use it for your needs
try this:
Add new annotation JsonParam and implement HandlerMethodArgumentResolver of this, Parse json to map and get data in HandlerMethodArgumentResolver
{
"aaabbcc": "aaa"
}
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface JsonParam {
String value();
}
#Component
public class JsonParamMethodResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(JsonParam.class);
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
RepeatedlyRequestWrapper nativeRequest = webRequest.getNativeRequest(RepeatedlyRequestWrapper.class);
if (nativeRequest == null) {
return null;
}
Gson gson = new Gson();
Map<String, Object> response = gson.fromJson(nativeRequest.getReader(), new TypeToken<Map<String, Object>>() {
}.getType());
if (response == null) {
return null;
}
JsonParam parameterAnnotation = parameter.getParameterAnnotation(JsonParam.class);
String value = parameterAnnotation.value();
Class<?> parameterType = parameter.getParameterType();
return response.get(value);
}
}
#Configuration
public class JsonParamConfig extends WebMvcConfigurerAdapter {
#Autowired
JsonParamMethodResolver jsonParamMethodResolver;
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(jsonParamMethodResolver);
}
}
#PostMapping("/methodName")
public void methodName(#JsonParam("aaabbcc") String ddeeff) {
System.out.println(username);
}
I am designing a REST API using JAX-RS. The endpoint looks like the following:
#GET
#Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public Response get(
#QueryParam("param1") final String param1,
#QueryParam("param2") final String param2) {
// Code goes here
}
I have nearly 7-8 parameters. So I would like to do something like the following:
#GET
#Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public Response get(#Context MyContextObject context) {
// Code goes here
}
And I have the context object as follows:
final class MyContextObject {
// get methods
public MyContextObject(final Builder builder) {
// set final fields
}
private final String param1;
private final String param2;
public static final class Builder {
// builder code goes here
}
}
Can you please advise how I can do that?
Thanks in advance.
If you want to do by creating separate bean class as you said, you would need to get the query parameters in bean class like below.
final class MyContextObject {
// get methods
public MyContextObject(final Builder builder) {
// set final fields
}
private #QueryParam("param1") final String param1;
private #QueryParam("param2") final String param2;
//and so on...
public static final class Builder {
// builder code goes here
}
}
If you do so, the query parameters get bounded to these private variables in the bean class and you would get them in your rest service using getters.