How can I execute custom logic after Spring cloud configuration refresh? - java

I have my application set up to use Spring cloud config for providing configuration and have the monitor enabled so that the config server publishes change events to my application. The configuration gets updated correctly, but I want to be notified of when the configuration changes so I can execute some custom logic based on the new config.
I have this configuration object
#Configuration
#RefreshScope
#ConfigurationProperties(prefix = "my.prefix")
public class MyConfig {
private Map<String, MyObject> configs;
private String someValue;
public Map<String, MyObject> getConfigs(){...}
public void setConfigs(){...}
public String getSomeValue(){...}
public void setSomeValue(){...}
}
...
public class MyObject {
private String field1;
public String getField1() {...}
public void setField1() {...}
}
And this in my config servers application.yml
my:
prefix:
configs:
TEST:
field1: "testValue"
someValue: "test"
Now when I change someValue in the configuration, and the config server publishes a refresh, it calls setSomeValue() and updates the value to the new value. I can add my custom logic to setSomeValue() and it will work fine. However it does not seem to call setConfigs() or setField1() when updating or adding/removing entries from configs.
I tried registering a listener for EnviornmentChangeEvents, RefreshEvents, or RefreshScopeRefreshedEvents but those are either triggered before Spring updates the values or aren't triggered at all. I also tried adding logic to #PreDestroy and #PostConstruct methods but only the PreDestroy ends up being called and it's called before the configuration is updated. I also tried implementing InitializingBean and putting my logic in afterPropertiesSet() but it never get's called either.
How can I get notified when this configuration get's updated?

With a RefreshScopeRefreshedEvent Listener you can get notified when the configuration is updated.
The following example works for me:
The configuration:
#Configuration
public class Config {
#Bean
#RefreshScope
public A aBean() {
return new A();
}
#Bean
public RefreshScopeRefreshedListener remoteApplicationEventListener(A aBean) {
return new RefreshScopeRefreshedListener(aBean);
}
}
And the listener:
public class RefreshScopeRefreshedListener implements ApplicationListener<RefreshScopeRefreshedEvent> {
private A aBean;
public RefreshScopeRefreshedListener(A abean) {
this.aBean = abean;
}
#Override
public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
System.out.println(aBean.getValue());
}
}
It always print the new value of the configuration.
If you already tried this listener, are you sure that is has been registered well? The bean has been created correctly?

What I've done is to get hook of EnvironmentChangeEvent listener, and then get the updated properties from the Environment itself, not from the #Autowired Bean bean
#Autowired
private Environment env;
#EventListener(EnvironmentChangeEvent.class)
public void onApplicationEvent(EnvironmentChangeEvent environmentChangeEvent) {
log.info("Received an environment changed event for keys {}", environmentChangeEvent.getKeys());
if(environmentChangeEvent.getKeys().contains("key.i.wanted.to.recalculate")) {
String newValue = env.getProperty("key.i.wanted.to.recalculate");
System.out.println("New Value: " + newValue);
}
}

Alternatively you can add #PostConstruct with #ConfigurationProperties so that you can write your custom logic
#ConfigurationProperties(prefix = "dummy.just.to.trigger.actuator.refresh")
Class AnyPropLoader {
#PostConstruct
private void customLogicMethod(){
...
}
}

Create a Bean (dto or an object)
update it, in any method
Autowire that dto/object in another class
That class is updating the values of that DTO/Object
This is how I solved this updating of data and using #Autowired.

Related

SimpUserRegistry doesnot contain any session objects

