Previously were using Spring cloud Finchley.M9 release. We used LoadBalancerClient as like below.
'''import java.net.URI;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.stereotype.Component;
#Component
public class UriResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(UriResolver.class);
private LoadBalancerClient loadBalancerClient;
#Autowired
public UriResolver(LoadBalancerClient loadBalancerClient) {
this.loadBalancerClient = loadBalancerClient;
}
public URI getBaseUri(String vendorName) {
ServiceInstance instance = loadBalancerClient.choose(vendorName);
URI uri = null;
if (!Objects.isNull(instance)) {
uri = instance.getUri();
LOGGER.debug("LoadBalancer Instance specific URI: " + instance.getUri());
LOGGER.debug("LoadBalancer Instance specific ServiceId: " + instance.getServiceId());
LOGGER.debug("LoadBalancer Instance specific Host: " + instance.getHost());
}
return uri;
}
}'''
Now we have upgraded the spring cloud version to Hoxton.SR8. We get the below error while starting the spring boot app.
Bean method 'loadBalancerClient' in 'RibbonAutoConfiguration' not loaded because AllNestedConditions 2 matched 2 did not; NestedCondition on RibbonAutoConfiguration.RibbonClassesConditions.RibbonPresent #ConditionalOnClass did not find required class 'com.netflix.ribbon.Ribbon'; NestedCondition on RibbonAutoConfiguration.RibbonClassesConditions.AsyncRestTemplatePresent #ConditionalOnClass found required class 'org.springframework.web.client.AsyncRestTemplate'; NestedCondition on RibbonAutoConfiguration.RibbonClassesConditions.RestTemplatePresent #ConditionalOnClass found required class 'org.springframework.web.client.RestTemplate'; NestedCondition on RibbonAutoConfiguration.RibbonClassesConditions.IClientPresent #ConditionalOnClass did not find required class 'com.netflix.client.IClient'
Related
I'm trying to define a #TestConfiguration class that is executed once before all integration tests to run a MongoDB TestContainer in Kotlin in a Spring Boot project.
Here is the code:
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.MongoDBContainer
import org.testcontainers.utility.DockerImageName
#TestConfiguration
class TestContainerMongoConfig {
companion object {
#JvmStatic
private val MONGO_CONTAINER: MongoDBContainer = MongoDBContainer(DockerImageName.parse("mongo").withTag("latest")).withReuse(true)
#JvmStatic
#DynamicPropertySource
private fun emulatorProperties(registry: DynamicPropertyRegistry) {
registry.add("spring.data.mongodb.uri", MONGO_CONTAINER::getReplicaSetUrl)
}
init { MONGO_CONTAINER.start() }
}
}
The issue seems to be that emulatorProperties method is not being called.
The regular flow should be that the container is started and then the properties are set.
The first step happens, the second does not.
I know there is an alternative for which I can do this configuration in each functional test class but I don't like it as it adds not needed noise to the test class.
For example, with a Java project that uses Postgres I managed to make it work with the following code:
import javax.sql.DataSource;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
#TestConfiguration
public class PostgresqlTestContainersConfig {
static final PostgreSQLContainer POSTGRES_CONTAINER;
private final static DockerImageName IMAGE = DockerImageName.parse("postgres").withTag("latest");
static {
POSTGRES_CONTAINER = new PostgreSQLContainer(IMAGE);
POSTGRES_CONTAINER.start();
}
#Bean
DataSource dataSource() {
return DataSourceBuilder.create()
.username(POSTGRES_CONTAINER.getUsername())
.password(POSTGRES_CONTAINER.getPassword())
.driverClassName(POSTGRES_CONTAINER.getDriverClassName())
.url(POSTGRES_CONTAINER.getJdbcUrl())
.build();
}
}
I'm trying to achieve the same thing but in Kotlin and using MongoDB.
Any idea on what may be the issue causing the #DynamicPropertySource not being called?
#DynamicPropertySource is part of the Spring-Boot context lifecycle. Since you want to replicate the Java setup in a way, it is not required to use #DynamicPropertySource. Instead you can follow the Singleton Container Pattern, and replicate it in Kotlin as well.
Instead of setting the config on the registry, you can set them as a System property and Spring Autoconfig will pick it up:
init {
MONGO_CONTAINER.start()
System.setProperty("spring.data.mongodb.uri", MONGO_CONTAINER.getReplicaSetUrl());
}
I was able to resolve similar problem in Groovy by:
Having static method annotated with #DynamicPropetySource directly in the test class (probably it would also work in superclass.
But I didn't want to copy the code into every test class that needs MongoDB.
I resolved the issue by using ApplicationContexInitializer
The example is written in groovy
class MongoTestContainer implements ApplicationContextInitializer<ConfigurableApplicationContext>{
static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6.0.2"))
#Override
void initialize(ConfigurableApplicationContext applicationContext) {
mongoDBContainer.start()
def testValues = TestPropertyValues.of("spring.data.mongodb.uri="+ mongoDBContainer.getReplicaSetUrl())
testValues.applyTo(applicationContext.getEnvironment())
}
}
To make it complete, in the test class, you just need to add #ContextConfiguration(initializers = MongoTestContainer) to activate context initializer for the test.
For this you could also create custom annotation which would combine #DataMongoTest with previous annotation.
This solution works for me.
Method with #DynamicPropertySource is inside companion object(also added #JvmStatic) and added org.testcontainers.junit.jupiter.Testcontainers on the test class
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.jdbc.DataSourceBuilder
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import javax.sql.DataSource
#ExtendWith(SpringExtension::class)
#Testcontainers
#TestConfiguration
#ContextConfiguration(classes = [PostgresqlTestContainersConfig::class])
class PostgresqlTestContainersConfig {
#Autowired
var dataSource: DataSource? = null
#Test
internal fun name() {
dataSource!!.connection.close()
}
#Bean
fun dataSource(): DataSource? {
return DataSourceBuilder.create()
.username(POSTGRES_CONTAINER.getUsername())
.password(POSTGRES_CONTAINER.getPassword())
.driverClassName(POSTGRES_CONTAINER.getDriverClassName())
.url(POSTGRES_CONTAINER.getJdbcUrl())
.build()
}
companion object {
#JvmStatic
#Container
private val POSTGRES_CONTAINER: PostgreSQLContainer<*> = PostgreSQLContainer("postgres:9.6.12")
.withDatabaseName("integration-tests-db")
.withUsername("sa")
.withPassword("sa")
#JvmStatic
#DynamicPropertySource
fun postgreSQLProperties(registry: DynamicPropertyRegistry) {
registry.add("db.url") { POSTGRES_CONTAINER.jdbcUrl }
registry.add("db.user") { POSTGRES_CONTAINER.username }
registry.add("db.password") { POSTGRES_CONTAINER.password }
}
}
}
I'm working on a project in which there is a requirement to convert single tenant application to multi tenant application. How to implement Multi-tenant support for Gateway using key cloak with spring security oauth2?
Any references please share
Use ReactiveClientRegistrationRepository to register your clients dynamically at runtime. You can then plug this implementation to a custom Webfilter, which will use your custom repository to compute the client details based on the logged in user's realm(which can be the hostname, email etc.)
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import reactor.core.publisher.Mono;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.stereotype.Component;
#Component
public class TenantClientRegistrationRepository implements ReactiveClientRegistrationRepository {
private final Map<String, String> tenants = new HashMap<>();
private final Map<String, Mono<ClientRegistration>> clients = new HashMap<>();
public TenantClientRegistrationRepository() {
this.tenants.put("tenant1", "http://localhost:8080/realms/tenant1");
this.tenants.put("tenant2", "http://localhost:8080/realms/tenant2");
}
#Override
public Mono<ClientRegistration> findByRegistrationId(String registrationId) {
return this.clients.computeIfAbsent(registrationId, this::fromTenant);
}
private Mono<ClientRegistration> fromTenant(String registrationId) {
return Optional.ofNullable(this.tenants.get(registrationId))
.map(uri -> Mono.defer(() -> clientRegistration(uri, registrationId)).cache())
.orElse(Mono.error(new IllegalArgumentException("unknown tenant")));
}
private Mono<ClientRegistration> clientRegistration(String uri, String registrationId) {
return Mono.just(ClientRegistrations.fromIssuerLocation(uri)
.registrationId(registrationId)
.clientId("web-client")//fetch client creds via rest or some other means
.clientSecret("********")
.scope("openid")
.build());
}
#KafkaListener(topics="tenants")
//TODO: Here we will populate tenants(realm) and respective client details based on tenant creation event
public void action(Map<String, Map<String, Object>> action) {
if (action.containsKey("created")) {
Map<String, Object> tenant = action.get("created");
String alias = (String) tenant.get("alias");
String issuerUri = (String) tenant.get("issuerUri");
this.tenants.put(alias, issuerUri);
this.clients.remove(alias);
}
}
}
import static org.springframework.security.config.Customizer.withDefaults;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
public class TenantFilterChain implements WebFilter, ApplicationContextAware {
private final Map<String, Mono<WebFilter>> tenants = new HashMap<>();
private final ReactiveClientRegistrationRepository clients;
private ApplicationContext context;
public TenantFilterChain(ReactiveClientRegistrationRepository clients) {
this.clients = clients;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Mono<ClientRegistration> tenant = toTenant(exchange);
Mono<WebFilter> filter = tenant.flatMap(this::fromTenant);
return Mono.zip(tenant, filter)
.flatMap(tuple -> tuple.getT2().filter(exchange, chain)
.contextWrite(Context.of(ClientRegistration.class, tuple.getT1()))
.thenReturn(exchange))
.switchIfEmpty(chain.filter(exchange).thenReturn(exchange))
.then(Mono.empty());
}
private Mono<ClientRegistration> toTenant(ServerWebExchange exchange) {
String host = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
.build().getHost();
return this.clients.findByRegistrationId(host);
}
private Mono<WebFilter> fromTenant(ClientRegistration registration) {
return this.tenants.computeIfAbsent(registration.getRegistrationId(), tenant -> {
ServerHttpSecurity http = new ContextAwareServerHttpSecurity(this.context);
OidcClientInitiatedServerLogoutSuccessHandler handler =
new OidcClientInitiatedServerLogoutSuccessHandler(this.clients);
handler.setPostLogoutRedirectUri("http://localhost:8282");
ServerAuthenticationEntryPoint entryPoint =
new RedirectServerAuthenticationEntryPoint("/oauth2/authorization/" + tenant);
// #formatter:off
http
.authorizeExchange(e -> e
.pathMatchers("/jwks").permitAll()
.anyExchange().authenticated())
.logout(l -> l.logoutSuccessHandler(handler))
.oauth2Login(withDefaults())
.exceptionHandling(e -> e.authenticationEntryPoint(entryPoint));
// #formatter:on
return Mono.just((WebFilter) new WebFilterChainProxy(http.build())).cache();
});
}
#Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context;
}
private static class ContextAwareServerHttpSecurity extends ServerHttpSecurity {
ContextAwareServerHttpSecurity(ApplicationContext context) {
super.setApplicationContext(context);
}
}
}
Referenced codebase
Talk by Josh Cummings on multitenancy with spring security OAuth2
Hope this helps !! :)
I have a springboot application which I’m hosting on my own home server. I have sql database setup on the same.
And for front end I’m planning to use android for initial testing phase then shift it to flutter.
I was wonder how do I send notifications from my spring boot to my front end application. I have seen a few articles on how to send it through fire base but I was wondering if there’s another way of achieving the same without using an external service.
I have setup my server running Ubuntu on on 3 pc which loadbalances my app and want to use one of them to send push notifications.
Please follow the below steps
Install Dependency (Gradle/ Maven)
Gradle
implementation 'com.google.firebase:firebase-admin:8.1.0'
Maven
<dependency>
<groupId>com.google.firebase</groupId>
<artifactId>firebase-admin</artifactId>
<version>8.1.0</version>
</dependency>
Add firebase-service-account.json file
../src/main/resources/firebase-service-account.json
Open MainApplication java class
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.FirebaseMessaging;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
#SpringBootApplication
public class BackendApplication {
public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
}
#Bean
FirebaseMessaging firebaseMessaging() throws IOException {
GoogleCredentials googleCredentials = GoogleCredentials
.fromStream(new ClassPathResource("firebase-service-account.json").getInputStream());
FirebaseOptions firebaseOptions = FirebaseOptions
.builder()
.setCredentials(googleCredentials)
.build();
FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "YOUR APP NAME");
return FirebaseMessaging.getInstance(app);
}
}
Create Service
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.Notification;
import org.springframework.stereotype.Service;
#Service
public class FirebaseMessagingService {
private final FirebaseMessaging firebaseMessaging;
public FirebaseMessagingService(FirebaseMessaging firebaseMessaging) {
this.firebaseMessaging = firebaseMessaging;
}
public void sendNotification(String title, String body, String token) throws FirebaseMessagingException {
Notification notification = Notification
.builder()
.setTitle(title)
.setBody(body)
.build();
Message message = Message
.builder()
.setToken(token)
.setNotification(notification)
// .putAllData(note.getData())
.build();
firebaseMessaging.send(message);
// For Send to multiple devices use Multicast Message Builder
MulticastMessage message = MulticastMessage
.builder()
.addAllTokens(<List Of Tokens>)
.setNotification(notification)
// .putAllData(note.getData())
.build();
firebaseMessaging.send(message);
}
}
Usage of service inside controller
#Autowired
private FirebaseMessagingService firebaseService;
public void sendPushMessage(){
firebaseService.sendNotification("Notification title", "Notification Text", "Receiver device token");
}
// Class CompteRepository
import org.springframework.data.jpa.repository.JpaRepository;
import org.entities.Compte;
public interface CompteRepository extends JpaRepository<Compte, String>{}
// CLASS BanqueMetierImpl``
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
#Service // SPring couche Metier
#Transactional
public class BanqueMetierImpl implements IBanqueMetier{
#Autowired
private CompteRepository compteRepository;
#Override
public Compte consulterCompte(String code) {
Compte cp = compteRepository.findOne(code);
return cp;
}
// The method findOne show up this error The method findOne(Example) in //the type QueryByExampleExecutor is not applicable for the arguments //(String)
I think the method findOne() is unsupported by version 1.5.1.SNAPSHOT of SPRING BOOT , so in 2.0.1.SNAPSHOT it's replaced by FindById() which is a QueryByExampleExecutor it's an Optional method (see Optional in JAVA 8) so I resolved the problem like this:
#Override public Compte consulterCompte(String code) throws NotFoundException {
Optional<Compte> cp = compteRepository.findById(code);
return cp.orElseThrow(
() -> new NotFoundException("Unable to get Account with Code = " + code)
);
}
I have a Spring Boot (V 1.3.5) Web Application with JSPs packaged as jar. The views are located in src/main/resources/META-INF/resources/WEB-INF/views.
See http://hillert.blogspot.lu/2016/03/spring-boot-with-jsp-in-executable-jar.html on that subject.
This works well on my Fedora dev. workstation, and as well on a colleague's with Windows 7, both using Eclipse Mars.
But on another Windows 8 PC, the same sourcecode run in Eclipse or STS, produces a 404 because no view can be found, and this starts aching my head seriously.
I append my configuration class here, maybe someone has an idea what could go wrong here.
package our.base.package;
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Profile;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.orm.hibernate4.LocalSessionFactoryBuilder;
import org.springframework.orm.jpa.vendor.HibernateJpaSessionFactoryBean;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
#Configuration
#EnableScheduling
#EnableCaching
//#EnableWebMvc
#ComponentScan(basePackages = { "our.base.package" })
public class SpringConfigRootApplication extends WebMvcConfigurerAdapter {
#Bean
public LocaleResolver localeResolver() {
final CookieLocaleResolver slr = new CookieLocaleResolver();
slr.setCookieMaxAge(86400 * 365 * 5);
slr.setCookieName("lang");
slr.setDefaultLocale(Locale.GERMAN);
return slr;
}
#Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
final LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("lang");
lci.setIgnoreInvalidLocale(true);
return lci;
}
#Override
public void addInterceptors(final InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
#Override
public void addViewControllers(final ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
}
#Bean
public InternalResourceViewResolver getInternalResourceViewResolver() {
final InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setRequestContextAttribute("requestContext");
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}
#Bean(initMethod = "init", destroyMethod = "shutdown")
#Order(value = Ordered.LOWEST_PRECEDENCE)
public Application application() {
return new Application();
}
}