So I'm developping some microservices in JAVA using Spring Boot and I'm facing some problems involving the objects I'm using.
So I have a data service which is the DB interface and a scheduling service which will be called by the frontend.
Both work with their own Response and Request objects eventhough at this point they are basically the same.
please ignore that there are no getters and setters in the code below.
Data-Service
#RestController
#RequestMapping("")
public class DataServiceResource {
#GetMapping(...)
public ResponseEntity<JobDetailsResponse> getJobDetailsSingleDate(#PathVariable("singledate") final String date) {
...
return response;
}
}
JobDetailsResponse
#JsonIgnoreProperties(ignoreUnknown = true)
public class JobDetailsResponse {
private Object requestSent;
private List<Job> jobsFound;
private boolean hasError;
private String errorMessage;
private LocalDateTime dataTimestamp;
}
JobDetailsSingleDateRequest
#JsonIgnoreProperties(ignoreUnknown = true)
public class JobDetailsSingleDateRequest {
private String dateFrom;
}
Scheduling Service
#RestController
#RequestMapping("")
public class SchedulingServiceResource {
...
#Autowired
private RestTemplate restTemplate;
#GetMapping(...)
public ResponseEntity<ReportDetailsResponse> getReportDetailsSingleDate(#PathVariable("singledate") final String singledate) {
ResponseEntity<ReportDetailsResponse> quoteResponse = this.restTemplate.exchange(DATA_SERVICE_JOB_DETAILS_SINGLE_DATE_URL + singledate, HttpMethod.GET,
null, new ParameterizedTypeReference<ReportDetailsResponse>() {});
...
return response;
}
ReportDetailsSingleDateRequest
#JsonIgnoreProperties(ignoreUnknown = true)
public class ReportDetailsSingleDateRequest {
private String dateFrom;
}
ReportDetailsResponse
#JsonIgnoreProperties(ignoreUnknown = true)
public class ReportDetailsResponse {
private Object requestSent;
private List<Job> jobsFound;
private boolean hasError;
private String errorMessage;
private LocalDateTime dataTimestamp;
}
So when I go through the quoteResponse.getBody().getJobsFound() method to check the data I got from the Data Service My List of jobs is empty.
I read that If the objects are equal in definition, spring would use reflection to pass the values, but in my case its not woking.
Is there a way to consume the microservice without having to add the data service dependency to the scheduling service?
Sorry for the long post but, until now I haven't found a proper example for my case. All the examples I found work with List as return of the microservice.
Thanks in advance.
Related
I'm working on the Spring application with Angular frontend. I'm trying to send a POST request to update data in the database but there is an exception:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot
construct instance of UpdateRecipesDTO
There is the DTO class that I receive from the frontend part:
#Data
public class UpdateRecipesDTO {
private String query;
private String meal;
private String dish;
private String cuisine;
}
And it goes to the controller method
#PostMapping(value = "/recipe-update")
public int updateByAdmin(HttpServletRequest request, #RequestBody UpdateRecipesDTO updateRecipesDTO) {
System.out.println("You're here");
}
Now the only that I want is a printed string but it doesn't print anything. I had tried to use #AllArgsConstructor Lombok annotation, but it didn't work. Writing AllArgsConstructor by myself didn't help also. So what should I do?
I have created an index (house) with a type "apartments" that contains 20 documents. I uploaded the Json as a binary file into elasticsearch using postman. I have a Spring Boot project that has the following classes:
EsConfig.java - I have configured the clustername which is the default name in the application.properties file.
#Configuration
#EnableElasticsearchRepositories(basePackages = "com.search.repository")
public class EsConfig {
#Value("${elasticsearch.clustername}")
private String EsClusterName;
#Bean
public Client esClient() throws UnknownHostException {
Settings esSettings = Settings.builder()
.put("cluster.name", EsClusterName)
.put("client.transport.sniff", true)
.put("client.transport.ignore_cluster_name", false)
.build();
TransportClient client = new PreBuiltTransportClient(esSettings)
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("localhost"), 9300));
return client;
}
#Bean
public ElasticsearchOperations elasticsearchTemplate() throws Exception{
return new ElasticsearchTemplate(esClient());
}
}
Apartments.java - This is my data model. The documents have the below fields in elasticsearch.
#Document(indexName = "house", type = "apartments")
#JsonIgnoreProperties(ignoreUnknown=true)
public class Apartments {
#Id
private String id;
#JsonProperty("Apartment_Name")
private String apartmentName;
#JsonProperty("Apartment_ID")
private String apartmentId;
#JsonProperty("Area_Name")
private String areaName;
//constructors along with getters and setters
}
ApartmentSearchRepository.java - This is an interface that extends the ElasticsearchRepository interface to perform crud operations.
public interface ApartmentSearchRepository extends ElasticsearchRepository<Apartments, String> {
List<Apartments> findByApartmentName(String apartmentName);
}
EsApartmentService.java -
#Service
public class EsApartmentService {
#Autowired
ApartmentSearchRepository apartmentSearchRepository;
public List<Apartments> getApartmentByName(String apartmentName) {
return apartmentSearchRepository.findByApartmentName(apartmentName);
}
}
ApartmentController.java - I have created an endpoint that should give back those 20 documents from elasticsearch. (Also, Apartment is a POJO in my project and Apartments is the data model.)
#Autowired
EsApartmentService esApartmentService;
#GetMapping(path = "/search",produces = "application/json")
public Set<Apartment> searchApartmentByName(
#RequestParam(value = "apartmentName", defaultValue = "") String apartmentName) throws IOException {
List<Apartment> apartments= new ArrayList<>();
esApartmentService.getApartmentByName(apartmentName).forEach(apartment-> {
apartments.add(new Apartment(apartment.getApartmentName(), apartment.getApartmentId(), apartment.getAreaName()));
});
return apartments.stream()
.collect(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Apartment::getApartmentId))));
}
This code gives back a status of 200 but with an empty response. I tried debugging but it seems that it is unable to read those documents from elasticsearch. I went through a couple of solutions but most of them have set the document data from within the code itself.
I am unable to retrieve those documents by hitting the endpoint I specified in the controller. Can someone let me know what I could be missing out on? Thanks! :)
Edit: The screenshot below shows the query and response in Postman.
As far I know, you are able to use #JsonProperty in order to map the POJO to the query response but you're loosing the ability to use the dynamic finder methods (findBy*) of spring data. The dynamic finders generation of spring data relies on reflection and there is where the field names in your POJO become important.
Would you mind to change the field names of you POJO or in your documents to verify this? Or just define a custom query? There is also a powerfull java api where you can define more complex queries: https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#elasticsearch.misc.filter
As mentioned above by #ibexit, I removed #JsonProperty and used the native search query builder in my service. Also, it was not taking Apartment_Name and worked when I gave apartment_Name. (seems like Elasticsearch has case issues so I gave it in Camel Case.)
My changes:
Apartments.java - Removed #JsonProperty
#Document(indexName = "house", type = "apartments")
//#JsonIgnoreProperties(ignoreUnknown=true)
public class Apartments {
#Id
private String id;
//#JsonProperty("apartment_ID")
private String apartment_ID;
//#JsonProperty("Area_Name")
private String area_Name;
//#JsonProperty("Apartment_Name")
private String apartment_Name;
}
EsApartmentService.java -
#Service
public class EsApartmentService {
#Autowired
private ElasticsearchTemplate elasticsearchTemplate;
public List<Apartments> getApartmentByName(String apartmentName) {
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(org.elasticsearch.index.query.QueryBuilders
.matchQuery("apartment_Name", apartmentName)).build();
Page<Apartments> sampleEntities =
elasticsearchTemplate.queryForPage(searchQuery,Apartments.class);
return sampleEntities.getContent();
}
}
Removed ApartmentSearchRepository.java file.
These changes gave me the required response! :)
struggling long time with that issue and I have the weird feeling it has something to do on how I am setting my #Transactional annotation.
So what do I want to do?
I am preparing some data and save them with the available repositories in the database.
This can be found here in my FormTest class in the prepareExampleApplication method
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#ActiveProfiles("test")
#DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class FormTest {
#Autowired
private ApplicationRepository applicationRepository;
#Autowired
private AppActionRepository appActionRepository;
#Autowired
private RoleRepository roleRepository;
#Autowired
private TestRestTemplate restTemplate;
#Autowired
private FormRepository formRepository;
#Autowired
private FormElementRepository formElementRepository;
#Autowired
private SectionRepository sectionRepository;
private Application application;
private InputField refInputField;
private Select refSelectBox;
#Before
public void prepareExampleApplication() {
Form form = formRepository.save(ModelFactory.getForm("Project"));
Application application = ModelFactory.getApplication("Example", form);
this.application = applicationRepository.save(application);
Role role = new Role();
role.setRoleName("ADMIN");
role.setApp(application);
role = roleRepository.save(role);
Section section = ModelFactory.getSection(form, null, null);
section = formElementRepository.save(section);
InputField inputField = ModelFactory.getInputField(form, section, section);
refInputField = formElementRepository.save(inputField);
//once again. Just for my own eyes to see if it is there
Iterable<Form> all = formRepository.findAll();
// lot more stuff
}
#Test
#Transactional
public void testUserInput() {
// first create a new container to give inouts
Long id = this.application.getEntity().getId();
// for the sake of debugging I am using the formRepo to really SEARCH for the persisted form AND IT IS THERE!!!
Form byId = formRepository.findById(id).orElseThrow(NotFoundException::new);
URI uri = this.restTemplate.postForLocation("/api/form/" + id + "/forminstance", null);
long containerId = TestHelper.extractId(uri);
}
}
The next thing I am doing having this data is to use the restTemplate and dispatch a post request to a REST service. You can find the POST call in the test method. For debugging reasons - and to see that the repo is working - I am REALLY using the repository to get the id of the form instead of using the class field that had been filled with the preparation method. AND the form will be returned!!
Within my rest service I am using the formRepository once again, looking for the entity I have found before in the test class. But THIS TIME the repository does not return anything. Only null. I have tried SO MANY different things with setting #Transactional on different locations, but whatever I do the formRepository within the REST service does only give me back 0 entites and a null. Here is the REST service
#Service
#Api("FormService")
#Path("/form")
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public class FormService {
#Autowired
private FormInstanceRepository formInstanceRepository;
#Autowired
private FormRepository formRepository;
#Autowired
private FormElementRepository formElementRepository;
#Autowired
private UserInputRepository userInputRepository;
#POST
#Path("{id}/forminstance")
#Transactional
public Response createFormInstance(#Context UriInfo info, #PathParam("id") long formId) {
// returns ALWAYS 0 elements
Iterable<Form> all = formRepository.findAll();
// returns always null
Form form = formRepository.findById(formId).orElse(null);
FormInstance formInstance = new FormInstance();
formInstance.setForm(form);
FormInstance save = formInstanceRepository.save(formInstance);
UriBuilder builder = info.getAbsolutePathBuilder();
builder.path(Long.toString(save.getId()));
return Response.created(builder.build()).build();
}
IF you know the answer I am really interested in the explanation to understand my error. I am using an in-memory H2 db for the tests.
Adding the Form entity and FormRepository, too
#Entity
#Data
public class Form {
#Id
#GeneratedValue
private Long id;
private String name;
}
-
public interface FormRepository extends CrudRepository<Form, Long> {
}
Thanks in advance for your help!!
From the frontend I receive GET-request which contains encoded json string as one of its parameters:
http://localhost:8080/engine/template/get-templates?context=%7B%22entityType%22%3A%22DOCUMENT%22%2C%22entityId%22%3A%22c7a2a0c6-fd34-4f33-9cb8-14c2090565ea%22%7D&page=1&start=0&limit=25
Json-parameter 'context' without encoding (UUID is random):
{"entityType":"DOCUMENT","entityId":"c7a2a0c6-fd34-4f33-9cb8-14c2090565ea"}
On backend my controller's method which handle that request looks like this:
#RequestMapping(value = "/get-templates", method = RequestMethod.GET)
public List<Template> getTemplates(#RequestParam(required = false, name = "context") Context context) {
//...
}
'Context' domain class:
public class Context {
private String entityType;
private UUID entityId;
public String getEntityType() {
return entityType;
}
public void setEntityType(String entityType) {
this.entityType = entityType;
}
public UUID getEntityId() {
return entityId;
}
public void setEntityId(UUID entityId) {
this.entityId = entityId;
}
}
I believed Spring's Jackson module would automatically convert that kind of json to java object of Context class, but when I run this code it gives me exception:
org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException: Failed to convert value of type 'java.lang.String' to required type 'com.company.domain.Context'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'com.company.domain.Context': no matching editors or conversion strategy found
On StackOverflow I've seen similar questions, but those were about POST-requests handling (with #RequestBody annotation), which doesn't fit with GET-request.
Could you help me to solve this problem?
Thanks in advance.
I think you need to specify that your GET mapping is looking to consume JSON:
#RequestMapping(value = "/get-templates", method = RequestMethod.GET, consumes = "application/json")
public List<Template> getTemplates(#RequestParam(required = false, name = "context") Context context) {
//...
}
If this doesn't work then you can call the Jackson ObjectMapper yourself:
#RequestMapping(value = "/get-templates", method = RequestMethod.GET)
public List<Template> getTemplates(#RequestParam(required = false, name = "context") String context) {
ObjectMapper mapper = new ObjectMapper();
Context myContext = mapper.readValue(context, Context.class);
//...
}
As far as I know Spring does not have the mechanism to convert from a String to a UUID in older releases. In such case you should declare you entityId as a String and then use a converter in order to convert it to UUID.
So your Context class should be like below:
public class Context {
private String entityType;
private String entityId;
public String getEntityType() {
return entityType;
}
public void setEntityType(String entityType) {
this.entityType = entityType;
}
public String getEntityId() {
return entityId;
}
public void setEntityId(String entityId) {
this.entityId = entityId;
}
public UUID getEntityIdAsUUID() {
return convertToUUID(this.entityId);
}
// Helper Conversion String to UUID method
private UUID convertToUUID(String entityId){
return UUID.fromString(entityId);
}
}
I faced the same issue in both Jersey and Spring MVC when trying to convert the JSON String {"x":"1001822.831","y":"200716.8913"} to a object of a class called Point
Point class is as below
public class Point
{
private Double x;
private Double y;
//getters and setters
}
As per Jersey documentation, I added the below method to Point class and it worked for both Jersey and Spring MVC.
//used by jax rs & spring mvc for converting queryParam String to Point
public static Point valueOf(String json) throws IOException
{
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(json, Point.class);
}
Please refer to section 3.2 here https://jersey.github.io/documentation/latest/jaxrs-resources.html#d0e2271
I have a bean that has a lot of fields annotated with JSR-303 validation annotations. There is a new requirement now that one of the fields is mandatory, but only in certain conditions.
I looked around and have found what I needed, validation groups.
This is what I have now:
public interface ValidatedOnCreationOnly {
}
#NotNull(groups = ValidatedOnCreationOnly.class)
private String employerId;
#Length(max = 255)
#NotNull
private String firstName;
#Length(max = 255)
#NotNull
private String lastName;
However, when I run this validation in a unit test:
#Test
public void testEmployerIdCanOnlyBeSetWhenCreating() {
EmployeeDTO dto = new EmployeeDTO();
ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
Set<ConstraintViolation<EmployeeDTO>> violations = vf.getValidator().validate(dto, EmployeeDTO.ValidatedOnCreationOnly.class);
assertEquals(violations.size(), 3);
}
It turns out that all of the non-group annotated validations are ignored and I get only 1 violation.
I can understand this behaviour but I would like to know if there is a way I can make the group include all non-annotated parameters as well. If not I'd have to do something like this:
public interface AlwaysValidated {
}
public interface ValidatedOnCreationOnly extends AlwaysValidated {
}
#NotNull(groups = ValidatedOnCreationOnly.class)
private String employerId;
#Length(max = 255, groups = AlwaysValidated.class)
#NotNull(groups = AlwaysValidated.class)
private String firstName;
#Length(max = 255, groups = AlwaysValidated.class)
#NotNull(groups = AlwaysValidated.class)
private String lastName;
The real class I'm working with has a lot more fields (about 20), so this method turns what was a clear way of indicating the validations into a big mess.
Can anyone tell me if there is a better way? Maybe something like:
vf.getValidator().validate(dto, EmployeeDTO.ValidatedOnCreationOnly.class, NonGroupSpecific.class);
I'm using this in a spring project so if spring has another way I'll be glad to know.
There is a Default group in javax.validation.groups.Default, which represents the default Bean Validation group. Unless a list of groups is explicitly defined:
constraints belong to the Default group
validation applies to the Default group
You could extends this group:
public interface ValidatedOnCreationOnly extends Default {}
just wanted to add more:
if you're using spring framework you can use org.springframework.validation.Validator
#Autowired
private Validator validator;
and to perform validation manually:
validator.validate(myObject, ValidationErrorsToException.getInstance());
and in controller:
#RequestMapping(method = RequestMethod.POST)
public Callable<ResultObject> post(#RequestBody #Validated(MyObject.CustomGroup.class) MyObject request) {
// logic
}
although in this way extending from javax.validation.groups.Default won't work so you have to include Default.class in groups:
class MyObject {
#NotNull(groups = {Default.class, CustomGroup.class})
private String id;
public interface CustomGroup extends Default {}
}
For me add Default.class everywhere is not good approach.
So I extended LocalValidatorFactoryBean which validate with some group and delegate for validation without any group.
I used spring boot 2.2.6.RELEASE
and I used spring-boot-starter-validation dependency.
My bean for validattion
public class SomeBean {
#NotNull(groups = {UpdateContext.class})
Long id;
#NotNull
String name;
#NotNull
String surName;
String optional;
#NotNull(groups = {CreateContext.class})
String pesel;
#Valid SomeBean someBean;
}
code of own class which extends LocalValidatorFactoryBean
public class CustomValidatorFactoryBean extends LocalValidatorFactoryBean {
#Override
public void validate(Object target, Errors errors, Object... validationHints) {
if (validationHints.length > 0) {
super.validate(target, errors, validationHints);
}
super.validate(target, errors);
}
}
Put it to spring context via #Bean or just with #Component (as you wish)
#Bean
#Primary
public LocalValidatorFactoryBean customLocalValidatorFactoryBean() {
return new CustomValidatorFactoryBean();
}
usage of it in some RestController
// So in this method will do walidation on validators with CreateContext group and without group
#PostMapping("/create")
void create(#RequestBody #Validated(CreateContext.class) SomeBean someBean) {
}
#PostMapping("/update")
void update(#RequestBody #Validated(UpdateContext.class) SomeBean someBean) {
}
Due to some reason testValidation is not working when is invoked DummyService.testValidation() by RestController or other spring bean.
Only on RestController side is working :/
#Validated
#Service
#AllArgsConstructor
class DummyService {
public void testValidation(#NotNull String string, #Validated(UpdateContext.class) SomeBean someBean) {
System.out.println(string);
System.out.println(someBean);
}
}