Iam new to Websockets. I have been trying to use SimpUserRegistry to find session object by Principal. I wrote a custom handshake handler to convert Anonymous users to authenticated users and Iam able to access the Principal name from Websocket session object.
The code for custom handshake handler is shown below
import java.security.Principal;
public class StompPrincipal implements Principal {
private String name;
public StompPrincipal(String name) {
this.name = name;
}
#Override
public String getName() {
return name;
}
}
Handler
class CustomHandshakeHandlerTwo extends DefaultHandshakeHandler {
// Custom class for storing principal
#Override
protected Principal determineUser(
ServerHttpRequest request,
WebSocketHandler wsHandler,
Map<String, Object> attributes
) {
// Generate principal with UUID as name
return new StompPrincipal(UUID.randomUUID().toString());
}
}
But as specified in many questions like this I'am not able to inject the SimpUserRegistry directly.
It throws error
Field simpUserRegistry required a bean of type 'org.springframework.messaging.simp.user.SimpUserRegistry' that could not be found.
The injection point has the following annotations:
- #org.springframework.beans.factory.annotation.Autowired(required=true)
Action:
Consider defining a bean of type 'org.springframework.messaging.simp.user.SimpUserRegistry' in your configuration.
So I created a configuration class as shown below.
#Configuration
public class UsersConfig {
final private SimpUserRegistry userRegistry = new DefaultSimpUserRegistry();
#Bean
#Primary
public SimpUserRegistry userRegistry() {
return userRegistry;
}
}
Now I can autowire and use it but everytime I try to acess the SimpUserRegistry it is empty.
What could be the cause of this problem?
EDIT:
Showing websocket config
#Configuration
#EnableWebSocket
#Controller
#Slf4j
public class WebSocketConfig implements WebSocketConfigurer {
#Autowired
EventTextHandler2 handler;
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
log.info("Registering websocket handler SocketTextHandler");
registry.addHandler(handler, "/event").setHandshakeHandler(new CustomHandshakeHandlerTwo());
}
}
SimpUserRegistry is an "infrastructure bean" registered/provided by Spring WebSocket, you should not instantiate it directly.
Is your WebSocket Spring configuration correct?
Make sure your application is well configured (ie. your configuration class is being scanned).
SimpUserRegistry is imported by spring-messaging dependency: make sure your configuration class is annotated with #EnableWebSocketMessageBroker.
Official documentation: https://docs.spring.io/spring-framework/docs/5.3.6/reference/html/web.html#websocket-stomp-enable
To back the connected users in Redis, you may want to create a new SimpUserRegistry implementation:
public class RedisSimpUserRegistry implements SimpUserRegistry, SmartApplicationListener {
private final RedisTemplate redisTemplate;
public RedisSimpUserRegistry(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
[...]
#Override
public void onApplicationEvent(ApplicationEvent event) {
// Maintain Redis collection on event type
// ie. SessionConnectedEvent / SessionDisconnectEvent
}
[...]
}
PS: The #Controller annotation on your config class is not necessary unless you have an endpoint defined in it.
Edit after new comments:
You can see the DefaultSimpUserRegistry implementation to get an idea of how to do it.
To intercept an application event, you have to implement the ApplicationListener interface (in this case SmartApplicationListener).
The supportsEventType method is important to define which event types you want to intercept:
#Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
return AbstractSubProtocolEvent.class.isAssignableFrom(eventType);
}
The AbstractSubProtocolEvent have multiple implementations. The most important ones are SessionConnectEvent, SessionDisconnectEvent.
Intercepting (see onApplicationEvent method) these event types will allow your implementation to maintain the desired state in your Redis cache. You could then store users (ids, etc.).

Can't achieve dependency injection oustide a controller in Spring Booot

I am new at spring MVC framework and i am currently working in a web application that uses a session scoped bean to control some data flow.
I can access these beans in my application context using #Autowired annotation without any problem in the controllers. The problem comes when I use a class in service layer that does not have any request mapping (#RequestMapping, #GetMapping nor #PostMapping) annotation.
When I try to access the application context directly or using #Autowired or even the #Resource annotation the bean has a null value.
I have a configuration class as follow:
#Configuration
#EnableAspectJAutoProxy
#EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class, basePackages = "com.quantumx.nitididea.NITIDideaweb.repository")
public class AppConfig implements WebMvcConfigurer {
#Bean (name = "lastTemplate")
#SessionScope
public LastTemplate getlastTemplate() {
return new LastTemplate();
}
//Some extra code
}
The POJO class is defined as :
public class LastTemplate {
private Integer lastId;
public LastTemplate(){
}
public Integer getLastId() {
return lastId;
}
public void setLastId(Integer lastId) {
this.lastId = lastId;
}
}
The I have a Test class that is annotated as service and does not have any request mapping annotated method:
//#Controller
#Service
public class Test {
// #Autowired
// private ApplicationContext context;
// #Autowired
#Resource(name = "lastTemplate")
public LastTemplate lastTemplate;
// #Autowired
// public void setLastTemplate(LastTemplate lastTemplate) {
// this.lastTemplate = lastTemplate;
// }
public Test() {
}
// #RequestMapping("/test")
public String testing() {
// TemplateForma last = (TemplateForma) context.getBean("lastInsertedTemplate");
// System.out.println(last);
System.out.println(lastTemplate);
// System.out.println(context.containsBean("lastTemplate"));
// System.out.println(context.getBean("lastTemplate"));
System.out.println("Testing complete");
return "Exit from testing method";
// return "/Messages/Success";
}
}
As you can see, there is a lot of commented code to show all the ways i have been trying to access my application context, using an Application context dependency, autowiring, declaring a resource and trying with a request mapping. The bean is null if no controller annotation and request mapping method is used and throws a java null pointer exception when I use the context getBean() methods.
Finally I just test my class in a controller that i have in my app:
#RequestMapping("/all")
public String showAll(Model model) {
Test test = new Test();
test.testing();
return "/Administrator/test";
}
Worth to mention that I also tried to change the scope of the bean to a Application scope and singleton, but it not worked. How can access my application context in a service class without mapping a request via controller?
Worth to mention that I also tried to change the scope of the bean to a Application scope and singleton, but it not worked
It should have worked in this case.
How can access my application context in a service class without mapping a request via controller?
Try one of these :-
#Autowired private ApplicationContext appContext;
OR
Implement ApplicationContextAware interface in the class where you want to access it.
Edit:
If you still want to access ApplicationContext from non spring managed class. Here is the link to article which shows how it can be achieved.
This page gives an example to get spring application context object with in non spring managed classes as well
What worked for me is that session scoped bean had to be removed in the application configuration declaration and moved to the POJO definition as follows:
#Component
#SessionScope
public class LastTemplate {
private Integer lastId;
public LastTemplate(){
}
public Integer getLastId() {
return lastId;
}
public void setLastId(Integer lastId) {
this.lastId = lastId;
}
}
The I just call the bean using #Autowired annotation.

