Question regarding how to have Spring Webflux Websocket as annotation please.
I am working on a Springboot Webflux Websocket project where it handles BOTH restful api AND a websocket.
To emphasize, it is a Spring Webflux project alone. Not a Springboot starter websocket, not a Springboot rsocket, not a Springboot pub sub.
The project is as follow:
#SpringBootApplication
public class EchoApplication {
public static void main(String[] args) {
SpringApplication.run(EchoApplication.class, args);
}
}
#RestController
public class EchoController {
#GetMapping(value = "/getEcho")
public Mono<String> getEcho() {
return Mono.just("echo");
}
}
public class EchoHandler implements WebSocketHandler {
#Override
public Mono<Void> handle(WebSocketSession session) {
return session.send( session.receive().map(msg -> "RECEIVED ON SERVER :: " + msg.getPayloadAsText()).map(session::textMessage));
}
}
#Configuration
public class EchoConfiguration {
#Bean
public EchoHandler echoHandler() {
return new EchoHandler();
}
#Bean
public HandlerMapping handlerMapping() {
Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/echo", echoHandler());
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setUrlMap(map);
mapping.setOrder(Ordered.HIGHEST_PRECEDENCE);
return mapping;
}
#Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter(webSocketService());
}
#Bean
public WebSocketService webSocketService() {
return new HandshakeWebSocketService(new ReactorNettyRequestUpgradeStrategy());
}
}
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
As you can see, in this project, there is the "traditional" annotation #GetMapping(value = "/getEcho"). Many Spring projects uses this style where it is exposed as annotation (event rscocket, pub/sub, etc...)
How to have:
#Bean
public HandlerMapping handlerMapping() {
Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/echo", echoHandler());
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setUrlMap(map);
as annotation, some kind of #WebsocketMapping("/echo") which will help present an unified and cleaner way to see routes?
Thank you
From Spring Team, this is not currently supported, neither have plan to support route based web socket for Webflux.
Related
I'm feeling stupid to ask this, but I can't understand where I'm wrong with my code.
The context is :
a Spring Boot application (1.5.7) with an embedded Jetty server and a
controller to expose some endpoints
a unique #Configuration class, where some of my beans are defined (Singleton and Prototype scopes)
a #Service that uses some beans defined in my #Configuration class
The problem is:
a NoSuchBeanDefinitionException for one of my #Configuration bean.
Now the details:
My SpringBootApplication :
#SpringBootApplication
public class HbbTVApplication {
public static void main(String[] args) {
SpringApplication.run(HbbTVApplication.class, args);
}
}
My #Configuration class:
#Configuration
#Profile(value = { "dev", "int", "pre", "pro" })
public class StandaloneFrontalConfig extends WebMvcConfigurerAdapter {
#Value("${kafka.bootstrap-servers}")
private String bootstrapServers;
#Bean
public Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
return props;
}
#Bean
public ProducerFactory<String, String> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
}
#Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/standalone/");
}
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*").allowedHeaders("*");
}
};
}
#Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
#Bean
public Security securityManager() {
return new Security();
}
#Bean
#Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public KngAflow getTechnicalCookie() {
return new KngAflow();
}
#Bean
public EmbeddedServletContainerCustomizer customizer() {
return new EmbeddedServletContainerCustomizer() {
#Override
public void customize(ConfigurableEmbeddedServletContainer container) {
if (container instanceof JettyEmbeddedServletContainerFactory) {
customizeJetty((JettyEmbeddedServletContainerFactory) container);
}
}
private void customizeJetty(JettyEmbeddedServletContainerFactory jetty) {
jetty.addServerCustomizers(new JettyServerCustomizer() {
#Override
public void customize(Server server) {
for (Connector connector : server.getConnectors()) {
if (connector instanceof ServerConnector) {
HttpConnectionFactory connectionFactory = ((ServerConnector) connector)
.getConnectionFactory(HttpConnectionFactory.class);
connectionFactory.getHttpConfiguration().setCookieCompliance(CookieCompliance.RFC2965);
}
}
}
});
}
};
}
}
My #Service:
#Service
public class CookieService implements services.CookieService, InitializingBean {
/**
* Serializable
*/
private static final long serialVersionUID = -1997257884335775587L;
#Autowired
ApplicationContext app;
#Override
public Cookie createTechnicalCookie() {
return new Cookie(app.getBean(KngAflow.class), null);
}
#Override
public void afterPropertiesSet() throws Exception {
if (app != null) {
for (String bean : app.getBeanDefinitionNames()) {
System.out.println("Bean: " + bean);
}
}
}
}
And the "non defined" bean:
#JsonInclude(Include.NON_NULL)
#JsonIgnoreProperties({ "security", "maxAge", "domain", "updated" })
public class KngAflow implements Serializable, InitializingBean {
#JsonProperty(value = "did")
private String did;
#JsonProperty(value = "checksum")
private String checksum;
#Autowired
private Security security;
private Integer maxAge;
private String domain;
private boolean updated = false;
public KngAflow() {
domain = ".mydomain.com";
}
#Override
public void afterPropertiesSet() throws Exception {
did = UUID.randomUUID().toString();
maxAge = 365 * 24 * 60 * 60;
checksum = security.encrypt(did + security.md5(did));
}
}
NB: Classes are not complete, and there are more classes in my project. I only put what I saw as relevant information.
If something else is needed, just ask me please.
By the way, all the endpoints are defined into a unique #Controller class, and all the endpoints are working except those needing the getTechCookie #Bean.
So, my problem occurs in runtime execution. When I start my Spring Boot app, Jetty is started and listening on the configured port.
Nevertheless, if you look at the CookieService #Service, I'm listing all the bean names defined in the autowired context and my getTechnicalCookie (a.k.a KngAflow) #Bean is missing. I can't understand why.
Of course, when I invoke my #controller to execute my #Service code, the NoSuchBeanDefinitionException is thrown executing the line app.getBean(KngAflow.class).
I tried to use a bean name instead of bean type, no change.
For testing purpose (as it doesn't make sense from a logical point of view), I defined my bean getTechCookie #Bean as a Singleton scoped bean, and the name is still missing from the ApplicationContext.
And the last but not least thing is: Everything works fine with Eclipse!
I mean, all my devs are done using Eclipse IDE. My Spring Boot app is built with Maven and executing it inside Eclipse works correctly (and my getTechCookie Bean is defined and listed).
When I package my app using the Maven Spring Boot plugin and execute it using java -jar, my getTechCookie (KngAflow.class) bean is missing. Nevertheless, this class is present inside the jar.
Spring parameters to launch the spring boot app are spring default values (port 8080, no SSL, ...) and the active.profiles are always between dev, int, pre or pro (those defined in my #Configuration class)
What am I doing wrong?
Thanks!
If it helps, I add my POM definition:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>my-app</artifactId>
<packaging>jar</packaging>
<parent>
<groupId>com.mydomain.bigdata</groupId>
<artifactId>mybigapp</artifactId>
<version>1.1-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*</include>
<include>application.yml</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
EDIT: I changed my #Service class to "force" spring to accept my class as a prototype bean, and it works. It's very ugly but it works. But if someone could help me to find what's wrong, I don't like this workaround:
#Override
public void afterPropertiesSet() throws Exception {
if (!context.containsBeanDefinition(KngAflow.class.getName()))
context.registerBeanDefinition(KngAflow.class.getName(),
BeanDefinitionBuilder.genericBeanDefinition(KngAflow.class).setScope("prototype").getBeanDefinition());
}
I made a following simple application to reproduce issue.
#SpringBootApplication
public class Application {
public static void main(String[] args) {
run(Application.class, args);
}
}
#Configuration
#Profile("dev")
public class BeanConfiguration {
#Bean
#Scope(scopeName = SCOPE_PROTOTYPE)
public PrototypeBean prototypeBean() {
return new PrototypeBean();
}
}
public class PrototypeBean {}
#Service
#Slf4j
public class SingletonBean implements InitializingBean {
#Autowired
private ApplicationContext context;
public PrototypeBean getPrototypeBean() {
return context.getBean(PrototypeBean.class);
}
#Override
public void afterPropertiesSet() throws Exception {
for (String name : context.getBeanDefinitionNames()) {
Class<?> c = context.getBean(name).getClass();
log.debug("===> Name: {}, Type = {}", name, c.getTypeName());
}
}
}
#RestController
#RequestMapping("/bean")
public class BeanRestController {
#Autowired
private SingletonBean singletonBean;
#GetMapping("/name")
public String getName() {
return singletonBean.getPrototypeBean().getClass().getName();
}
}
When I execute application with -Dspring.profiles.active=dev setting
Then I see in the log without no issue and REST endpoint gives back response properly:
===> Name: prototypeBean, Type = PrototypeBean
But if I execute application without profile setting
Then I see error in the log and REST endpoint raise exception:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'PrototypeBean' available
My goal is to create a Webserver with Spring. It has to implement Multitenancy, which works great if you don't make it dynamic (adding, removing, changing). Is it possible to update the datasource bean in Spring?
My code:
#SpringBootApplication
public class MyApplication {
public static void main(String[] args) throws IOException {
SpringApplication.run(MyApplication.class, args);
}
//Multitenancy
#Bean
public DataSource dataSource(){
//implements AbstractRoutingDataSource
CustomRoutingDataSource customDataSource = new CustomRoutingDataSource();
//logic here
return customDataSource;
}
}
What I've tried:
CustomRoutingDataSource c = context.getBean(CustomRoutingDataSource.class);
c.setTargetDataSources(CustomRoutingDataSource.getCustomDatasources());
which updates the bean(?) but doesn't update Spring's datasources, database connections are still missing if added with this method.
Simple solution for those with the same problem:
Add #RefreshScope
#Bean
#RefreshScope
public DataSource dataSource() {
CustomRoutingDataSource customDataSource = new CustomRoutingDataSource();
...
return customDataSource;
}
Add spring actuator endpoint in pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
POST to /actuator/refresh to update datasources!
I am trying add Spring cache to my project. I read the getting started and some examples on the net and look easy, but when I use cacheable annotation, my method results in HTTP 404 error.
These were my steps:
Add cache dependency to my pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
Add #EnableCaching annotation to my main class.
#SpringBootApplication
#EnableCaching
public class MyProjectApplication {
public static void main(String[] args) {
SpringApplication.run(MyProjectApplication.class, args);
}
}
Add a CacheManager Bean to my project:
#Configuration
public class ConfigApplication {
#Bean
public CacheManager cacheManager() {
String[] cacheNames = { "videoInfo" };
return new ConcurrentMapCacheManager(cacheNames);
}
}
Now in the method that I like caching, I added #Cacheable("videoInfo") annotation. (This method's bean is annotated with #RestController). I added other method with #CacheEvict to reset the cache.
#Override
#Cacheable("videoInfo")
#RequestMapping(value = "/get-video-download", method = RequestMethod.POST, produces = "application/json")
public DownloadInfo getDownloadUrls(#RequestParam String videoId) {
DownloadInfo di = null;
di = downloadService.getDownloadInfo(videoId);
return di;
}
#CacheEvict(value = "videoInfo", allEntries = true)
#RequestMapping(value = "/get-video-download-reset-cache", method = RequestMethod.GET)
public void getDownloadUrlsResetCache() {
LOG.debug("Se ha limpiado la caché de videos correctamente");
}
Then, when I make an HTTP request for any method in this bean, I get 404 error. I don't see logs from error and all work fine if I comment these two annotations in the above methods. Any idea of this?
I have tried the new way of customizing the health Actuator in Spring Boot 2.0.0.M5, as described here: https://spring.io/blog/2017/08/22/introducing-actuator-endpoints-in-spring-boot-2-0:
#Endpoint(id = "health")
public class HealthEndpoint {
#ReadOperation
public Health health() {
return new Health.Builder()
.up()
.withDetail("MyStatus", "is happy")
.build();
}
}
However, when I run HTTP GET to localhost:port/application/health, I still get the standard default health info. My code is completely ignored.
When I use the "traditional way" of customizing the health info via implementation of HealthIndicator, it works as expected, the health information is decorated with the given details:
#Component
public class MyHealthIndicator implements HealthIndicator {
#Override
public Health health() {
return new Health.Builder()
.up()
.withDetail("MyStatus 1.1", "is happy")
.withDetail("MyStatus 1.2", "is also happy")
.build();
}
}
QUESTION: What more shall I configure and/or implement to make the #Endpoint(id = "health") solution working?
My intention is not to create a custom actuator myhealth, but to customize the existing health actuator. Based on the documentation I expect to reach the same result as by implementing HealthIndicator. Am I wrong in that assumption?
The Maven configuration pom.xml contains:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.M5</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
The Spring Boot configuration application.properties contains:
endpoints.health.enabled=true
endpoints.autoconfig.enabled=true
endpoints.autoconfig.web.enabled=true
Update
The documentation on the new Spring Actuator Endpoints is not very lucid. It's trying to explain the new endpoint infrastructure with the existing health endpoint as an example.
A new endpoint ID has to be unique and shouldn't be same as an existing actuator endpoint. If one tries to the change the ID of the example shown below to health, one will get the following exception:
java.lang.IllegalStateException: Found two endpoints with the id 'health'
The above comment about declaring the endpoint classes with #Bean annotation is correct.
Customizing the health endpoint hasn't changed in Spring Boot 2.0. You still have to implement HealthIndicator to add custom values.
Custom Actuator Endpoint
Here are the changes needed to create a custom Actuator endpoint in Spring Boot 2.0.
Model
The domain containing your custom information.
#Data
#JsonInclude(JsonInclude.Include.NON_EMPTY)
public class MyHealth {
private Map<String, Object> details;
#JsonAnyGetter
public Map<String, Object> getDetails() {
return this.details;
}
}
My Health Endpoint
Declaring myhealth endpoint,
#Endpoint(id = "myhealth")
public class MyHealthEndpoint {
#ReadOperation
public MyHealth health() {
Map<String, Object> details = new LinkedHashMap<>();
details.put("MyStatus", "is happy");
MyHealth health = new MyHealth();
health.setDetails(details);
return health;
}
}
My Health Extension
Extension for myhealth endpoint,
#WebEndpointExtension(endpoint = MyHealthEndpoint.class)
public class MyHealthWebEndpointExtension {
private final MyHealthEndpoint delegate;
public MyHealthWebEndpointExtension(MyHealthEndpoint delegate) {
this.delegate = delegate;
}
#ReadOperation
public WebEndpointResponse<MyHealth> getHealth() {
MyHealth health = delegate.health();
return new WebEndpointResponse<>(health, 200);
}
}
Actuator Configuration
Configuration to expose the two newly created actuator classes as beans,
#Configuration
public class ActuatorConfiguration {
#Bean
#ConditionalOnMissingBean
#ConditionalOnEnabledEndpoint
public MyHealthEndpoint myHealthEndpoint() {
return new MyHealthEndpoint();
}
#Bean
#ConditionalOnMissingBean
#ConditionalOnEnabledEndpoint
#ConditionalOnBean({MyHealthEndpoint.class})
public MyHealthWebEndpointExtension myHealthWebEndpointExtension(
MyHealthEndpoint delegate) {
return new MyHealthWebEndpointExtension(delegate);
}
}
Application Properties
Changes to application.yml,
endpoints:
myhealth:
enabled: true
Once you start your application, you should be able to access the newly actuator endpoint at http://<host>:<port>/application/myhealth.
You should expect a response similar to one shown below,
{
"MyStatus": "is happy"
}
A complete working example can be found here.
Provide your own #WebEndpoint like
#Component
#WebEndpoint(id = "acmehealth")
public class AcmeHealthEndpoint {
#ReadOperation
public String hello() {
return "hello health";
}
}
and
include it
map the original /health to, say, /internal/health
map your custom endpoint to /health
via application.properties:
management.endpoints.web.exposure.include=acmehealth
management.endpoints.web.path-mapping.health=internal/health
management.endpoints.web.path-mapping.acmehealth=/health
This will override /health completely, not just add the information to the existing /health, as a custom HealthIndicator would. Question is, what you want, because #Endpoint(id = "health") and "My intention is not to create a custom actuator myhealth, but to customize the existing health actuator" kind of collide. But you can use the existing HealthEndpoint in your AcmeHealthEndpoint and accomplish both:
#Component
#WebEndpoint(id = "prettyhealth")
public class PrettyHealthEndpoint {
private final HealthEndpoint healthEndpoint;
private final ObjectMapper objectMapper;
#Autowired
public PrettyHealthEndpoint(HealthEndpoint healthEndpoint, ObjectMapper objectMapper) {
this.healthEndpoint = healthEndpoint;
this.objectMapper = objectMapper;
}
#ReadOperation(produces = "application/json")
public String getHealthJson() throws JsonProcessingException {
Health health = healthEndpoint.health();
ObjectWriter writer = objectMapper.writerWithDefaultPrettyPrinter();
return writer.writeValueAsString(health);
}
#ReadOperation
public String prettyHealth() throws JsonProcessingException {
return "<html><body><pre>" + getHealthJson() + "</pre></body></html>";
}
}
I have the below FeignClient:
#FeignClient(name="FooMS",fallback=CustomerFeign.CustomerFeignImpl.class)
public interface CustomerFeign {
#RequestMapping(value="/bar/{phoneNo}")
List<Long> getFriends(#PathVariable("phoneNo") Long phoneNo);
class CustomerFeignImpl implements CustomerFeign{
#Override
public List<Long> getFriends(Long phoneNo) {
return new ArrayList<Long>(108);
}
}
}
When the FooMS instance is down, I get a 500 error instead of the fallback being executed. Why is this happening?
adding #Component and feign.hystrix.enabled=true works fine
Tag your CustomerFeignImpl as a #Component or create a #Bean out of it.
This works for me with 2020.0.3:
In application.properties
feign.circuitbreaker.enabled=true
In pom.xml
<spring-cloud.version>2020.0.3</spring-cloud.version>
and
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
Thank you, rostlvan!
I am outlining my implementation below:
I am using Spring Cloud version 2020.0.4 and the following configuration worked for me:
in pom.xml, I have these dependencies:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
Though I'm not sure if we need to have both openfeign and hystrix dependencies. Someone can validate that!
In my application.properties I have feign.circuitbreaker.enabled=true
In my Main Application class, I have
#SpringBootApplication
#EnableFeignClients
public class MySpringBootApplication{
public static void main(String[] args) {
SpringApplication.run(MySpringBootApplication.class, args);
}
}
And finally, my Feign Client, fallback and fallback factory:
UserServiceFeignClient.java
#FeignClient(name = "USER-SERVICE", fallbackFactory = UserServiceFallbackFactory.class)
public interface UserServiceFeignClient {
#GetMapping("/api/users/{userId}")
public ResponseEntity<User> getUser(#PathVariable String userId);
}
UserServiceFeignClientFallback.java
public class UserServiceFeignClientFallback implements UserServiceFeignClient{
#Override
public ResponseEntity<User> getUser(String userId) {
return ResponseEntity.ok().body(new User());
}
}
And, UserServiceFeignClientFallbackFactory.java:
#Component
public class UserServiceFallbackFactory implements FallbackFactory<UserServiceFeignClientFallback>{
#Override
public UserServiceFeignClientFallback create(Throwable cause) {
return new UserServiceFeignClientFallback();
}
}
Was facing the problem myself, until I stumbled upon the answer from #rostlvan