Spring data Elasticsearch : Reinitialise elasticsearch hosts/cluster_password programmatically - java

TL;DR : How to switch elastic search hosts at runtime using spring-data-elasticsearch?
We have a spring boot application (v : 2.5.2) using spring data elasticsearch (v: 4.2.2).
I am using java configurations to initialize ES connection as mentioned in High Level Rest Client section of spring's documentation.
We have a requirement where we need to change the elasticsearch hosts at runtime and not manually restart our servers.
I tried restarting spring IOC on hostname config change using something similar to spring actuator's RestartEndpoint. But this seems to work only in spring's embedded tomcat and not with the external tomcat that we use.
Is there an alternate way to do this?
[This is required in case there is a disaster and we need to switch to backup]

Nothing out of the box.
One idea (I did not try it out): You'd need to create your own implementation of the ElasticsearchOperations interface, lets call it MyOperations and return that from the AbstractElasticsearchConfiguration#elasticsearchOperations() method which your configuration inherits. In this MyOperations implementation (which is a singleton Spring bean), you'd create a delegate ElasticsearchOperations with the same code like in the configuration class that is connected to normal cluster. Every method in your implementation then delegates to the corresponding method of the delegate.
In the failure case, your implementation would create a new delegate that now connects to the secondary cluster and replace the first one with it. Or create both and implement a toggle feature to switch between the two.

Expanding on the answer provided by P.J.Meisch,
Create a custom implementation for ElasticsearchOperations
public class CustomElasticsearchOperations implements ElasticsearchOperations {
private ElasticsearchOperations delegateElasticSearchOp;
/**
* Main Constructor to create the instance using existing elasticsearchConverter bean
* #param elasticsearchConverter : ElasticsearchConverter
*/
public CustomElasticsearchOperations(ElasticsearchConverter elasticsearchConverter) {
updateElasticsearchOperationDelegate(elasticsearchConverter);
}
private ElasticsearchOperations createNewElasticsearchOperationDelegate(ElasticsearchConverter elasticsearchConverter){
return new ElasticsearchRestTemplate(ElasticsearchRestHighLevelClientProvider.INSTANCE.elasticsearchClient(),elasticsearchConverter);
}
/**
* Method to update the delegate ElasticsearchRestTemplate to register elasticsearch configurations
* #param elasticsearchConverter : Default spring lib provided ElasticsearchConverter instance
*/
public void updateElasticsearchOperationDelegate(ElasticsearchConverter elasticsearchConverter){
this.delegateElasticSearchOp = createNewElasticsearchOperationDelegate(elasticsearchConverter);
}
/*
* ElasticsearchOperations method implementation using Delegate ElasticsearchRestTemplatea
*/
#Override
public IndexOperations indexOps(Class<?> aClass) {
return delegateElasticSearchOp.indexOps(aClass);
}
#Override
public IndexOperations indexOps(IndexCoordinates indexCoordinates) {
return delegateElasticSearchOp.indexOps(indexCoordinates);
}
#Override
public ClusterOperations cluster() {
return delegateElasticSearchOp.cluster();
}
#Override
public ElasticsearchConverter getElasticsearchConverter() {
return delegateElasticSearchOp.getElasticsearchConverter();
}
// And so on... delegate the impl of overriden method to existing ElasticsearchRestTemplate
}
Where ElasticsearchRestHighLevelClientProvider is a class that holds the logic to create object of RestHighLevelClient that holds the values for current cluster configurations
// This is a sample from spring documentation. Create your own rest client with your own configs
public enum ElasticsearchRestHighLevelClientProvider{
INSTANCE;
// below is a sample, change it according to your requirements.
public RestHighLevelClient elasticsearchClient() {
String hosts [] = getLatestConfigs().getHosts();
final ClientConfiguration clientConfiguration =
ClientConfiguration.builder()
.connectedTo(hosts)
.build();
return RestClients.create(clientConfiguration).rest();
}
// your code for fetching cluster configs..
}
And finally, register the bean of CustomElasticsearchOperations
#Configuration
public class ElasticSearchAutoConfigurations extends ElasticsearchConfigurationSupport {
#Bean(
name = {"elasticsearchOperations", "elasticsearchTemplate"}
)
public ElasticsearchOperations elasticsearchOperations(ElasticsearchConverter elasticsearchConverter) {
return new CustomElasticsearchOperations(elasticsearchConverter);
}
}
Now when your configurations change you can Autowire the instances of ElasticsearchOperations and ElasticsearchConverter beans, and invoke ElasticsearchOperations#updateElasticsearchOperationDelegate(elasticsearchConverter)
This will replace the instance of ElasticsearchRestTemplate within CustomElasticsearchOperations singleton bean with the new configs, and new requests will go to the cluster you want to switch to.
Note that
If you have autowired ElasticsearchRestTemplate to make non spring repository based calls to elasticsearch, change the autowired candidate to parent interface ElasticsearchOperations as ElasticsearchRestTemplate's instance will not be present in the application context
ElasticsearchConverter is already created within ElasticsearchConfigurationSupport class. So no need to create this bean again