Spring Boot - Autowire component using different profiles

I have a component EmbeddedRedis that depends on a configuration object RedisConfig parsed from the application's property file. There are different property files, corresponding to the possible application profiles that can be run. Thus, when run in profile master, the component EmbeddedRedis will be provisioned according to the master profile.
In a test class, that is supposed to set-up a local Redis cluster, I also require Redis objects provisioned according to all other profiles. I sketched my idea below using the #Qualifier annotation, which does not bring the desired result.
#Autowired #Qualifier("dev-cluster-master")
private Redis embeddedRedisMaster;
#Autowired #Qualifier("dev-cluster-slave-001")
private Redis embeddedRedisSlave1;
#Autowired #Qualifier("dev-cluster-slave-002")
private Redis embeddedRedisSlave2;
How can I archive the desired result in Spring Boot? If that doesn't work directly, would it also suffice to obtain the before-mentioned configuration objects parsed from the different property files.
#Component
#ConfigurationProperties(prefix = "spring.redis")
public class RedisConfig {
....
}
Thanks in advance!
You can do something like this:
Consider you have a class definition (Redis in your example)
public class CustomService {
private String name;
public CustomService(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
And a configuration class like:
#Configuration
public class Config {
#Bean
#Profile("master")
CustomService serverConfig1(){
CustomService service1 = new CustomService("master");
return service1;
}
#Bean
#Profile("slave")
CustomService serverConfig2(){
CustomService service1 = new CustomService("slave");
return service1;
}
}
which initiate 2 different objects based on current active profile. If current active profile is "master", then serverConfig1() will get executed, otherwise serverConfig2().
And finally autowired your service/object like this:
#Autowired
CustomService service;
This will depends on above executed bean definition in configuration file.
And property file should look like this:
spring.profiles.active=slave
So in this example, after executing above code, the value of 'name' in CustomService service; will be "slave" instead of "master", because current active profile is "slave" and thus "serverConfig2()" will get executed
You can do something like this: Consider you have an interface definition:
public interface SomeService {
String someMethod;
}
And two implemented class:
#Profile("production")
#Service
public class SomeServiceProd implements SomeService {
#Override String someMethod() {return "production";}
}
#Profile("development")
#Service
public class SomeServiceProd implements SomeService {
#Override String someMethod() {return "development";}
}
And use this service in test and main code:
#Autowired SomeService service;
If you need your component only with certain profile and you don't want to or can't create a common interface you could inject like this:
#Autowired
private Optional< Redis > redis;
You would have to check if present on each different object on every profile.
If you share a common interface use the other answer solution to create different beans implementing the interface per each profile.

How do i register AbstractMongoEventListener programmatically?

In my Spring Boot application, i have a configuration, which reads entries from a Mongo database.
After this is done, my subclass of AbstractMongoEventListener is created, even though it operates on a different table and different scope (my own custom #CustomerScope).
Here is the listener:
#CustomerScoped
#Component
public class ProjectsRepositoryListener extends AbstractMongoEventListener<Project> {
#Override
public void onAfterSave(Project source, DBObject dbo) {
System.out.println("saved");
}
}
And here the configuration:
#Configuration
public class MyConfig {
#Autowired
private CustomersRepository customers;
#PostConstruct
public void initializeCustomers() {
for (Customer customer : customers.findAll()) {
System.out.println(customer.getName());
}
}
}
I find it surprising that the listener is instantiated at all. Especially since it is instantiated well after the call to the customers repository has finished.
Is there a way to prevent this? I was thinking of programmatically registering it per table/scope, without annotation magic.
To prevent auto-instantiation, the listener must not be annotated as #Component. The configuration needs to get ahold of the ApplicationContext, which can be autowired.
Thus, my configuration class looks like this:
#Autowired
private AbstractApplicationContext context;
private void registerListeners() {
ProjectsRepositoryListener firstListener = beanFactory.createBean(ProjectsRepositoryListener.class);
context.addApplicationListener(firstListener);
MySecondListener secondListener = beanFactory.createBean(MySecondListener.class);
context.addApplicationListener(secondListener);
}
Note that this works for any ApplicationListener, not just AbstractMongoEventListener.

Loading Beans based on hostname

I am writing services in Spring boot that get their configurations from Spring cloud. These services are multi-tenant and the tenant is based on the host name.
what I have now is
public class MyController {
#Autowired
public MyController(MyServiceFactory factory) {
...
}
#RequestMapping("some/path/{id}")
ResponseEntity<SomeEntity> getSomeEntity(#RequestHeader header, #PathVariable id) {
return factory.getMyService(header).handle(id);
}
}
where MyServiceFactory looks something like...
public class MyServiceFactory {
private final HashMap<String, MyService> serviceRegistry = new HashMap<>();
public MyService getMyService(String key) {
return serviceRegistry.get(key);
}
MyServiceFactory withService(String key, MyService service) {
this.serviceRegistry.put(key, service);
return this;
}
}
then in a configuration file
#Configuration
public ServiceFactoryConfiguration {
#Bean
public MyServiceFactory getMyServiceFactory() {
return new MyServiceFactory()
.withService("client1", new MyService1())
.withService("client2", new MyService2());
}
}
While what I have now works, I don't like that I need to create a factory for every dependency my controller may have. I'd like to have my code look something like this...
public class MyController {
#Autowired
public MyController(MyService service) {
...
}
#RequestMapping("some/path/{id}")
ResponseEntity<SomeEntity> getSomeEntity(#PathVariable id) {
return service.handle(id);
}
}
with a configuration file like
#Configuration
public class MyServiceConfiguration() {
#Bean
#Qualifier("Client1")
public MyService getMyService1() {
return new MyService1();
}
#Bean
#Qualifier("Client2")
public MyService getMyService2() {
return new MyService2();
}
}
I can get the code that I want to write if I use a profile at application start up. But I want to have lots of different DNS records pointing to the same (pool of) instance(s) and have an instance be able to handle requests for different clients. I want to be able to swap out profiles on a per request basis.
Is this possible to do?
Spring profiles would not help here, you would need one application context per client, and that seems not what you want.
Instead you could use scoped beans.
Create your client dependent beans with scope 'client' :
#Bean
#Scope(value="client",proxyMode = ScopedProxyMode.INTERFACES)
#Primary
MyService myService(){
//does not really matter, which instance you create here
//the scope will create the real instance
//may be you can even return null, did not try that.
return new MyServiceDummy();
}
There will be at least 3 beans of type MyService : the scoped one, and one for each client. The annotation #Primary tells spring to always use the scoped bean for injection.
Create a scope :
public class ClientScope implements Scope {
#Autowired
BeanFactory beanFactory;
Object get(String name, ObjectFactory<?> objectFactory){
//we do not use the objectFactory here, instead the beanFactory
//you somehow have to know which client is the current
//from the config, current request, session, or ThreadLocal..
String client=findCurrentClient(..);
//client now is something like 'Client1'
//check if your cache (HashMap) contains an instance with
//BeanName = name for the client, if true, return that
..
//if not, create a new instance of the bean with the given name
//for the current client. Easiest way using a naming convention
String clientBeanName=client+'.'+name;
Object clientBean=BeanFactory.getBean(clientBeanName);
//put in cache ...
return clientBean;
};
}
And your client specific beans are configured like this :
#Bean('Client1.myService')
public MyService getMyService1() {
return new MyService1();
}
#Bean('Client2.myService')
public MyService getMyService2() {
return new MyService2();
}
Did not test it but used it in my projects. Should work.
tutorial spring custom scope

Categories

Resources