How to make advice in Spring & aspectJ - spring

I tried to create Pointcut & advice for annotation Scheduled and public methods in com.example package, but it doesnt work. When I tryed to call services in com.example, advice doesnt work. Also for annotation #Scheduled it doesnt work. I try to read documentation, it seems that it should work, but in reality it doesnt work. Can please someone give me a point, how to solve this issue.
package com.dhl.common.logging;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.MDC;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import java.net.InetAddress;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
#Aspect
#Configuration
#Order(Ordered.HIGHEST_PRECEDENCE)
public class LoggingAspect {
private static final String DATE_FORMATTER= "MMM dd, yyyy'T'HH:mm:ss.SSS";
public static final String LOG_LEVEL_KEY = "LOG_LEVEL";
public static final String APP_NAME_KEY = "APP_NAME";
public static final String LOGGER_CLASS_NAME_KEY = "LOGGER_CLASS_NAME";
public static final String SERVER_NAME_KEY = "SERVER_NAME";
public static final String FORMATED_TIME_KEY = "FORMATED_TIME";
public static final String ENVIROMENT_KEY = "ENVIROMENT";
public static final String DISTRIBUTE_TRACE_ID_KEY = "DISTRIBUTE_TRACE_ID";
#Pointcut("#annotation(org.springframework.scheduling.annotation.Scheduled)")
private void scheduled() {}
#Pointcut("within(com.example..*)")
private void service() {}
#Around("scheduled() && service()")
public Object connectionAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
MDC.put(LOG_LEVEL_KEY, "INFO");
MDC.put(APP_NAME_KEY, "CRDB");
MDC.put(LOGGER_CLASS_NAME_KEY, joinPoint.getSourceLocation().getWithinType().toString());
String serverName = InetAddress.getLocalHost().getHostName();
MDC.put(SERVER_NAME_KEY, serverName);
MDC.put(FORMATED_TIME_KEY, getFormatedTime());
if(serverName != null) {
if(serverName.toUpperCase().contains("LOCALHOST")) {
MDC.put(ENVIROMENT_KEY,"LOCALHOST");
} else if(serverName.toUpperCase().contains("TEST")) {
MDC.put(ENVIROMENT_KEY,"TEST");
} else if(serverName.toUpperCase().contains("UAT")) {
MDC.put(ENVIROMENT_KEY,"UAT");
} else {
MDC.put(ENVIROMENT_KEY,"PRODUCTION");
}
}
MDC.put(DISTRIBUTE_TRACE_ID_KEY, UUID.randomUUID().toString());
try {
return joinPoint.proceed();
}
finally {
// Might as well clear all the MDC, not just the "myId"
MDC.clear();
}
}
private String getFormatedTime() {
LocalDateTime localDateTime = LocalDateTime.now(); //get current date time
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMATTER);
String formatDateTime = localDateTime.format(formatter);
return formatDateTime;
}
}

There are a list of things that you may forgot:
You may forgot to add the aspect class to your context, as I see that #Component is not on the code. There are a few ways to do it, i.e. i.e. through xml, programmatically or through spring's #ComponentScan.
While assigning point cut through annotation should work, we should take precaution to where the annotated method is called. Spring AOP will not trigger the Advice when the method is called from within the class itself. You can see why here.

Related

How to us a constructor with parameters in a method used by Spring Boot's #RestController annotation to create a request handler

