once I override the addCustomDataToJsonMap() method of JsonLayout how can I use that to set custom flields
I have added below code
#Setter
public class CustomLoggingConfig extends JsonLayout {
public static final String ADDITIONAL_INFO = "additionalInfo";
private String additionalInfo;
#Override
public void addCustomDataToJsonMap(Map<String, Object> var1, ILoggingEvent var2) {
var1.put(ADDITIONAL_INFO, additionalInfo);
}
}
and my logs are displayed like this:
{
"timestamp" : "2022-08-08 16:48:29.491",
"level" : "ERROR",
"thread" : "main",
"logger" : "jsonLogger",
"message" : "Exception occurred with message: Error in connecting to database",
"context" : "default",
"additionalInfo" : null
}
Question is how can I set additionalInfo?
Related
I'm trying to add a URI to a resource located in a different microservice using OpenFeign and a ResourceAssembler, while preserving the hostname from the original request.
When making a REST request to a HATEOAS resource in another microservice, the resource.getId() method returns a link where the hostname is the Docker container hash instead of the original hostname used to make the request.
Controller
#RestController
#RequestMapping("/bulletins")
public class BulletinController {
// Autowired dependencies
#GetMapping(produces = MediaTypes.HAL_JSON_VALUE)
public ResponseEntity<PagedResources<BulletinResource>> getBulletins(Pageable pageable) {
Page<Bulletin> bulletins = bulletinRepository.findAll(pageable);
return ResponseEntity.ok(pagedResourceAssembler.toResource(bulletins, bulletinResourceAssembler));
}
}
Assembler
#Component
public class BulletinResourceAssembler extends ResourceAssemblerSupport<Bulletin, BulletinResource> {
private final AdministrationService administrationService;
#Autowired
public BulletinResourceAssembler(AdministrationService administrationService) {
super(BulletinController.class, BulletinResource.class);
this.administrationService = administrationService;
}
#Override
public BulletinResource toResource(Bulletin entity) {
Resource<Site> siteRessource = administrationService.getSiteBySiteCode(entity.getSiteCode());
\\ Set other fields ...
bulletinRessource.add(siteRessource.getId().withRel("site"));
return bulletinRessource;
}
}
Feign Client
#FeignClient(name = "${feign.administration.serviceId}", path = "/api")
public interface AdministrationService {
#GetMapping(value = "/sites/{siteCode}")
Resource<Site> getSiteBySiteCode(#PathVariable("siteCode") String siteCode);
}
Bulletin Resource
#Data
public class BulletinResource extends ResourceSupport {
// fields
}
Expected result
curl http://myhost/api/bulletins
{
"_embedded" : {
"bulletinResources" : [ {
"entityId" : 1,
"_links" : {
"self" : {
"href" : "http://myhost/api/bulletins/1"
},
"site" : {
"href" : "http://myhost/api/sites/000"
}
}
} ]
},
[...]
}
Actual result
curl http://myhost/api/bulletins
{
"_embedded" : {
"bulletinResources" : [ {
"entityId" : 1,
"_links" : {
"self" : {
"href" : "http://myhost/api/bulletins/1"
},
"site" : {
"href" : "http://b4dc1a02586c:8080/api/sites/000"
}
}
} ]
},
[...]
}
Notice the site href is b4dc1a02586c, which is the Docker container id.
The solution was to manually define a RequestInterceptor for the FeignClient and manually add the X-Forwarded-Host header, as well as define a ForwardedHeaderFilter bean in the service the request was made to.
Client Side
public class ForwardHostRequestInterceptor implements RequestInterceptor {
private static final String HOST_HEADER = "Host";
private static final String X_FORWARDED_HOST = "X-Forwarded-Host";
#Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
if (requestAttributes == null) {
return;
}
HttpServletRequest request = requestAttributes.getRequest();
String host = request.getHeader(X_FORWARDED_HOST);
if (host == null) {
host = request.getHeader(HOST_HEADER);
}
requestTemplate.header(X_FORWARDED_HOST, host);
}
}
Producer side
The producer side also required modification as per the discussion on
https://github.com/spring-projects/spring-hateoas/issues/862
which refers to the following documentation
https://docs.spring.io/spring-hateoas/docs/current-SNAPSHOT/reference/html/#server.link-builder.forwarded-headers
which states to add the following bean in order to use forward headers.
#Bean
ForwardedHeaderFilter forwardedHeaderFilter() {
return new ForwardedHeaderFilter();
}
I have a simple UserRepository which exposed using Spring Data REST.
Here is the User entity class:
#Document(collection = User.COLLECTION_NAME)
#Setter
#Getter
public class User extends Entity {
public static final String COLLECTION_NAME = "users";
private String name;
private String email;
private String password;
private Set<UserRole> roles = new HashSet<>(0);
}
I've created a UserProjection class which looks the following way:
#JsonInclude(JsonInclude.Include.NON_NULL)
#Projection(types = User.class)
public interface UserProjection {
String getId();
String getName();
String getEmail();
Set<UserRole> getRoles();
}
Here is the repository class:
#RepositoryRestResource(collectionResourceRel = User.COLLECTION_NAME, path = RestPath.Users.ROOT,
excerptProjection = UserProjection.class)
public interface RestUserRepository extends MongoRepository<User, String> {
// Not exported operations
#RestResource(exported = false)
#Override
<S extends User> S insert(S entity);
#RestResource(exported = false)
#Override
<S extends User> S save(S entity);
#RestResource(exported = false)
#Override
<S extends User> List<S> save(Iterable<S> entites);
}
I've also specified user projection in configuration to make sure it will be used.
config.getProjectionConfiguration().addProjection(UserProjection.class, User.class);
So, when I do GET on /users path, I get the following response (projection is applied):
{
"_embedded" : {
"users" : [ {
"name" : "Yuriy Yunikov",
"id" : "5812193156aee116256a33d4",
"roles" : [ "USER", "ADMIN" ],
"email" : "yyunikov#gmail.com",
"points" : 0,
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8080/users/5812193156aee116256a33d4"
},
"user" : {
"href" : "http://127.0.0.1:8080/users/5812193156aee116256a33d4{?projection}",
"templated" : true
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8080/users"
},
"profile" : {
"href" : "http://127.0.0.1:8080/profile/users"
}
},
"page" : {
"size" : 20,
"totalElements" : 1,
"totalPages" : 1,
"number" : 0
}
}
However, when I try to make a GET call for single resource, e.g. /users/5812193156aee116256a33d4, I get the following response:
{
"name" : "Yuriy Yunikov",
"email" : "yyunikov#gmail.com",
"password" : "123456",
"roles" : [ "USER", "ADMIN" ],
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8080/users/5812193156aee116256a33d4"
},
"user" : {
"href" : "http://127.0.0.1:8080/users/5812193156aee116256a33d4{?projection}",
"templated" : true
}
}
}
As you may see, the password field is getting returned and projection is not applied. I know there is #JsonIgnore annotation which can be used to hide sensitive data of resource. However, my User object is located in different application module which does not know about API or JSON representation, so it does not make sense to mark fields with #JsonIgnore annotation there.
I've seen a post by #Oliver Gierke here about why excerpt projections are not applied to single resource automatically. However, it's still very inconvenient in my case and I would like to return the same UserProjection when I get a single resource. Is it somehow possible to do it without creating a custom controller or marking fields with #JsonIgnore?
I was able to create a ResourceProcessor class which applies projections on any resource as suggested in DATAREST-428. It works the following way: if projection parameter is specified in URL - the specified projection will be applied, if not - projection with name default will be returned, applied first found projection will be applied. Also, I had to add custom ProjectingResource which ignores the links, otherwise there are two _links keys in the returning JSON.
/**
* Projecting resource used for {#link ProjectingProcessor}. Does not include empty links in JSON, otherwise two
* _links keys are present in returning JSON.
*
* #param <T>
*/
#JsonInclude(JsonInclude.Include.NON_EMPTY)
class ProjectingResource<T> extends Resource<T> {
ProjectingResource(final T content) {
super(content);
}
}
/**
* Resource processor for all resources which applies projection for single resource. By default, projections
* are not
* applied when working with single resource, e.g. http://127.0.0.1:8080/users/580793f642d54436e921f6ca. See
* related issue DATAREST-428
*/
#Component
public class ProjectingProcessor implements ResourceProcessor<Resource<Object>> {
private static final String PROJECTION_PARAMETER = "projection";
private final ProjectionFactory projectionFactory;
private final RepositoryRestConfiguration repositoryRestConfiguration;
private final HttpServletRequest request;
public ProjectingProcessor(#Autowired final RepositoryRestConfiguration repositoryRestConfiguration,
#Autowired final ProjectionFactory projectionFactory,
#Autowired final HttpServletRequest request) {
this.repositoryRestConfiguration = repositoryRestConfiguration;
this.projectionFactory = projectionFactory;
this.request = request;
}
#Override
public Resource<Object> process(final Resource<Object> resource) {
if (AopUtils.isAopProxy(resource.getContent())) {
return resource;
}
final Optional<Class<?>> projectionType = findProjectionType(resource.getContent());
if (projectionType.isPresent()) {
final Object projection = projectionFactory.createProjection(projectionType.get(), resource
.getContent());
return new ProjectingResource<>(projection);
}
return resource;
}
private Optional<Class<?>> findProjectionType(final Object content) {
final String projectionParameter = request.getParameter(PROJECTION_PARAMETER);
final Map<String, Class<?>> projectionsForType = repositoryRestConfiguration.getProjectionConfiguration()
.getProjectionsFor(content.getClass());
if (!projectionsForType.isEmpty()) {
if (!StringUtils.isEmpty(projectionParameter)) {
// projection parameter specified
final Class<?> projectionClass = projectionsForType.get(projectionParameter);
if (projectionClass != null) {
return Optional.of(projectionClass);
}
} else if (projectionsForType.containsKey(ProjectionName.DEFAULT)) {
// default projection exists
return Optional.of(projectionsForType.get(ProjectionName.DEFAULT));
}
// no projection parameter specified
return Optional.of(projectionsForType.values().iterator().next());
}
return Optional.empty();
}
}
I was looking at something similar recently and ended up going round in circles when trying to approach it from the Spring Data /Jackson side of things.
An alternative, and very simple solution, then is to approach it from a different angle and ensure the Projection parameter in the HTTP request is always present. This can be done by using a Servlet Filter to modify the parameters of the incoming request.
This would look something like the below:
public class ProjectionResolverFilter extends GenericFilterBean {
private static final String REQUEST_PARAM_PROJECTION_KEY = "projection";
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
if (shouldApply(request)) {
chain.doFilter(new ResourceRequestWrapper(request), res);
} else {
chain.doFilter(req, res);
}
}
/**
*
* #param request
* #return True if this filter should be applied for this request, otherwise
* false.
*/
protected boolean shouldApply(HttpServletRequest request) {
return request.getServletPath().matches("some-path");
}
/**
* HttpServletRequestWrapper implementation which allows us to wrap and
* modify the incoming request.
*
*/
public class ResourceRequestWrapper extends HttpServletRequestWrapper {
public ResourceRequestWrapper(HttpServletRequest request) {
super(request);
}
#Override
public String getParameter(final String name) {
if (name.equals(REQUEST_PARAM_PROJECTION_KEY)) {
return "nameOfDefaultProjection";
}
return super.getParameter(name);
}
}
}
Following these tutorials 1, 2, I successfully made a RESTful Spring application combined with JPA. Currently, I have 3 drivers in my Driver repository.
My problem is when I go on localhost:8080/driver, it says this:
{
"_embedded" : {
"driver" : [ {
"_links" : {
"self" : {
"href" : "http://localhost:8080/driver/1"
},
"driver" : {
"href" : "http://localhost:8080/driver/1"
}
}
}, {
"_links" : {
"self" : {
"href" : "http://localhost:8080/driver/2"
},
"driver" : {
"href" : "http://localhost:8080/driver/2"
}
}
}, {
"_links" : {
"self" : {
"href" : "http://localhost:8080/driver/3"
},
"driver" : {
"href" : "http://localhost:8080/driver/3"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/driver"
},
"profile" : {
"href" : "http://localhost:8080/profile/driver"
},
"search" : {
"href" : "http://localhost:8080/driver/search"
}
},
"page" : {
"size" : 20,
"totalElements" : 3,
"totalPages" : 1,
"number" : 0
}
}
and when I go onto a particular's drivers page, something like localhost:8080/driver/1, it says this:
{
"_links" : {
"self" : {
"href" : "http://localhost:8080/driver/1"
},
"driver" : {
"href" : "http://localhost:8080/driver/1"
}
}
}
In my Driver Entity Class, I have fields like firstName, lastName, telephone and stuff like that. My question is: is there a way I can display on it either localhost:8080/driver or localhost:8080/driver/1? So that it looks similar to this:
{
"firstName" : "Frodo",
"lastName" : "Baggins",
"_links" : {
"self" : {
"href" : "http://localhost:8080/people/1"
}
}
}
I know the fields are properly saved in the database, because I can successfully search for them, but I haven't yet found an example of how to display them, except POSTing with curl.
Thanks in advance to anyone who can help!
Edit: This is my Starter class:
#SpringBootApplication
public class StartServer implements ApplicationListener<ContextRefreshedEvent> {
private static final Logger log = LoggerFactory.getLogger(StartServer.class);
public static void main(String[] args) {
SpringApplication.run(StartServer.class, args);
}
#Override
public void onApplicationEvent(final ContextRefreshedEvent event) {
}
#Bean
public CommandLineRunner demo(DriverRepository repository) {
return (args) -> {
repository.save(new Driver("Jack", "Bauer"));
repository.save(new Driver("JackY", "Aasd"));
repository.save(new Driver("JackMe", "Commou"));
// fetch all customers
log.info("Drivers found with findAll():");
log.info("-------------------------------");
for (Driver driver : repository.findAll()) {
log.info(driver.toString());
}
log.info("");
Driver driver = repository.findOne(1L);
log.info("Customer found with findOne(1L):");
log.info("--------------------------------");
log.info(driver.toString());
log.info("");
// fetch customers by last name
log.info("Driver found with findByLastName('Bauer'):");
log.info("--------------------------------------------");
for (Driver bauer : repository.findByLastName("Bauer")) {
log.info(bauer.toString());
}
log.info("");
};
}
}
And my DriverRepository class:
#RepositoryRestResource(collectionResourceRel = "driver", path = "driver")
public interface DriverRepository extends PagingAndSortingRepository<Driver, Long> {
List<Driver> findByLastName(#Param("name") String name);
}
I do not have a Controller Class for /driver, because the #RepositoryRestResource(collectionResourceRel = "driver", path = "driver") took care of it for me.
My Driver Entity class:
#Entity
public class Driver {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private long id;
String firstName;
String lastName;
protected Driver() {
}
public Driver(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
#Override
public String toString() {
return String.format( "Customer[id=%d, firstName='%s', lastName='%s']",
id, firstName, lastName);
}
}
I'm having duplicate results on a collection with this simple model: an entity Module and an entity Page. A Module has a set of pages, and a Page belongs to the module.
This is set up with Spring Boot with Spring Data JPA and Spring Data Rest.
The full code is accessible on GitHub
Entities
Here's the code for the entities. Most setters removed for brevity:
Module.java
#Entity
#Table(name = "dt_module")
public class Module {
private Long id;
private String label;
private String displayName;
private Set<Page> pages;
#Id
public Long getId() {
return id;
}
public String getLabel() {
return label;
}
public String getDisplayName() {
return displayName;
}
#OneToMany(mappedBy = "module")
public Set<Page> getPages() {
return pages;
}
public void addPage(Page page) {
if (pages == null) {
pages = new HashSet<>();
}
pages.add(page);
if (page.getModule() != this) {
page.setModule(this);
}
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Module module = (Module) o;
return Objects.equals(label, module.label) && Objects.equals(displayName, module.displayName);
}
#Override
public int hashCode() {
return Objects.hash(label, displayName);
}
}
Page.java
#Entity
#Table(name = "dt_page")
public class Page {
private Long id;
private String name;
private String action;
private String description;
private Module module;
#Id
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getAction() {
return action;
}
public String getDescription() {
return description;
}
#ManyToOne
public Module getModule() {
return module;
}
public void setModule(Module module) {
this.module = module;
this.module.addPage(this);
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Page page = (Page) o;
return Objects.equals(name, page.name) &&
Objects.equals(action, page.action) &&
Objects.equals(description, page.description) &&
Objects.equals(module, page.module);
}
#Override
public int hashCode() {
return Objects.hash(name, action, description, module);
}
}
Repositories
Now the code for the Spring repositories, which is fairly simple:
ModuleRepository.java
#RepositoryRestResource(collectionResourceRel = "module", path = "module")
public interface ModuleRepository extends PagingAndSortingRepository<Module, Long> {
}
PageRepository.java
#RepositoryRestResource(collectionResourceRel = "page", path = "page")
public interface PageRepository extends PagingAndSortingRepository<Page, Long> {
}
Config
The configuration comes from 2 files:
Application.java
#EnableJpaRepositories
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
application.properties
spring.jpa.database = H2
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.generate-ddl=false
spring.jpa.hibernate.ddl-auto=validate
spring.datasource.initialize=true
spring.datasource.url=jdbc:h2:mem:demo;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.data.rest.basePath=/api
Database
Finally the db schema and some test data:
schema.sql
drop table if exists dt_page;
drop table if exists dt_module;
create table DT_MODULE (
id IDENTITY primary key,
label varchar(30) not NULL,
display_name varchar(40) not NULL
);
create table DT_PAGE (
id IDENTITY primary key,
name varchar(50) not null,
action varchar(50) not null,
description varchar(255),
module_id bigint not null REFERENCES dt_module(id)
);
data.sql
INSERT INTO DT_MODULE (label, display_name) VALUES ('mod1', 'Module 1'), ('mod2', 'Module 2'), ('mod3', 'Module 3');
INSERT INTO DT_PAGE (name, action, description, module_id) VALUES ('page1', 'action1', 'desc1', 1);
That's about it. Now, I run thus from the command line to start the application: mvn spring-boot:run. After the application starts, I can query it's main endpoint like this:
Get API
$ curl http://localhost:8080/api
Response
{
"_links" : {
"page" : {
"href" : "http://localhost:8080/api/page{?page,size,sort}",
"templated" : true
},
"module" : {
"href" : "http://localhost:8080/api/module{?page,size,sort}",
"templated" : true
},
"profile" : {
"href" : "http://localhost:8080/api/alps"
}
}
}
Get all modules
curl http://localhost:8080/api/module
Response
{
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module"
}
},
"_embedded" : {
"module" : [ {
"label" : "mod1",
"displayName" : "Module 1",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module/1"
},
"pages" : {
"href" : "http://localhost:8080/api/module/1/pages"
}
}
}, {
"label" : "mod2",
"displayName" : "Module 2",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module/2"
},
"pages" : {
"href" : "http://localhost:8080/api/module/2/pages"
}
}
}, {
"label" : "mod3",
"displayName" : "Module 3",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module/3"
},
"pages" : {
"href" : "http://localhost:8080/api/module/3/pages"
}
}
} ]
},
"page" : {
"size" : 20,
"totalElements" : 3,
"totalPages" : 1,
"number" : 0
}
}
Get all pages for one module
curl http://localhost:8080/api/module/1/pages
Response
{
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module/1/pages"
}
},
"_embedded" : {
"page" : [ {
"name" : "page1",
"action" : "action1",
"description" : "desc1",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/page/1"
},
"module" : {
"href" : "http://localhost:8080/api/page/1/module"
}
}
}, {
"name" : "page1",
"action" : "action1",
"description" : "desc1",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/page/1"
},
"module" : {
"href" : "http://localhost:8080/api/page/1/module"
}
}
} ]
}
}
So as you can see, I'm getting the same page twice here. What's going on?
Bonus question: Why this works?
I was cleaning the code to submit this question, and in order to make it more compact, I moved the JPA Annotations on the Page entity to field level, like this:
Page.java
#Entity
#Table(name = "dt_page")
public class Page {
#Id
private Long id;
private String name;
private String action;
private String description;
#ManyToOne
private Module module;
...
All the rest of the class remains the same. This can be seen on the same github repo on branch field-level.
As it turns out, executing the same request with that change to the API will render the expected result (after starting the server the same way I did before):
Get all pages for one module
curl http://localhost:8080/api/module/1/pages
Response
{
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/module/1/pages"
}
},
"_embedded" : {
"page" : [ {
"name" : "page1",
"action" : "action1",
"description" : "desc1",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/page/1"
},
"module" : {
"href" : "http://localhost:8080/api/page/1/module"
}
}
} ]
}
}
This is causing your issue (Page Entity):
public void setModule(Module module) {
this.module = module;
this.module.addPage(this); //this line right here
}
Hibernate uses your setters to initialize the entity because you put the JPA annotations on getters.
Initialization sequence that causes the issue:
Module object created
Set Module properties (pages set is initialized)
Page object created
Add the created Page to Module.pages
Set Page properties
setModule is called on the Page object and this adds (addPage) the current Page to Module.pages the second time
You can put the JPA annotations on the fields and it will work, because setters won't be called during initialization (bonus question).
I had this issue and I just changed fetch=FetchType.EAGER to fetch=FetchType.LAZY
This solved my problem!
Let's say I have two repositories:
#RepositoryRestResource(collectionResourceRel = "person", path = "person")
public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {
List<Person> findByLastName(#Param("name") String name);
}
and
#RepositoryRestResource(collectionResourceRel = "person1", path = "person1")
public interface PersonRepository1 extends PagingAndSortingRepository<Person1, Long> {
List<Person1> findByLastName(#Param("name") String name);
}
with one regular controller:
#Controller
public class HelloController {
#RequestMapping("/hello")
#ResponseBody
public HttpEntity<Hello> hello(#RequestParam(value = "name", required = false, defaultValue = "World") String name) {
Hello hello = new Hello(String.format("Hello, %s!", name));
hello.add(linkTo(methodOn(HelloController.class).hello(name)).withSelfRel());
return new ResponseEntity<>(hello, HttpStatus.OK);
}
}
Now, a response for http://localhost:8080/ is:
{
"_links" : {
"person" : {
"href" : "http://localhost:8080/person{?page,size,sort}",
"templated" : true
},
"person1" : {
"href" : "http://localhost:8080/person1{?page,size,sort}",
"templated" : true
}
}
}
but I want to get something like this:
{
"_links" : {
"person" : {
"href" : "http://localhost:8080/person{?page,size,sort}",
"templated" : true
},
"person1" : {
"href" : "http://localhost:8080/person1{?page,size,sort}",
"templated" : true
},
"hello" : {
"href" : "http://localhost:8080/hello?name=World"
}
}
}
#Component
public class HelloResourceProcessor implements ResourceProcessor<RepositoryLinksResource> {
#Override
public RepositoryLinksResource process(RepositoryLinksResource resource) {
resource.add(ControllerLinkBuilder.linkTo(HelloController.class).withRel("hello"));
return resource;
}
}
based on
http://docs.spring.io/spring-data/rest/docs/current/reference/html/#customizing-sdr.customizing-json-output
How to add links to root resource in Spring Data REST?
You need to have a ResourceProcessory for your Person resource registered as a Bean. see https://stackoverflow.com/a/24660635/442773