Related

Why is caching not working on wrapped reactive Methods [duplicate]

Spring cache is not working when calling cached method from another method of the same bean.
Here is an example to explain my problem in clear way.
Configuration:
<cache:annotation-driven cache-manager="myCacheManager" />
<bean id="myCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="myCache" />
</bean>
<!-- Ehcache library setup -->
<bean id="myCache"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:shared="true">
<property name="configLocation" value="classpath:ehcache.xml"></property>
</bean>
<cache name="employeeData" maxElementsInMemory="100"/>
Cached service :
#Named("aService")
public class AService {
#Cacheable("employeeData")
public List<EmployeeData> getEmployeeData(Date date){
..println("Cache is not being used");
...
}
public List<EmployeeEnrichedData> getEmployeeEnrichedData(Date date){
List<EmployeeData> employeeData = getEmployeeData(date);
...
}
}
Result :
aService.getEmployeeData(someDate);
output: Cache is not being used
aService.getEmployeeData(someDate);
output:
aService.getEmployeeEnrichedData(someDate);
output: Cache is not being used
The getEmployeeData method call uses cache employeeData in the second call as expected. But when the getEmployeeData method is called within the AService class (in getEmployeeEnrichedData), Cache is not being used.
Is this how spring cache works or am i missing something ?
I believe this is how it works. From what I remember reading, there is a proxy class generated that intercepts all requests and responds with the cached value, but 'internal' calls within the same class will not get the cached value.
From https://code.google.com/p/ehcache-spring-annotations/wiki/UsingCacheable
Only external method calls coming in through the proxy are
intercepted. This means that self-invocation, in effect, a method
within the target object calling another method of the target object,
will not lead to an actual cache interception at runtime even if the
invoked method is marked with #Cacheable.
Since Spring 4.3 the problem could be solved using self-autowiring over #Resource annotation:
#Component
#CacheConfig(cacheNames = "SphereClientFactoryCache")
public class CacheableSphereClientFactoryImpl implements SphereClientFactory {
/**
* 1. Self-autowired reference to proxified bean of this class.
*/
#Resource
private SphereClientFactory self;
#Override
#Cacheable(sync = true)
public SphereClient createSphereClient(#Nonnull TenantConfig tenantConfig) {
// 2. call cached method using self-bean
return self.createSphereClient(tenantConfig.getSphereClientConfig());
}
#Override
#Cacheable(sync = true)
public SphereClient createSphereClient(#Nonnull SphereClientConfig clientConfig) {
return CtpClientConfigurationUtils.createSphereClient(clientConfig);
}
}
The example below is what I use to hit the proxy from within the same bean, it is similar to #mario-eis' solution, but I find it a bit more readable (maybe it's not:-). Anyway, I like to keep the #Cacheable annotations at the service level:
#Service
#Transactional(readOnly=true)
public class SettingServiceImpl implements SettingService {
#Inject
private SettingRepository settingRepository;
#Inject
private ApplicationContext applicationContext;
#Override
#Cacheable("settingsCache")
public String findValue(String name) {
Setting setting = settingRepository.findOne(name);
if(setting == null){
return null;
}
return setting.getValue();
}
#Override
public Boolean findBoolean(String name) {
String value = getSpringProxy().findValue(name);
if (value == null) {
return null;
}
return Boolean.valueOf(value);
}
/**
* Use proxy to hit cache
*/
private SettingService getSpringProxy() {
return applicationContext.getBean(SettingService.class);
}
...
See also Starting new transaction in Spring bean
Here is what I do for small projects with only marginal usage of method calls within the same class. In-code documentation is strongly advidsed, as it may look strage to colleagues. But its easy to test, simple, quick to achieve and spares me the full blown AspectJ instrumentation. However, for more heavy usage I'd advice the AspectJ solution.
#Service
#Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
class AService {
private final AService _aService;
#Autowired
public AService(AService aService) {
_aService = aService;
}
#Cacheable("employeeData")
public List<EmployeeData> getEmployeeData(Date date){
..println("Cache is not being used");
...
}
public List<EmployeeEnrichedData> getEmployeeEnrichedData(Date date){
List<EmployeeData> employeeData = _aService.getEmployeeData(date);
...
}
}
If you call a cached method from same bean it will be treated as a private method and annotations will be ignored
Yes, the caching will not happen because of the reasons that were already mentioned in the other posts. However I would solve the problem by putting that method to its own class (service in this case). With that your code will be easier to maintain/test and understand.
#Service // or #Named("aService")
public class AService {
#Autowired //or how you inject your dependencies
private EmployeeService employeeService;
public List<EmployeeData> getEmployeeData(Date date){
employeeService.getEmployeeData(date);
}
public List<EmployeeEnrichedData> getEmployeeEnrichedData(Date date){
List<EmployeeData> employeeData = getEmployeeData(date);
...
}
}
#Service // or #Named("employeeService")
public class EmployeeService {
#Cacheable("employeeData")
public List<EmployeeData> getEmployeeData(Date date){
println("This will be called only once for same date");
...
}
}
In my Case I add variable :
#Autowired
private AService aService;
So I call the getEmployeeData method by using the aService
#Named("aService")
public class AService {
#Cacheable("employeeData")
public List<EmployeeData> getEmployeeData(Date date){
..println("Cache is not being used");
...
}
public List<EmployeeEnrichedData> getEmployeeEnrichedData(Date date){
List<EmployeeData> employeeData = aService.getEmployeeData(date);
...
}
}
It will use the cache in this case.
Better approach should be creating another service like ACachingService and call ACachingService.cachingMethod() instead of self Autowiring ( or any other approach trying to self inject). This way you do not fall into Circular dependency, which may be resulted in warning/error when upgrade to newer Spring ( Spring 2.6.6 in my case ) :
ERROR o.s.boot.SpringApplication - Application run failed
org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'webSecurityConfig':
Requested bean is currently in creation: Is there an unresolvable circular reference?
We looked at all the solutions here and decided to use a separate class for the cached methods because Spring 5 doesn't like circular dependencies.
Use static weaving to create proxy around your bean. In this case even 'internal' methods would work correctly
I use internal inner bean (FactoryInternalCache) with real cache for this purpose:
#Component
public class CacheableClientFactoryImpl implements ClientFactory {
private final FactoryInternalCache factoryInternalCache;
#Autowired
public CacheableClientFactoryImpl(#Nonnull FactoryInternalCache factoryInternalCache) {
this.factoryInternalCache = factoryInternalCache;
}
/**
* Returns cached client instance from cache.
*/
#Override
public Client createClient(#Nonnull AggregatedConfig aggregateConfig) {
return factoryInternalCache.createClient(aggregateConfig.getClientConfig());
}
/**
* Returns cached client instance from cache.
*/
#Override
public Client createClient(#Nonnull ClientConfig clientConfig) {
return factoryInternalCache.createClient(clientConfig);
}
/**
* Spring caching feature works over AOP proxies, thus internal calls to cached methods don't work. That's why
* this internal bean is created: it "proxifies" overloaded {#code #createClient(...)} methods
* to real AOP proxified cacheable bean method {#link #createClient}.
*
* #see Spring Cache #Cacheable - not working while calling from another method of the same bean
* #see Spring cache #Cacheable method ignored when called from within the same class
*/
#EnableCaching
#CacheConfig(cacheNames = "ClientFactoryCache")
static class FactoryInternalCache {
#Cacheable(sync = true)
public Client createClient(#Nonnull ClientConfig clientConfig) {
return ClientCreationUtils.createClient(clientConfig);
}
}
}
I would like to share what I think is the easiest approach:
Autowire the controller and use to call the method it instead of using the class context this.
The updated code would look like:
#Controller
public class TestController {
#Autowired TestController self;
#RequestMapping("/test")
public String testView(){
self.expensiveMethod();
return "test";
}
#Cacheable("ones")
public void expensiveMethod(){
System.out.println("Cache is not being used");
}
}
The default advice mode for processing caching annotation is “proxy”. At the startup of an application, all the caching annotations like #Caching, #Cacheable, #CacheEvict etc. are scanned and a target proxy class is generated for all of these classes. The proxy allows for intercepting the calls to these cacheable methods, which adds the caching advice/behavior.
So when we invoke the cacheable methods from the same class, as shown below, calls from the clients don’t get intercepted in a way that allows for caching advice to be added to them. Hence, every single time there is an unexpected cache miss.
Solution: Invoke the Cacheable methods from a different bean to use proxy class with caching advice.

How to create a dynamic proxy of a already proxyed class in spring

I'm relativly new to spring/spring boot.
At the moment I'm using a spring boot rest application which provides an FeignClient to be included in other projects. Now, I want those FeignClients be wrapped by a CircuitBreaker.
The best solution I came up with, is that I dynamically create a proxy which includes the CircuitBreaker implementation which itself calls the created FeignClient.
So let's assume I have the following interface which describes the RestController:
#RequestMapping("/")
public interface MyWebService {
#GetMapping("name")
public String getName();
}
Now, I have the interface for the FeignClient:
#FeignClient("app")
public interface WebServiceClient extends WebService {
}
So.. My goal would be to achieve something like I have another annotation e. g. #WithCircuitBreaker which I then will be scanned for and dynamically create a proxy bean which will be injected instead of the FeignClient bean.
At the moment my code looks like this:
#FeignClient("app")
#WithCircuitBreaker
public interface WebServiceClient extends WebService {
}
As far as I know, I can now create a #Configuration Class which will look like this:
#Configuration
public class WithCircuitBreakerConfiguration implements ImportAware {
private AnnotationMetadata annotationMetadata;
private AnnotationAttributes annotationAttributes;
#Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
this.annotationMetadata = importMetadata;
Map<String, Object> annotatedClasses = importMetadata.getAnnotationAttributes(WithCircuitBreaker.class.getName());
this.annotationAttributes = AnnotationAttributes.fromMap(annotatedClasses);
}
What else to import to create the proxy and inject it?
}
Now I'm at the point, which I don't know how to continue. How to dynamically create a proxy class which does something like this:
public class PorxyedWebService {
private WebService feignClientProxy;
#Autowired
public ProxyedWebService(WebService feignClientProxy) {
this.feignClientProxy = feignClientProxy;
}
public String getName() {
...
<some circuitbreaker stuff>
....
return this.feignClientProxy.getName();
}
}
and then return this proxy instead of the proxy generated from Feign as soon as someone autowires the WebService interface.
I am not a Spring user, but I do know that Spring does not create proxies recursively if e.g. multiple Spring AOP aspects are applied to the same object. Instead, additional interceptors (or advices in AOP language) are registered upon the same proxy. I think you want to use that infrastructure in order to achieve whatever your objective is.
You can just use the resilience4j Spring Boot2 starter.
You can combine the #CircuitBreaker annotation with the #FeignClient annotation at interface level.
You can then use it as follows:
#FeignClient(name = DUMMY_FEIGN_CLIENT_NAME)
#CircuitBreaker(name = DUMMY_FEIGN_CLIENT_NAME)
public interface DummyFeignClient {
String DUMMY_FEIGN_CLIENT_NAME = "dummyFeignClient";
#GetMapping(path = "/api/{param}")
void doSomething(#PathVariable(name = "param") String param);
}

How to cache data during application startup in Spring boot application

I have a Spring boot Application connecting to SQL Server Database. I need some help in using caching in my application. I have a table for CodeCategory which has a list of codes for Many codes. This table will be loaded every month and data changes only once in a month.
I want to cache this entire table when the Application starts. In any subsequent calls to the table should get value from this cache instead of calling the Database.
For Example,
List<CodeCategory> findAll();
I want to cache the above DB query value during application startup. If there is a DB call like List<CodeCategory> findByCodeValue(String code) should fetch the code result from the already Cached data instead of calling the Database.
Please let me know how this can be achieved using spring boot and ehcache.
As pointed out, It takes some time for ehcache to setup and it is not working completely with #PostConstruct. In that case make use of ApplicationStartedEvent to load the cache.
GitHub Repo: spring-ehcache-demo
#Service
class CodeCategoryService{
#EventListener(classes = ApplicationStartedEvent.class )
public void listenToStart(ApplicationStartedEvent event) {
this.repo.findByCodeValue("100");
}
}
interface CodeCategoryRepository extends JpaRepository<CodeCategory, Long>{
#Cacheable(value = "codeValues")
List<CodeCategory> findByCodeValue(String code);
}
Note: There are multiple ways as pointed by others. You can choose as per your needs.
My way is to define a generic cache handler
#FunctionalInterface
public interface GenericCacheHandler {
List<CodeCategory> findAll();
}
And its implementation as below
#Component
#EnableScheduling // Important
public class GenericCacheHandlerImpl implements GenericCacheHandler {
#Autowired
private CodeRepository codeRepo;
private List<CodeCategory> codes = new ArrayList<>();
#PostConstruct
private void intializeBudgetState() {
List<CodeCategory> codeList = codeRepo.findAll();
// Any customization goes here
codes = codeList;
}
#Override
public List<CodeCategory> getCodes() {
return codes;
}
}
Call it in Service layer as below
#Service
public class CodeServiceImpl implements CodeService {
#Autowired
private GenericCacheHandler genericCacheHandler;
#Override
public CodeDTO anyMethod() {
return genericCacheHandler.getCodes();
}
}
Use the second level hibernate caching to cache all the required db queries.
For caching at the application start-up, we can use #PostContruct in any of the Service class.
Syntax will be :-
#Service
public class anyService{
#PostConstruct
public void init(){
//call any method
}
}
Use CommandLineRunner interface.
Basically , you can create a Spring #Component and implement CommandLineRunner interface. You will have to override it's run method. The run method will be called at the start of the app.
#Component
public class DatabaseLoader implements
CommandLineRunner {
#override
Public void run(.... string){
// Any code here gets called at the start of the app.
}}
This approach is mostly used to bootstrap the application with some initial data.

How to set tableName dynamically using environment variable in spring boot?

I am using AWS ECS to host my application and using DynamoDB for all database operations. So I'll have same database with different table names for different environments. Such as "dev_users" (for Dev env), "test_users" (for Test env), etc.. (This is how our company uses same Dynamo account for different environments)
So I would like to change the "tableName" of the model class using the environment variable passed through "AWS ECS task definition" environment parameters.
For Example.
My Model Class is:
#DynamoDBTable(tableName = "dev_users")
public class User {
Now I need to replace the "dev" with "test" when I deploy my container in test environment. I know I can use
#Value("${DOCKER_ENV:dev}")
to access environment variables. But I'm not sure how to use variables outside the class. Is there any way that I can use the docker env variable to select my table prefix?
My Intent is to use like this:
I know this not possible like this. But is there any other way or work around for this?
Edit 1:
I am working on the Rahul's answer and facing some issues. Before writing the issues, I'll explain the process I followed.
Process:
I have created the beans in my config class (com.myapp.users.config).
As I don't have repositories, I have given my Model class package name as "basePackage" path. (Please check the image)
For 1) I have replaced the "table name over-rider bean injection" to avoid the error.
For 2) I printed the name that is passing on to this method. But it is Null. So checking all the possible ways to pass the value here.
Check the image for error:
I haven't changed anything in my user model class as beans will replace the name of the DynamoDBTable when the beans got executed. But the table name over riding is happening. Data is pulling from the table name given at the Model Class level only.
What I am missing here?
The table names can be altered via an altered DynamoDBMapperConfig bean.
For your case where you have to Prefix each table with a literal, you can add the bean as such. Here the prefix can be the environment name in your case.
#Bean
public TableNameOverride tableNameOverrider() {
String prefix = ... // Use #Value to inject values via Spring or use any logic to define the table prefix
return TableNameOverride.withTableNamePrefix(prefix);
}
For more details check out the complete details here:
https://github.com/derjust/spring-data-dynamodb/wiki/Alter-table-name-during-runtime
I am able to achieve table names prefixed with active profile name.
First added TableNameResolver class as below,
#Component
public class TableNameResolver extends DynamoDBMapperConfig.DefaultTableNameResolver {
private String envProfile;
public TableNameResolver() {}
public TableNameResolver(String envProfile) {
this.envProfile=envProfile;
}
#Override
public String getTableName(Class<?> clazz, DynamoDBMapperConfig config) {
String stageName = envProfile.concat("_");
String rawTableName = super.getTableName(clazz, config);
return stageName.concat(rawTableName);
}
}
Then i setup DynamoDBMapper bean as below,
#Bean
#Primary
public DynamoDBMapper dynamoDBMapper(AmazonDynamoDB amazonDynamoDB) {
DynamoDBMapper mapper = new DynamoDBMapper(amazonDynamoDB,new DynamoDBMapperConfig.Builder().withTableNameResolver(new TableNameResolver(envProfile)).build());
return mapper;
}
Added variable envProfile which is an active profile property value accessed from application.properties file.
#Value("${spring.profiles.active}")
private String envProfile;
We have the same issue with regards to the need to change table names during runtime. We are using Spring-data-dynamodb 5.0.2 and the following configuration seems to provide the solutions that we need.
First I annotated my bean accessor
#EnableDynamoDBRepositories(dynamoDBMapperConfigRef = "getDynamoDBMapperConfig", basePackages = "my.company.base.package")
I also setup an environment variable called ENV_PREFIX which is Spring wired via SpEL.
#Value("#{systemProperties['ENV_PREFIX']}")
private String envPrefix;
Then I setup a TableNameOverride bean:
#Bean
public DynamoDBMapperConfig.TableNameOverride getTableNameOverride() {
return DynamoDBMapperConfig.TableNameOverride.withTableNamePrefix(envPrefix);
}
Finally, I setup the DynamoDBMapperConfig bean using TableNameOverride injection. In 5.0.2, we had to setup a standard DynamoDBTypeConverterFactory in the DynamoDBMapperConfig builder to avoid NPE.:
#Bean
public DynamoDBMapperConfig getDynamoDBMapperConfig(DynamoDBMapperConfig.TableNameOverride tableNameOverride) {
DynamoDBMapperConfig.Builder builder = new DynamoDBMapperConfig.Builder();
builder.setTableNameOverride(tableNameOverride);
builder.setTypeConverterFactory(DynamoDBTypeConverterFactory.standard());
return builder.build();
}
In hind sight, I could have setup a DynamoDBTypeConverterFactory bean that returns a standard DynamoDBTypeConverterFactory and inject that into the getDynamoDBMapperConfig() method using the DynamoDBMapperConfig builder. But this will also do the job.
I up voted the other answer but here is an idea:
Create a base class with all your user details:
#MappedSuperclass
public abstract class AbstractUser {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
Create 2 implentations with different table names and spirng profiles:
#Profile(value= {"dev","default"})
#Entity(name = "dev_user")
public class DevUser extends AbstractUser {
}
#Profile(value= {"prod"})
#Entity(name = "prod_user")
public class ProdUser extends AbstractUser {
}
Create a single JPA respository that uses the mapped super classs
public interface UserRepository extends CrudRepository<AbstractUser, Long> {
}
Then switch the implentation with the spring profile
#RunWith(SpringJUnit4ClassRunner.class)
#DataJpaTest
#Transactional
public class UserRepositoryTest {
#Autowired
protected DataSource dataSource;
#BeforeClass
public static void setUp() {
System.setProperty("spring.profiles.active", "prod");
}
#Test
public void test1() throws Exception {
DatabaseMetaData metaData = dataSource.getConnection().getMetaData();
ResultSet tables = metaData.getTables(null, null, "PROD_USER", new String[] { "TABLE" });
tables.next();
assertEquals("PROD_USER", tables.getString("TABLE_NAME"));
}
}

Spring Data Rest: RepositoryEventHandler methods not invoked

I am trying to add a RepositoryEventHandler as described on Spring Data REST documentation to the REST repository shown below:
#RepositoryRestResource(collectionResourceRel = "agents", path = "/agents")
public interface AgentRepository extends CrudRepository<Agent, Long> {
// no implementation required; Spring Data will create a concrete Repository
}
I created an AgentEventHandler:
#Component
#RepositoryEventHandler(Agent.class)
public class AgentEventHandler {
/**
* Called before {#link Agent} is persisted
*
* #param agent
*/
#HandleBeforeSave
public void handleBeforeSave(Agent agent) {
System.out.println("Saving Agent " + agent.toString());
}
}
and declared it in a #Configuration component:
#Configuration
public class RepositoryConfiguration {
/**
* Declare an instance of the {#link AgentEventHandler}
*
* #return
*/
#Bean
AgentEventHandler agentEvenHandler() {
return new AgentEventHandler();
}
}
When I am POSTing to the REST resource, the Entity gets persisted but the method handleBeforeSave never gets invoked. What am I missing?
I'm using: Spring Boot 1.1.5.RELEASE
Sometimes obvious mistakes go unnoticed.
POST-ing a Spring Data REST resource, emits a BeforeCreateEvent. To catch this event, the method handleBeforeSave must be annotated with #HandleBeforeCreate instead of #HandleBeforeSave (the latter gets invoked on PUT and PATCH HTTP calls).
Tests pass successfully on my (cleaned up) demo app now.
How does your main Application class look like? Does it import the RepositoryRestMvcConfiguration as described in https://spring.io/guides/gs/accessing-data-rest/?

Categories