I bought this new book to try to learn Spring Boot quickly. It started out well, and I easily created a REST API. But then we added CrudRepository, and I'm seeing issues with the code as described in the book. Also, there is no code available to download because the author took it down from Oreily's git repo in order to fix some things...
The issue is that if I try to build the code as the book describes (without a default constructor) I get a Java error complaining that there is no default constructor. If I add a default constructor, it builds, but Spring uses it instead of the new constructor, that requires a parameter to be passed. So when I actually call the API, like if I call the /coffees endpoint, I get a java.lang.NullPointerException: null
So how is Spring supposed to know which constructor to use, and how could it pass in values for this parameter?
Here is the controller:
package com.bw.restdemo;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
#RestController
#RequestMapping("/coffees")
class RestAPIDemoController {
private final CoffeeRepository coffeeRepository;
public RestAPIDemoController(CoffeeRepository coffeeRepository) {
this.coffeeRepository = coffeeRepository;
this.coffeeRepository.saveAll(List.of(
new Coffee("Cafe Cereza"),
new Coffee("Freedom Fuel"),
new Coffee("Cold Brew"),
new Coffee("Sumatra")
));
}
public RestAPIDemoController() {
this.coffeeRepository = null;
};
//#RequestMapping(value = "/coffees", method = RequestMethod.GET)
#GetMapping
Iterable<Coffee> getCoffees() {
return coffeeRepository.findAll();
}
#GetMapping("/{id}")
Optional<Coffee> getCoffeeById(#PathVariable String id) {
return coffeeRepository.findById(id);
}
#PostMapping
Coffee postCoffee(#RequestBody Coffee coffee) {
return coffeeRepository.save(coffee);
}
#PutMapping("/{id}")
ResponseEntity<Coffee> putCoffee(#PathVariable String id, #RequestBody Coffee coffee) {
return (!coffeeRepository.existsById(id))
? new ResponseEntity<>(coffeeRepository.save(coffee), HttpStatus.CREATED)
: new ResponseEntity<>(coffeeRepository.save(coffee), HttpStatus.OK);
}
#DeleteMapping("/{id}")
void deleteCoffee(#PathVariable String id) {
coffeeRepository.deleteById(id);
}
}
Here is where I'm defining the interface:
package com.bw.restdemo;
import org.springframework.data.repository.CrudRepository;
interface CoffeeRepository extends CrudRepository<Coffee, String> {
}
And here's the main class -- apologies for the class stuffed at the bottom.
package com.bw.restdemo;
import java.util.UUID;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class RestDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RestDemoApplication.class, args);
}
}
#Entity
class Coffee {
#Id
private String id;
private String name;
public Coffee(String id, String name) {
this.id = id;
this.name = name;
}
public void setId(String id) {
this.id = id;
}
public Coffee(String name) {
this(UUID.randomUUID().toString(), name);
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
CoffeeRepository interface is missing #Repository Annotation.
Update:
Add #Repository Annotation at CoffeeRepository
Remove the default constructor from RestAPIDemoController.
package com.bw.restdemo;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
#Repository
interface CoffeeRepository extends CrudRepository<Coffee, String> {
}
Explanation
In spring framework, #Component annotation marks a java class as a bean so the component-scanning mechanism can pick it up and pull it into the application context. As #Repository serves as a specialization of #Component , it also enable annotated classes to be discovered and registered with application context.
More at HowToDoInJava - #Repository annotation in Spring Boot

How to use spring to marshal and unmarshal xml?

I have a spring boot project. I have a few xsds in my project. I have generated the classes using maven-jaxb2-plugin. I have used this tutorial to get a sample spring boot application running.
import org.kaushik.xsds.XOBJECT;
#SpringBootApplication
public class JaxbExample2Application {
public static void main(String[] args) {
//SpringApplication.run(JaxbExample2Application.class, args);
XOBJECT xObject = new XOBJECT('a',1,2);
try {
JAXBContext jc = JAXBContext.newInstance(User.class);
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(xObject, System.out);
} catch (PropertyException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (JAXBException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
But my concern is that I need to have all the jaxb classes of the schema mapped. Also is there something in Spring that I can use to make my task easier. I have looked at the Spring OXM project but it had application context configured in xml. Does spring boot have anything that I can use out of the box. Any examples will be helpful.
Edit
I tried xerx593's answer and I ran a simple test using main method
JaxbHelper jaxbHelper = new JaxbHelper();
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(XOBJECT.class);
jaxbHelper.setMarshaller(marshaller);
XOBJECT xOBJECT= (PurchaseOrder)jaxbHelper.load(new StreamSource(new FileInputStream("src/main/resources/PurchaseOrder.xml")));
System.out.println(xOBJECT.getShipTo().getName());
It ran perfectly fine. Now I just need to plug it in using spring boot.
OXM is definitely the right for you!
A simple java configuration of a Jaxb2Marshaller would look like:
//...
import java.util.HashMap;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
//...
#Configuration
public class MyConfigClass {
#Bean
public Jaxb2Marshaller jaxb2Marshaller() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(new Class[]{
//all the classes the context needs to know about
org.kaushik.xsds.All.class,
org.kaushik.xsds.Of.class,
org.kaushik.xsds.Your.class,
org.kaushik.xsds.Classes.class
});
// "alternative/additiona - ly":
// marshaller.setContextPath(<jaxb.context-file>)
// marshaller.setPackagesToScan({"com.foo", "com.baz", "com.bar"});
marshaller.setMarshallerProperties(new HashMap<String, Object>() {{
put(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, true);
// set more properties here...
}});
return marshaller;
}
}
In your Application/Service class you could approach like this:
import java.io.InputStream;
import java.io.StringWriter;
import javax.xml.bind.JAXBException;
import javax.xml.transform.Result;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
#Component
public class MyMarshallerWrapper {
// you would rather:
#Autowired
private Jaxb2Marshaller marshaller;
// than:
// JAXBContext jc = JAXBContext.newInstance(User.class);
// Marshaller marshaller = jc.createMarshaller();
// marshalls one object (of your bound classes) into a String.
public <T> String marshallXml(final T obj) throws JAXBException {
StringWriter sw = new StringWriter();
Result result = new StreamResult(sw);
marshaller.marshal(obj, result);
return sw.toString();
}
// (tries to) unmarshall(s) an InputStream to the desired object.
#SuppressWarnings("unchecked")
public <T> T unmarshallXml(final InputStream xml) throws JAXBException {
return (T) marshaller.unmarshal(new StreamSource(xml));
}
}
See Jaxb2Marshaller-javadoc, and a related Answer
If you just want serializing/deserializing bean with XML. I think jackson fasterxml is one good choice:
ObjectMapper xmlMapper = new XmlMapper();
String xml = xmlMapper.writeValueAsString(new Simple()); // serializing
Simple value = xmlMapper.readValue("<Simple><x>1</x><y>2</y></Simple>",
Simple.class); // deserializing
maven:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
Refer: https://github.com/FasterXML/jackson-dataformat-xml
Spring BOOT is very smart and it can understand what you need with a little help.
To make XML marshalling/unmarshalling work you simply need to add annotations #XmlRootElement to class and #XmlElement to fields without getter and target class will be serialized/deserialized automatically.
Here is the DTO example
package com.exmaple;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;
import java.util.Date;
import java.util.Random;
#AllArgsConstructor
#NoArgsConstructor
#ToString
#Setter
#XmlRootElement
public class Contact implements Serializable {
#XmlElement
private Long id;
#XmlElement
private int version;
#Getter private String firstName;
#XmlElement
private String lastName;
#XmlElement
private Date birthDate;
public static Contact randomContact() {
Random random = new Random();
return new Contact(random.nextLong(), random.nextInt(), "name-" + random.nextLong(), "surname-" + random.nextLong(), new Date());
}
}
And the Controller:
package com.exmaple;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
#Controller
#RequestMapping(value="/contact")
public class ContactController {
final Logger logger = LoggerFactory.getLogger(ContactController.class);
#RequestMapping("/random")
#ResponseBody
public Contact randomContact() {
return Contact.randomContact();
}
#RequestMapping(value = "/edit", method = RequestMethod.POST)
#ResponseBody
public Contact editContact(#RequestBody Contact contact) {
logger.info("Received contact: {}", contact);
contact.setFirstName(contact.getFirstName() + "-EDITED");
return contact;
}
}
You can check-out full code example here: https://github.com/sergpank/spring-boot-xml
Any questions are welcome.
You can use StringSource / StringResult to read / read xml source with spring
#Autowired
Jaxb2Marshaller jaxb2Marshaller;
#Override
public Service parseXmlRequest(#NonNull String xmlRequest) {
return (Service) jaxb2Marshaller.unmarshal(new StringSource(xmlRequest));
}
#Override
public String prepareXmlResponse(#NonNull Service xmlResponse) {
StringResult stringResult = new StringResult();
jaxb2Marshaller.marshal(xmlResponse, stringResult);
return stringResult.toString();
}

Entity not getting saved in EclipseLink EntityListener

I have written EntityListener using eclipseLink's "DescriptorEventAdapter". I tried almost all variations whatever present online BUT the entity which I am saving from my listener is not getting saved. I suspect something fishy is going on with transaction but didn't get the root cause. Here is the code :
package com.db;
import java.util.Date;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManagerFactory;
import javax.transaction.Transactional;
import javax.transaction.Transactional.TxType;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.DescriptorEvent;
import org.eclipse.persistence.descriptors.DescriptorEventAdapter;
import org.eclipse.persistence.jpa.JpaEntityManager;
import org.eclipse.persistence.queries.InsertObjectQuery;
import org.eclipse.persistence.queries.UpdateObjectQuery;
import org.eclipse.persistence.sessions.changesets.DirectToFieldChangeRecord;
import org.eclipse.persistence.sessions.changesets.ObjectChangeSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
#Component
public class NotificationMessageListener extends DescriptorEventAdapter {
public static Logger logger = LoggerFactory.getLogger(NotificationMessageListener.class);
private static final String targetColumn = "STATUS";
//private static AuditRepository auditRepo;
#Autowired
private StatusAuditRepository statusAuditRepo;
#Autowired
private RuleResultAuditRepository ruleResultRepo;
#Autowired
private EntityManagerFactory factory;
JpaEntityManager entityManager = null;
#PostConstruct
public void init() {
try {
entityManager = (JpaEntityManager) factory.createEntityManager();
// Use the entity manager to get a ClassDescriptor for the Entity class
ClassDescriptor desc =
entityManager.getSession().getClassDescriptor(NotificationMessage.class);
// Add this class as a listener to the class descriptor
desc.getEventManager().addListener(this);
} finally {
if (entityManager != null) {
// Cleanup the entity manager
entityManager.close();
}
}
}
/*#Autowired
public void setAuditRepo(AuditRepository auditRepo) {
NotificationMessageListener.auditRepo = auditRepo;
}*/
#Transactional(value = TxType.REQUIRES_NEW)
#Override
public void postInsert(DescriptorEvent event) {
logger.info("post insert is called ");
//NotificationMessage notificationMsg = (NotificationMessage) ((InsertObjectQuery) event.getQuery()).getObject();
//entityManager.getTransaction().begin();
NotificationStatusAudit statusAudit = new NotificationStatusAudit();
statusAudit.setInsertionTime(new Date());
//statusAudit.setNewVal(notificationMsg.getStatus());
statusAudit.setNewVal("abc");
statusAudit.setOldval("asdf");
statusAudit.setTargetColumnName("from listner");
//statusAudit.setTargetRecordId(notificationMsg.getId());
statusAudit.setTargetRecordId(123L);
statusAudit = statusAuditRepo.save(statusAudit);
//entityManager.getTransaction().commit();
//logger.info("Number of records "+statusAuditRepo.count());
//auditRuleResult(notificationMsg.getMessageCorrelationId() , true);
}
#Override
public void postUpdate(DescriptorEvent event) {
ObjectChangeSet objectChanges = ((UpdateObjectQuery) event.getQuery()).getObjectChangeSet();
DirectToFieldChangeRecord statusChanges = (DirectToFieldChangeRecord) objectChanges
.getChangesForAttributeNamed("status");
if (statusChanges != null && !statusChanges.getNewValue().equals(statusChanges.getOldValue())) {
NotificationStatusAudit statusAudit = new NotificationStatusAudit();
statusAudit.setInsertionTime(new Date());
statusAudit.setNewVal("abc");
statusAudit.setOldval("asdf");
statusAudit.setTargetColumnName(targetColumn);
statusAudit.setTargetRecordId((Long) objectChanges.getId());
statusAudit = statusAuditRepo.save(statusAudit);
}
}
}
Here all I have to do is save the record in another (Audit) table when data is getting inserted in one table. My application is spring boot app and am using eclipseLink for persistent. I had to manually register my entity-listener in "PostConstruct" because if it is registered using #EntityListner annotation , spring-data-repos were not getting autowired. Here are my questions :
1) Using EntityListener for my requirement is good approach or should I use direct "save" operations ?
2) I debugged the EntityListener code and method is not initiated a new Transaction even after adding Requires_new. I can see method is not being called $proxy (spring-proxy). I don't understand why ?
I am not sure about what you are doing in your #PostConstruct init() method... but I suspect you should be configuring this DescriptorEventAdapter using EclipseLink's DescriptorCustomizer. Here is an example:
public class MessageEventListener extends DescriptorEventAdapter implements DescriptorCustomizer {
#Override
public void customize(ClassDescriptor descriptor) {
descriptor.getEventManager().addListener(this);
}
#Override
public void postUpdate(DescriptorEvent event) {
ObjectChangeSet objectChanges = ((UpdateObjectQuery) event.getQuery()).getObjectChangeSet();
//More business logic...
}
}
#Entity
#Customizer(MessageEventListener.class)
public class Message {
#Id private long id;
private String content;
}

how to overload Spring PropertyPlaceholderConfigurer's processProperties() method

My requirement is as given below :
"we have a load a big properties file using Spring and then loop through it .
While looping we have to check the very first column of properties file for some particular values.
As soon as we find those values we have to print that value and continue this printing process till the very end."
For this i finally able to build a code like below :
import java.util.Map;
import java.util.Properties;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SpringPropertiesUtil extends PropertyPlaceholderConfigurer {
private static Map<String, String> propertiesMap;
private static String keyToFind = "myProperty";
// Default as in PropertyPlaceholderConfigurer
private int springSystemPropertiesMode = SYSTEM_PROPERTIES_MODE_FALLBACK;
#Override
public void setSystemPropertiesMode(int systemPropertiesMode) {
super.setSystemPropertiesMode(systemPropertiesMode);
springSystemPropertiesMode = systemPropertiesMode;
}
#Override
protected void processProperties(ConfigurableListableBeanFactory beanFactory, Properties props) throws BeansException {
super.processProperties(beanFactory, props);
for (Object key : props.keySet()) {
String keyStr = key.toString();
if(keyStr.equals(keyToFind)) {
String valueStr = resolvePlaceholder(keyStr, props, springSystemPropertiesMode);
System.out.println(valueStr);
}
}
}
public String getProperty(String name) {
return propertiesMap.get(name).toString();
}
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
((ConfigurableApplicationContext)context).close();
}
}
It is working fine but i am finding a nicer way of doing this if i can overload processProperties() and can pass String keyTofind to this method rather than defining it globally.
Any suggestion is welcome.
Thanks.
Alternatively to your solution, you could implement a custom property source. I believe this is the mechanism Spring has intended for proprietary "source parsing logic".
The subject is not terribly well documented on the web, but this article gives some insight: http://scottfrederick.cfapps.io/blog/2012/05/22/Custom-PropertySource-in-Spring-3.1---Part-1/

Customizing HATEOAS link generation for entities with composite ids

I have configured a RepositoryRestResource on a PageAndSortingRepository that accesses an Entity that includes a composite Id:
#Entity
#IdClass(CustomerId.class)
public class Customer {
#Id BigInteger id;
#Id int startVersion;
...
}
public class CustomerId {
BigInteger id;
int startVersion;
...
}
#RepositoryRestResource(collectionResourceRel = "customers", path = "customers", itemResourceRel = "customers/{id}_{startVersion}")
public interface CustomerRepository extends PagingAndSortingRepository<Customer, CustomerId> {}
When i access the server at "http://<server>/api/customers/1_1" for instance, I get the correct resource back as json, but the href in the _links section for self is the wrong and also the same for any other customer i query: "http://<server>/api/customer/1"
i.e.:
{
"id" : 1,
"startVersion" : 1,
...
"firstname" : "BOB",
"_links" : {
"self" : {
"href" : "http://localhost:9081/reps/api/reps/1" <-- This should be /1_1
}
}
}
I suppose this is because of my composite Id, But I am chuffed as to how i can change this default behaviour.
I've had a look at the ResourceSupport and the ResourceProcessor class but am not sure how much i need to change in order fix this issue.
Can someone who knows spring lend me a hand?
Unfortunately, all Spring Data JPA/Rest versions up to 2.1.0.RELEASE are not able to serve your need out of the box.
The source is buried inside Spring Data Commons/JPA itself. Spring Data JPA supports only Id and EmbeddedId as identifier.
Excerpt JpaPersistentPropertyImpl:
static {
// [...]
annotations = new HashSet<Class<? extends Annotation>>();
annotations.add(Id.class);
annotations.add(EmbeddedId.class);
ID_ANNOTATIONS = annotations;
}
Spring Data Commons doesn't support the notion of combined properties. It treats every property of a class independently from each other.
Of course, you can hack Spring Data Rest. But this is cumbersome, doesn't solve the problem at its heart and reduces the flexibility of the framework.
Here's the hack. This should give you an idea how to tackle your problem.
In your configuration override repositoryExporterHandlerAdapter and return a CustomPersistentEntityResourceAssemblerArgumentResolver.
Additionally, override backendIdConverterRegistry and add CustomBackendIdConverter to the list of known id converter:
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.rest.core.projection.ProxyProjectionFactory;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgumentResolver;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.plugin.core.OrderAwarePluginRegistry;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
#Configuration
#Import(RepositoryRestMvcConfiguration.class)
#EnableSpringDataWebSupport
public class RestConfig extends RepositoryRestMvcConfiguration {
#Autowired(required = false) List<ResourceProcessor<?>> resourceProcessors = Collections.emptyList();
#Autowired
ListableBeanFactory beanFactory;
#Override
#Bean
public PluginRegistry<BackendIdConverter, Class<?>> backendIdConverterRegistry() {
List<BackendIdConverter> converters = new ArrayList<BackendIdConverter>(3);
converters.add(new CustomBackendIdConverter());
converters.add(BackendIdConverter.DefaultIdConverter.INSTANCE);
return OrderAwarePluginRegistry.create(converters);
}
#Bean
public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() {
List<HttpMessageConverter<?>> messageConverters = defaultMessageConverters();
configureHttpMessageConverters(messageConverters);
RepositoryRestHandlerAdapter handlerAdapter = new RepositoryRestHandlerAdapter(defaultMethodArgumentResolvers(),
resourceProcessors);
handlerAdapter.setMessageConverters(messageConverters);
return handlerAdapter;
}
private List<HandlerMethodArgumentResolver> defaultMethodArgumentResolvers()
{
CustomPersistentEntityResourceAssemblerArgumentResolver peraResolver = new CustomPersistentEntityResourceAssemblerArgumentResolver(
repositories(), entityLinks(), config().projectionConfiguration(), new ProxyProjectionFactory(beanFactory));
return Arrays.asList(pageableResolver(), sortResolver(), serverHttpRequestMethodArgumentResolver(),
repoRequestArgumentResolver(), persistentEntityArgumentResolver(),
resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE,
peraResolver, backendIdHandlerMethodArgumentResolver());
}
}
Create CustomBackendIdConverter. This class is responsible for rendering your custom entity ids:
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import java.io.Serializable;
public class CustomBackendIdConverter implements BackendIdConverter {
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
return id;
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
if(entityType.equals(Customer.class)) {
Customer c = (Customer) id;
return c.getId() + "_" +c.getStartVersion();
}
return id.toString();
}
#Override
public boolean supports(Class<?> delimiter) {
return true;
}
}
CustomPersistentEntityResourceAssemblerArgumentResolver in turn should return a CustomPersistentEntityResourceAssembler:
import org.springframework.core.MethodParameter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.projection.ProjectionDefinitions;
import org.springframework.data.rest.core.projection.ProjectionFactory;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.config.PersistentEntityResourceAssemblerArgumentResolver;
import org.springframework.data.rest.webmvc.support.PersistentEntityProjector;
import org.springframework.hateoas.EntityLinks;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
public class CustomPersistentEntityResourceAssemblerArgumentResolver extends PersistentEntityResourceAssemblerArgumentResolver {
private final Repositories repositories;
private final EntityLinks entityLinks;
private final ProjectionDefinitions projectionDefinitions;
private final ProjectionFactory projectionFactory;
public CustomPersistentEntityResourceAssemblerArgumentResolver(Repositories repositories, EntityLinks entityLinks,
ProjectionDefinitions projectionDefinitions, ProjectionFactory projectionFactory) {
super(repositories, entityLinks,projectionDefinitions,projectionFactory);
this.repositories = repositories;
this.entityLinks = entityLinks;
this.projectionDefinitions = projectionDefinitions;
this.projectionFactory = projectionFactory;
}
public boolean supportsParameter(MethodParameter parameter) {
return PersistentEntityResourceAssembler.class.isAssignableFrom(parameter.getParameterType());
}
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String projectionParameter = webRequest.getParameter(projectionDefinitions.getParameterName());
PersistentEntityProjector projector = new PersistentEntityProjector(projectionDefinitions, projectionFactory,
projectionParameter);
return new CustomPersistentEntityResourceAssembler(repositories, entityLinks, projector);
}
}
CustomPersistentEntityResourceAssembler needs to override getSelfLinkFor. As you can see entity.getIdProperty() return either id or startVersion property of your Customer class which in turn gets used to retrieve the real value with the help of a BeanWrapper. Here we are short circuit the whole framework with the use of instanceof operator. Hence your Customer class should implement Serializable for further processing.
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.model.BeanWrapper;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.support.Projector;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.util.Assert;
public class CustomPersistentEntityResourceAssembler extends PersistentEntityResourceAssembler {
private final Repositories repositories;
private final EntityLinks entityLinks;
public CustomPersistentEntityResourceAssembler(Repositories repositories, EntityLinks entityLinks, Projector projector) {
super(repositories, entityLinks, projector);
this.repositories = repositories;
this.entityLinks = entityLinks;
}
public Link getSelfLinkFor(Object instance) {
Assert.notNull(instance, "Domain object must not be null!");
Class<? extends Object> instanceType = instance.getClass();
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(instanceType);
if (entity == null) {
throw new IllegalArgumentException(String.format("Cannot create self link for %s! No persistent entity found!",
instanceType));
}
Object id;
//this is a hack for demonstration purpose. don't do this at home!
if(instance instanceof Customer) {
id = instance;
} else {
BeanWrapper<Object> wrapper = BeanWrapper.create(instance, null);
id = wrapper.getProperty(entity.getIdProperty());
}
Link resourceLink = entityLinks.linkToSingleResource(entity.getType(), id);
return new Link(resourceLink.getHref(), Link.REL_SELF);
}
}
That's it! You should see this URIs:
{
"_embedded" : {
"customers" : [ {
"name" : "test",
"_links" : {
"self" : {
"href" : "http://localhost:8080/demo/customers/1_1"
}
}
} ]
}
}
Imho, if you are working on a green field project I would suggest to ditch IdClass entirely and go with technical simple ids based on Long class. This was tested with Spring Data Rest 2.1.0.RELEASE, Spring data JPA 1.6.0.RELEASE and Spring Framework 4.0.3.RELEASE.
Although not desirable, I have worked around this issue by using an #EmbeddedId instead of a IdClass annotation on my JPA entity.
Like so:
#Entity
public class Customer {
#EmbeddedId
private CustomerId id;
...
}
public class CustomerId {
#Column(...)
BigInteger key;
#Column(...)
int startVersion;
...
}
I now see the correctly generated links 1_1 on my returned entities.
If anyone can still direct me to a solution that does not require I change the representation of my model, It would be highly appreciated. Luckily I had not progressed far in my application development for this to be of serious concern in changing, but I imagine that for others, there would be significant overhead in performing a change like this: (e.g. changing all queries that reference this model in JPQL queries).
I had a similar problem where the composite key scenarios for data rest was not working. #ksokol detailed explanation provided the necessary inputs to solve the issue. changed my pom primarily for data-rest-webmvc and data-jpa as
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-webmvc</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.7.1.RELEASE</version>
</dependency>
which solved all the issues related to composite key and I need not do the customization. Thanks ksokol for the detailed explanation.
First, create a SpringUtil to get bean from spring.
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
#Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
}
Then, implement BackendIdConverter.
import com.alibaba.fastjson.JSON;
import com.example.SpringUtil;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.stereotype.Component;
import javax.persistence.EmbeddedId;
import javax.persistence.Id;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.net.URLEncoder;
#Component
public class CustomBackendIdConverter implements BackendIdConverter {
#Override
public boolean supports(Class<?> delimiter) {
return true;
}
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
if (id == null) {
return null;
}
//first decode url string
if (!id.contains(" ") && id.toUpperCase().contains("%7B")) {
try {
id = URLDecoder.decode(id, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
//deserialize json string to ID object
Object idObject = null;
for (Method method : entityType.getDeclaredMethods()) {
if (method.isAnnotationPresent(Id.class) || method.isAnnotationPresent(EmbeddedId.class)) {
idObject = JSON.parseObject(id, method.getGenericReturnType());
break;
}
}
//get dao class from spring
Object daoClass = null;
try {
daoClass = SpringUtil.getBean(Class.forName("com.example.db.dao." + entityType.getSimpleName() + "DAO"));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//get the entity with given primary key
JpaRepository simpleJpaRepository = (JpaRepository) daoClass;
Object entity = simpleJpaRepository.findOne((Serializable) idObject);
return (Serializable) entity;
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
if (id == null) {
return null;
}
String jsonString = JSON.toJSONString(id);
String encodedString = "";
try {
encodedString = URLEncoder.encode(jsonString, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return encodedString;
}
}
After that. you can do what you want.
There is a sample below.
If the entity has single property pk, you can use
localhost:8080/demo/1 as normal. According to my code, suppose the pk
has annotation "#Id".
If the entity has composed pk, suppose the pk is demoId type, and has
annotation "#EmbeddedId", you can use localhost:8080/demo/{demoId
json} to get/put/delete. And your self link will be the same.
The answers provides above are helpful, but if you need a more generic approach that would be following -
package com.pratham.persistence.config;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.istack.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import javax.persistence.EmbeddedId;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Base64;
import java.util.Optional;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Customization of how composite ids are exposed in URIs.
* The implementation will convert the Ids marked with {#link EmbeddedId} to base64 encoded json
* in order to expose them properly within URI.
*
* #author im-pratham
*/
#Component
#RequiredArgsConstructor
public class EmbeddedBackendIdConverter implements BackendIdConverter {
private final ObjectMapper objectMapper;
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
return getFieldWithEmbeddedAnnotation(entityType)
.map(Field::getType)
.map(ret -> {
try {
String decodedId = new String(Base64.getUrlDecoder().decode(id));
return (Serializable) objectMapper.readValue(decodedId, (Class) ret);
} catch (JsonProcessingException ignored) {
return null;
}
})
.orElse(id);
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
try {
String json = objectMapper.writeValueAsString(id);
return Base64.getUrlEncoder().encodeToString(json.getBytes(UTF_8));
} catch (JsonProcessingException ignored) {
return id.toString();
}
}
#Override
public boolean supports(#NonNull Class<?> entity) {
return isEmbeddedIdAnnotationPresent(entity);
}
private boolean isEmbeddedIdAnnotationPresent(Class<?> entity) {
return getFieldWithEmbeddedAnnotation(entity)
.isPresent();
}
#NotNull
private static Optional<Field> getFieldWithEmbeddedAnnotation(Class<?> entity) {
return Arrays.stream(entity.getDeclaredFields())
.filter(method -> method.isAnnotationPresent(EmbeddedId.class))
.findFirst();
}
}

Resources