register webhook tg bot using spring boot - spring

I am using telegrambots-spring-boot-starter v 5.2.0 and trying register my bot
Here's my bot config:
bot.url=https://74e437885ee9.ngrok.io
bot.path=adam
#Slf4j
#Configuration
public class BotConfig {
#Value("${bot.url}")
private String BOT_URL;
#Bean
public SetWebhook setWebhookInstance() {
return SetWebhook.builder().url(BOT_URL).build();
}
// Create it as
#Bean
public AdamSmithBot adamSmithBot(SetWebhook setWebhookInstance) throws TelegramApiException {
AdamSmithBot adamSmithBot = new AdamSmithBot(setWebhookInstance);
// DefaultWebhook defaultWebhook = new DefaultWebhook();
// defaultWebhook.setInternalUrl(BOT_URL);
// defaultWebhook.registerWebhook(adamSmithBot);
TelegramBotsApi telegramBotsApi = new TelegramBotsApi(DefaultBotSession.class);
log.info("SetWebHook from AdamSmith bot {}", adamSmithBot.getSetWebhook());
telegramBotsApi.registerBot(adamSmithBot, adamSmithBot.getSetWebhook());
return adamSmithBot;
}
}
But it dont working, but when i send this request, it working perfectly and updates recieve to me
https://api.telegram.org/MY_TOKEN_HERE/setWebhook?url=https://74e437885ee9.ngrok.io
I think my mistake in BotConfig,but i also publush my other clases bot and controller:
public class AdamSmithBot extends SpringWebhookBot {
#Value("${bot.token}")
private String TOKEN;
#Value("${bot.name}")
private String BOT_USERNAME;
#Value("${bot.path}")
private String BOT_PATH;
public AdamSmithBot(SetWebhook setWebhook) {
super(setWebhook);
}
public AdamSmithBot(DefaultBotOptions options, SetWebhook setWebhook) {
super(options, setWebhook);
}
#Override
public String getBotUsername() {
return BOT_USERNAME;
}
#Override
public String getBotToken() {
return TOKEN;
}
#Override
public BotApiMethod<?> onWebhookUpdateReceived(Update update) {
if (update.getMessage() != null && update.getMessage().hasText()) {
Long chatId = update.getMessage().getChatId();
try {
execute(new SendMessage(chatId.toString(), "HI HANDSOME " + update.getMessage().getText()));
} catch (TelegramApiException e) {
throw new IllegalStateException(e);
}
}
return null;
}
#Override
public String getBotPath() {
return "adam";
}
}
Controller:
#Slf4j
#RestController
public class WebHookBotRecieveController {
#Autowired
private AdamSmithBot adamSmithBot;
#PostMapping("/")
public void getUpdate(#RequestBody Update update){
log.info("some update recieved {}",update.toString());
adamSmithBot.onWebhookUpdateReceived(update);
}
#PostMapping("/callback/adam")
public void getUpdateWithDifferentUrl(#RequestBody Update update){
log.info("some update recieved {}",update.toString());
adamSmithBot.onWebhookUpdateReceived(update);
}
}
NOTE: I seemd some info here:
https://github.com/rubenlagus/TelegramBots/wiki/How-To-Update
They do that:
https://i.stack.imgur.com/9JKRT.png
But when i trying put DefaultWebhook class instead it produce NullPointerException
What i made wrong?
EDIT : I refactored some code
#Value("${bot.url}") private String BOT_URL; - where was null value (fixed),reloaded library, but now i have that exception:
Caused by: javax.ws.rs.ProcessingException: Failed to start Grizzly HTTP server: Cannot assign requested address: bind
at org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory.createHttpServer(GrizzlyHttpServerFactory.java:270) ~[jersey-container-grizzly2-http-2.33.jar:na]
at org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory.createHttpServer(GrizzlyHttpServerFactory.java:93) ~[jersey-container-grizzly2-http-2.33.jar:na]
at org.telegram.telegrambots.updatesreceivers.DefaultWebhook.startServer(DefaultWebhook.java:64) ~[telegrambots-5.2.0.jar:na]
at org.telegram.telegrambots.meta.TelegramBotsApi.<init>(TelegramBotsApi.java:50) ~[telegrambots-meta-5.2.0.jar:na]
at ru.website.selenium.bot.telegram.config.BotConfig.adamSmithBot(BotConfig.java:44) ~[classes/:na]
at ru.website.selenium.bot.telegram.config.BotConfig$$EnhancerBySpringCGLIB$$4eb8259d.CGLIB$adamSmithBot$0(<generated>) ~[classes/:na]
at ru.website.selenium.bot.telegram.config.BotConfig$$EnhancerBySpringCGLIB$$4eb8259d$$FastClassBySpringCGLIB$$1e185cfd.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244) ~[spring-core-5.3.8.jar:5.3.8]
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331) ~[spring-context-5.3.8.jar:5.3.8]
at ru.website.selenium.bot.telegram.config.BotConfig$$EnhancerBySpringCGLIB$$4eb8259d.adamSmithBot(<generated>) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ~[spring-beans-5.3.8.jar:5.3.8]
... 39 common frames omitted
Caused by: java.net.BindException: Cannot assign requested address: bind
at java.base/sun.nio.ch.Net.bind0(Native Method) ~[na:na]
at java.base/sun.nio.ch.Net.bind(Net.java:461) ~[na:na]
at java.base/sun.nio.ch.Net.bind(Net.java:453) ~[na:na]
at java.base/sun.nio.ch.ServerSocketChannelImpl.bind(ServerSocketChannelImpl.java:227) ~[na:na]
at java.base/sun.nio.ch.ServerSocketAdaptor.bind(ServerSocketAdaptor.java:80) ~[na:na]
at org.glassfish.grizzly.nio.transport.TCPNIOBindingHandler.bindToChannelAndAddress(TCPNIOBindingHandler.java:107) ~[grizzly-framework-2.4.4.jar:2.4.4]
at org.glassfish.grizzly.nio.transport.TCPNIOBindingHandler.bind(TCPNIOBindingHandler.java:64) ~[grizzly-framework-2.4.4.jar:2.4.4]
at org.glassfish.grizzly.nio.transport.TCPNIOTransport.bind(TCPNIOTransport.java:215) ~[grizzly-framework-2.4.4.jar:2.4.4]
at org.glassfish.grizzly.nio.transport.TCPNIOTransport.bind(TCPNIOTransport.java:195) ~[grizzly-framework-2.4.4.jar:2.4.4]
at org.glassfish.grizzly.nio.transport.TCPNIOTransport.bind(TCPNIOTransport.java:186) ~[grizzly-framework-2.4.4.jar:2.4.4]
at org.glassfish.grizzly.http.server.NetworkListener.start(NetworkListener.java:711) ~[grizzly-http-server-2.4.4.jar:2.4.4]
at org.glassfish.grizzly.http.server.HttpServer.start(HttpServer.java:256) ~[grizzly-http-server-2.4.4.jar:2.4.4]
at org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory.createHttpServer(GrizzlyHttpServerFactory.java:267) ~[jersey-container-grizzly2-http-2.33.jar:na]
... 53 common frames omitted
Process finished with exit code 0

Okay, I get it, I didn't know that TelegramBotApi runs a grizzly server underneath and that was the reason for my mistakes
For those who ever come here with the same problem, I will describe the solution :
Let's start with the fact that for the test on localhost we need a public address , e.g. ngrok , then we have to do the following, start grizzli on , let's say port 80 (localhost), specify the correct ngrok url , and then run spring boot application on , let's say port 8080, examples in the code
For ex. ngrok forwarding:
Forwarding http://b44ecce72666.ngrok.io -> http://localhost:8080
Forwarding https://b44ecce72666.ngrok.io -> http://localhost:8080
#Slf4j
#Configuration
public class BotConfig {
// #Value("${bot.url}")
// #NotNull
// #NotEmpty
// private String BOT_URL;
#Value("${bot.token}")
#NotNull
#NotEmpty
private String TOKEN;
#Value("${bot.name}")
#NotNull
#NotEmpty
private String BOT_USERNAME;
#Bean
public SetWebhook setWebhookInstance() {
return SetWebhook.builder().url("https://b44ecce72666.ngrok.io").build();
} // public address, now it is ngrok, in the future it will (i think) be the server address
// Create it as
#Bean
public AdamSmithBot adamSmithBot(SetWebhook setWebhookInstance) throws TelegramApiException {
AdamSmithBot adamSmithBot = new AdamSmithBot(setWebhookInstance);
adamSmithBot.setBOT_USERNAME(BOT_USERNAME);
adamSmithBot.setTOKEN(TOKEN);
adamSmithBot.setBOT_PATH("adam");
DefaultWebhook defaultWebhook = new DefaultWebhook();
defaultWebhook.setInternalUrl(
"http://localhost:80"); // the port to start the server, on the localhost computer, on the server it
// be the server address
// defaultWebhook.registerWebhook(adamSmithBot);
TelegramBotsApi telegramBotsApi = new TelegramBotsApi(DefaultBotSession.class, defaultWebhook);
log.info("SetWebHook from AdamSmith bot {}", setWebhookInstance);
adamSmithBot.getBotUsername();
telegramBotsApi.registerBot(adamSmithBot, setWebhookInstance);
return adamSmithBot;
}
}
Next is code of webhook bot:
#Slf4j
#Setter
public class AdamSmithBot extends SpringWebhookBot {
#Value("${bot.token}")
private String TOKEN;
#Value("${bot.name}")
private String BOT_USERNAME;
#Value("${bot.path}")
private String BOT_PATH;
#Autowired
private MainService mainService;
public AdamSmithBot(SetWebhook setWebhook) {
super(setWebhook);
}
public AdamSmithBot(DefaultBotOptions options, SetWebhook setWebhook) {
super(options, setWebhook);
}
#Override
public String getBotUsername() {
log.info("BOT_USERNAME FROM ADAM BOT {}",BOT_USERNAME);
return BOT_USERNAME;
}
#Override
public String getBotToken() {
return TOKEN;
}
#Override
public BotApiMethod<?> onWebhookUpdateReceived(Update update) { //All messages coming from the grizzly server will trigger this method
log.info("Message teext {}",update.toString());
if (update.getMessage() != null && update.getMessage().hasText()) {
Long chatId = update.getMessage().getChatId();
List<PartialBotApiMethod<Message>> listOfCommands= mainService.receiveUpdate(update);
listOfCommands.forEach(x->{
try {
if(x instanceof SendMessage){
execute((SendMessage)x);
}
if(x instanceof SendPhoto){
execute((SendPhoto) x);
}
} catch (TelegramApiException e) {
e.printStackTrace();
}
});
}
return null;
}
#Override
public String getBotPath() {
return "adam"; //any other path here
}
}
My code is excessive in some places, and too fancy, but it's not hard to figure out. If you have any suggestions on how to make it better, I'd love to hear them.

This was the most helpful question and answer from #handsome so decided to post here some more additional info that was not so evident for me at the beginning.
#Configuration
public class BotConfig {
#Value("${telegram.webhook-host}")
private String webhookHost;
#Value("${telegram.endpoint}")
private String botEndpoint;
#Value("${telegram.token}")
private String botToken;
#Value("${telegram.username}")
private String botUsername;
#Bean
public SetWebhook setWebhookInstance() {
return SetWebhook.builder().url(webhookHost).build();
}
#Bean
public ParserBot getBot(SetWebhook setWebhook) {
ParserBot parserBot = new ParserBot(setWebhook);
parserBot.setBotPath(botEndpoint);
parserBot.setBotToken(botToken);
parserBot.setBotUsername(botUsername);
try {
DefaultWebhook defaultWebhook = new DefaultWebhook();
defaultWebhook.setInternalUrl("http://localhost:80");
TelegramBotsApi telegramBotsApi = new TelegramBotsApi(DefaultBotSession.class, defaultWebhook);
telegramBotsApi.registerBot(parserBot, parserBot.getSetWebhook());
} catch (TelegramApiException e) {
throw new RuntimeException("Can't register telegram bot", e);
}
return parserBot;
}
}
Most of the info is clear but sometimes it is necessary to say:
You can register webhook endpoint manually by calling https://api.telegram.org/bot<your_token>/setWebHook?url=<your_url>
In this case your URL can be any path you want. If you register your webhook endpoint using TelegramBotsApi, then your endpoint will look like https://host/callback/ , as example
telegram.webhook-host=https://12345124.eu.ngrok.io
telegram.endpoint="webhook"
then your endpoint is https://12345124.eu.ngrok.io/callback/webhook
Yes, you can't set any path to your endpoint but you don't need to set it manually
Another thing. To activate WebhookBot in application you have to use DefaultWebhook to activate TelegramBotsApi.useWebhook property. This is according to source.
Your endpoint have to be https. It can't be http.
Webhook can be set up only on ports 80, 88, 443 or 8443

Related

Spring boot & mongodb, domain class issue with findAll() and findById()

I am implementing a spring boot service with an underlying mongodb database for data storage.
In this service I have to use some domain classes that comes from another library which I can not alter. Unfortunately these classes have instance variables marked as final. For example here is the Discount domain class:
#Value
#NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
#AllArgsConstructor(access = AccessLevel.PRIVATE)
#SuperBuilder(toBuilder = true)
#JsonDeserialize(builder = Discount.DiscountBuilderImpl.class)
#EqualsAndHashCode(callSuper = true)
#ToString(callSuper = true)
public class Discount extends AbstractEntityBase {
#NonNull
UUID id;
#NonNull
String no;
#JsonProperty("iNo")
Integer iNo;
#NonNull
LocalizedTexts designation;
LocalizedTexts printText;
boolean isAutomatic;
LocalDate validFromDate;
LocalDate validToDate;
LocalTime validFromTime;
LocalTime validToTime;
String checkScriptParameters;
ReferenceScript checkScript;
#NonNull
ReferenceScript calculationScript;
public static DiscountBuilder<?, ?> builder(UUID id,
String no,
LocalizedTexts designation,
ReferenceScript calculationScript,
boolean isAutomatic) {
return new DiscountBuilderImpl()
.id(id)
.no(no)
.designation(designation)
.calculationScript(calculationScript)
.isAutomatic(isAutomatic);
}
/**
* Overwritten getters for optional properties
*/
...
#JsonPOJOBuilder(withPrefix = "")
#JsonIgnoreProperties(ignoreUnknown = true)
public static final class DiscountBuilderImpl
extends DiscountBuilder<Discount, DiscountBuilderImpl> {
}
}
In my test class I am testing save and find (findAll, findById) operations. Save operations work fine but I have issues with findAll and findById methods. Here is such a test class:
#RunWith(SpringRunner.class)
#DataMongoTest
public class TestDiscountRepository {
#Autowired
DiscountRepository discountRepository;
#Before
public void init(){
}
#After
public void resetMongoDb() {
//discountRepository.deleteAll();
}
#Test
public void save_success() {
UUID id = UUID.randomUUID();
...
Discount discount = Discount.builder(id, no, designation, calculationScript, true)
.iNo(iNo)
.printText(printText)
.isAutomatic(isAutomatic)
.validFromDate(validFromDate)
.validToDate(validToDate)
.validFromTime(validFromTime)
.validToTime(validToTime)
.checkScriptParameters(checkScriptParameters)
.checkScript(checkScript)
.build();
Discount savedDiscount = discountRepository.save(discount);
assertThat(savedDiscount).isNotNull();
assertThat(savedDiscount.getId()).isNotNull();
}
#Test
public void save_bulk_success() {
UUID id1 = UUID.randomUUID();
...
UUID id2 = UUID.randomUUID();
...
UUID id3 = UUID.randomUUID();
...
List<Discount> discounts = Arrays.asList(
Discount.builder(id1, no1, designation1, calculationScript1, true)
...
.build(),
Discount.builder(id2, no2, designation2, calculationScript2, true)
...
.build(),
Discount.builder(id3, no3, designation3, calculationScript3, true)
...
.build()
);
List<Discount> allDiscounts = discountRepository.saveAll(discounts);
AtomicInteger validIdFound = new AtomicInteger();
allDiscounts.forEach(discount -> {
if(discount.getId() != null) {
validIdFound.getAndIncrement();
}
});
assertThat(validIdFound.intValue()).isEqualTo(3);
}
#Test
public void find_all_success() {
List<Discount> discounts = discountRepository.findAll();
Assert.assertNotNull(discounts);
}
#Test
public void find_by_id_success() {
UUID id = UUID.randomUUID();
String no = "900";
...
Discount discount = Discount.builder(id, no, designation, calculationScript, true)
...
.build();
Discount savedDiscount = discountRepository.save(discount);
Optional<Discount> result = discountRepository.findById(savedDiscount.getId());
Assert.assertNotNull(result.get());
}
}
And here is the exception I am getting when I try to run/test findAll() and findById():
java.lang.IllegalStateException: Cannot set property id because no setter, no wither and it's not part of the persistence constructor private net.mspos.possible.svc_pos_controller_data.entity.Discount()!
at org.springframework.data.mapping.model.InstantiationAwarePropertyAccessor.setProperty(InstantiationAwarePropertyAccessor.java:118)
at org.springframework.data.mapping.model.ConvertingPropertyAccessor.setProperty(ConvertingPropertyAccessor.java:64)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readAndPopulateIdentifier(MappingMongoConverter.java:450)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.populateProperties(MappingMongoConverter.java:418)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:394)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readDocument(MappingMongoConverter.java:356)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:292)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:288)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:107)
at org.springframework.data.mongodb.core.MongoTemplate$ReadDocumentCallback.doWith(MongoTemplate.java:3207)
at org.springframework.data.mongodb.core.MongoTemplate.executeFindOneInternal(MongoTemplate.java:2822)
at org.springframework.data.mongodb.core.MongoTemplate.doFindOne(MongoTemplate.java:2529)
at org.springframework.data.mongodb.core.MongoTemplate.doFindOne(MongoTemplate.java:2499)
at org.springframework.data.mongodb.core.MongoTemplate.findById(MongoTemplate.java:888)
at org.springframework.data.mongodb.repository.support.SimpleMongoRepository.findById(SimpleMongoRepository.java:132)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:289)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:137)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:121)
at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:529)
at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:639)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:163)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:138)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
at jdk.proxy2/jdk.proxy2.$Proxy93.findById(Unknown Source)
at net.mspos.possible.svc_pos_controller_data.repository.TestDiscountRepository.find_by_id_success(TestDiscountRepository.java:209)
Here is also how a document into the database looks like:
Any ideas on how I could overcome this issue? Unfortunately altering the domain classes is not an option.
Finally the easiest and more convenient solution in my case is to avoid using repositories to map the domain classes and provide basic CRUD operations.
Instead of that I am using the MongoCollection interface alongside with ObjectMapper to serialize/deserialize objects into mongodb documents for all CRUD operations. For instance a relevant service for a domain class could look like:
#Service
#EnableConfigurationProperties(MongoProperties.class)
public class DiscountMongoService implements BasicMongoService {
private MongoProperties mongoProperties;
private CustomMongoConfig customMongoConfig;
private BusinessEntityMapper mapper;
private MongoCollection<Document> discountCollection;
#Autowired
public DiscountMongoService(MongoProperties mongoProperties, CustomMongoConfig customMongoConfig, BusinessEntityMapper mapper) {
this.mongoProperties = mongoProperties;
this.customMongoConfig = customMongoConfig;
this.mapper = mapper;
discountCollection =
this.customMongoConfig.mongoClient().getDatabase(this.mongoProperties.getDatabase()).getCollection(Discount.class.getSimpleName());
}
// Insert
public boolean insertOne(Discount discount) throws JsonProcessingException {
Document discountDoc = transformEntityIntoDocumentWithMongodbId(discount);
InsertOneResult result = discountCollection.insertOne(discountDoc);
System.out.println(result);
return result.wasAcknowledged();
}
....
// Find
public List<Discount> findAll() {
List<Discount> discounts = new ArrayList<>();
discountCollection.find().forEach(dd -> {
Discount discount;
try {
discount = (Discount) mapper.getBusinessEntityFromJSON(dd.toJson(),Discount.class);
discounts.add(discount);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
});
return discounts;
}

more than one 'primary' service instance suppliers found during load balancing (spring boot/cloud)

I'm currently updating from Spring boot 2.2.x to 2.6.x + legacy code, it's a big jump so there were multiple changes. I'm now running into a problem with load balancing through an api-gateway. I'll apologize in advance for the wall of code to come. I will put the point of failure at the bottom.
When I send in an API request, I get the following error:
more than one 'primary' bean found among candidates: [zookeeperDiscoveryClientServiceInstanceListSupplier, serviceInstanceListSupplier, retryAwareDiscoveryClientServiceInstanceListSupplier]
it seems that the zookeeperDiscovery and retryAware suppliers are loaded through the default serviceInsatnceListSupplier, which has #Primary over it. I thought would take precedence over the other ones. I assume I must be doing something wrong due changes in the newer version, here are the relevant code in question:
#Configuration
#LoadBalancerClients(defaultConfiguration = ClientConfiguration.class)
public class WebClientConfiguration {
#Bean
#Qualifier("microserviceWebClient")
#ConditionalOnMissingBean(name = "microserviceWebClient")
public WebClient microserviceWebClient(#Qualifier("microserviceWebClientBuilder") WebClient.Builder builder) {
return builder.build();
}
#Bean
#Qualifier("microserviceWebClientBuilder")
#ConditionalOnMissingBean(name = "microserviceWebClientBuilder")
#LoadBalanced
public WebClient.Builder microserviceWebClientBuilder() {
return WebClient.builder();
}
#Bean
#Primary
public ReactorLoadBalancerExchangeFilterFunction reactorLoadBalancerExchangeFilterFunction(
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory) {
//the transformer is currently null, there wasn't a transformer before the upgrade
return new CustomExchangeFilterFunction(loadBalancerFactory, transformer);
}
}
There are also some Feign Client related configs here which I will omit, since it's not (or shouldn't be) playing a role in this problem:
public class ClientConfiguration {
/**
* The property key within the feign clients configuration context for the feign client name.
*/
public static final String FEIGN_CLIENT_NAME_PROPERTY = "feign.client.name";
public ClientConfiguration() {
}
//Creates a new BiPredicate for shouldClose. This will be used to determine if HTTP Connections should be automatically closed or not.
#Bean
#ConditionalOnMissingBean
public BiPredicate<Response, Type> shouldClose() {
return (Response response, Type type) -> {
if(type instanceof Class) {
Class<?> currentClass = (Class<?>) type;
return (null == AnnotationUtils.getAnnotation(currentClass, EnableResponseStream.class));
}
return true;
};
}
//Creates a Custom Decoder
#Bean
public Decoder createCustomDecoder(
ObjectFactory<HttpMessageConverters> converters, BiPredicate<Response, Type> shouldClose
) {
return new CustomDecoder(converters, shouldClose);
}
#Bean
#Qualifier("loadBalancerName")
public String loadBalancerName(PropertyResolver propertyResolver) {
String name = propertyResolver.getProperty(FEIGN_CLIENT_NAME_PROPERTY);
if(StringUtils.hasText(name)) {
// we are in a feign context
return name;
}
// we are in a LoadBalancerClientFactory context
name = propertyResolver.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
Assert.notNull(name, "Could not find a load balancer name within the configuration context!");
return name;
}
#Bean
public ReactorServiceInstanceLoadBalancer reactorServiceInstanceLoadBalancer(
BeanFactory beanFactory, #Qualifier("loadBalancerName") String loadBalancerName
) {
return new CustomRoundRobinLoadBalancer(
beanFactory.getBeanProvider(ServiceInstanceListSupplier.class),
loadBalancerName
);
}
#Bean
#Primary
public ServiceInstanceListSupplier serviceInstanceListSupplier(
#Qualifier(
"filter"
) Predicate<ServiceInstance> filter, DiscoveryClient discoveryClient, Environment environment, #Qualifier(
"loadBalancerName"
) String loadBalancerName
) {
// add service name to environment if necessary
if(environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME) == null) {
StandardEnvironment wrapped = new StandardEnvironment();
if(environment instanceof ConfigurableEnvironment) {
((ConfigurableEnvironment) environment).getPropertySources()
.forEach(s -> wrapped.getPropertySources().addLast(s));
}
Map<String, Object> additionalProperties = new HashMap<>();
additionalProperties.put(LoadBalancerClientFactory.PROPERTY_NAME, loadBalancerName);
wrapped.getPropertySources().addLast(new MapPropertySource(loadBalancerName, additionalProperties));
environment = wrapped;
}
return new FilteringInstanceListSupplier(filter, discoveryClient, environment);
}
}
There was a change in the ExchangeFilter constructor, but as far as I can tell, it accepts that empty transformer,I don't know if it's supposed to:
public class CustomExchangeFilterFunction extends ReactorLoadBalancerExchangeFilterFunction {
private static final ThreadLocal<ClientRequest> REQUEST_HOLDER = new ThreadLocal<>();
//I think it's wrong but I don't know what to do here
private static List<LoadBalancerClientRequestTransformer> transformersList;
private final Factory<ServiceInstance> loadBalancerFactory;
public CustomExchangeFilterFunction (Factory<ServiceInstance> loadBalancerFactory) {
this(loadBalancerFactory);
///according to docs, but I don't know where and if I need to use this
#Bean
public LoadBalancerClientRequestTransformer transformer() {
return new LoadBalancerClientRequestTransformer() {
#Override
public ClientRequest transformRequest(ClientRequest request, ServiceInstance instance) {
return ClientRequest.from(request)
.header(instance.getInstanceId())
.build();
}
};
}
public CustomExchangeFilterFunction (Factory<ServiceInstance> loadBalancerFactory, List<LoadBalancerClientRequestTransformer> transformersList) {
super(loadBalancerFactory, transformersList); //the changed constructor
this.loadBalancerFactory = loadBalancerFactory;;
}
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
// put the current request into the thread context - ugly, but couldn't find a better way to access the request within
// the choose method without reimplementing nearly everything
REQUEST_HOLDER.set(request);
try {
return super.filter(request, next);
} finally {
REQUEST_HOLDER.remove();
}
}
//used to be an override, but the function has changed
//code execution doesn't even get this far yet
protected Mono<Response<ServiceInstance>> choose(String serviceId) {
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerFactory.getInstance(serviceId);
if(loadBalancer == null) {
return Mono.just(new EmptyResponse());
}
ClientRequest request = REQUEST_HOLDER.get();
// this might be null, if the underlying implementation changed and this method is no longer executed in the same
// thread
// as the filter method
Assert.notNull(request, "request must not be null, underlying implementation seems to have changed");
return choose(loadBalancer, filter);
}
protected Mono<Response<ServiceInstance>> choose(
ReactiveLoadBalancer<ServiceInstance> loadBalancer,
Predicate<ServiceInstance> filter
) {
return Mono.from(loadBalancer.choose(new DefaultRequest<>(filter)));
}
}
There were pretty big changes in the CustomExchangeFilterFunction, but the current execution doesn't even get there. It fails here, in .getIfAvailable(...):
public class CustomRoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final int DEFAULT_SEED_POSITION = 1000;
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final String serviceId;
private final int seedPosition;
private final AtomicInteger position;
private final Map<String, AtomicInteger> positionsForVersions = new HashMap<>();
public CustomRoundRobinLoadBalancer (
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId
) {
this(serviceInstanceListSupplierProvider, serviceId, new Random().nextInt(DEFAULT_SEED_POSITION));
}
public CustomRoundRobinLoadBalancer (
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId,
int seedPosition
) {
Assert.notNull(serviceInstanceListSupplierProvider, "serviceInstanceListSupplierProvider must not be null");
Assert.notNull(serviceId, "serviceId must not be null");
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.serviceId = serviceId;
this.seedPosition = seedPosition;
this.position = new AtomicInteger(seedPosition);
}
#Override
// we have no choice but to use the raw type Request here, because this method overrides another one with this signature
public Mono<Response<ServiceInstance>> choose(#SuppressWarnings("rawtypes") Request request) {
//fails here!
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get().next().map((List<ServiceInstance> instances) -> getInstanceResponse(instances, request));
}
}
Edit: after some deeper stacktracing, it seems that it does go into the CustomFilterFunction and invokes the constructor with super(loadBalancerFactory, transformer)
I found the problem or a workaround. I was using #LoadBalancerClients because I thought it would just set the same config for all clients that way (even if I technically only have one atm). I changed it to ##LoadBalancerClient and it suddenly worked. I don't quite understand why this made a difference but it did!

RequestHandlerRetryAdvice cannot be made to work with Ftp.outboundGateway in Spring Integration

My situation is similar to the one described in this SO question. The difference being that I don't use a WebFlux.outboundGateway but an Ftp.outboundGateway on which I call an AbstractRemoteFileOutboundGateway.Command.GETcommand, the common problem being that I can't get the defined RequestHandlerRetryAdvice to be used.
The configuration looks like this (stripped down to the relevant parts):
#RestController
#RequestMapping( value = "/somepath" )
public class DownloadController
{
private DownloadGateway downloadGateway;
public DownloadController( DownloadGateway downloadGateway )
{
this.downloadGateway = downloadGateway;
}
#PostMapping( "/downloads" )
public void download( #RequestParam( "filename" ) String filename )
{
Map<String, Object> headers = new HashMap<>();
downloadGateway.triggerDownload( filename, headers );
}
}
#MessagingGateway
public interface DownloadGateway
{
#Gateway( requestChannel = "downloadFiles.input" )
void triggerDownload( Object value, Map<String, Object> headers );
}
#Configuration
#EnableIntegration
public class FtpDefinition
{
private FtpProperties ftpProperties;
public FtpDefinition( FtpProperties ftpProperties )
{
this.ftpProperties = ftpProperties;
}
#Bean
public DirectChannel gatewayDownloadsOutputChannel()
{
return new DirectChannel();
}
#Bean
public IntegrationFlow downloadFiles( RemoteFileOutboundGatewaySpec<FTPFile, FtpOutboundGatewaySpec> getRemoteFile )
{
return f -> f.handle( getRemoteFile, getRetryAdvice() )
.channel( "gatewayDownloadsOutputChannel" );
}
private Consumer<GenericEndpointSpec<AbstractRemoteFileOutboundGateway<FTPFile>>> getRetryAdvice()
{
return e -> e.advice( ( (Supplier<RequestHandlerRetryAdvice>) () -> {
RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice();
advice.setRetryTemplate( getRetryTemplate() );
return advice;
} ).get() );
}
private RetryTemplate getRetryTemplate()
{
RetryTemplate result = new RetryTemplate();
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod( 5000 );
result.setBackOffPolicy( backOffPolicy );
return result;
}
#Bean
public RemoteFileOutboundGatewaySpec<FTPFile, FtpOutboundGatewaySpec> getRemoteFile( SessionFactory sessionFactory )
{
return
Ftp.outboundGateway( sessionFactory,
AbstractRemoteFileOutboundGateway.Command.GET,
"payload" )
.fileExistsMode( FileExistsMode.REPLACE )
.localDirectoryExpression( "'" + ftpProperties.getLocalDir() + "'" )
.autoCreateLocalDirectory( true );
}
#Bean
public SessionFactory<FTPFile> ftpSessionFactory()
{
DefaultFtpSessionFactory sessionFactory = new DefaultFtpSessionFactory();
sessionFactory.setHost( ftpProperties.getServers().get( 0 ).getHost() );
sessionFactory.setPort( ftpProperties.getServers().get( 0 ).getPort() );
sessionFactory.setUsername( ftpProperties.getServers().get( 0 ).getUser() );
sessionFactory.setPassword( ftpProperties.getServers().get( 0 ).getPassword() );
return sessionFactory;
}
}
#SpringBootApplication
#EnableIntegration
#IntegrationComponentScan
public class FtpTestApplication {
public static void main(String[] args) {
SpringApplication.run( FtpTestApplication.class, args );
}
}
#Configuration
#PropertySource( "classpath:ftp.properties" )
#ConfigurationProperties( prefix = "ftp" )
#Data
public class FtpProperties
{
#NotNull
private String localDir;
#NotNull
private List<Server> servers;
#Data
public static class Server
{
#NotNull
private String host;
#NotNull
private int port;
#NotNull
private String user;
#NotNull
private String password;
}
}
The Controller is mostly just there for testing purposes, in the actual implementation there's a poller. My FtpProperties hold a list of servers because in the actual implementation, I use a DelegatingSessionFactory to pick an instance based on some parameters.
According to Gary Russell's comment, I'd expect a failed download to be retried. But if I interrupt a download server-side (by issuing "Kick user" in a FileZilla instance), I just get an immediate stack trace and no retries:
org.apache.commons.net.ftp.FTPConnectionClosedException: FTP response 421 received. Server closed connection.
[...]
I also need to upload files, for which I use an Ftp.outboundAdapter. In this case and with the same RetryTemplate, if I interrupt an upload server-side, Spring Integration performs two more attempts with a delay of 5s each, and only then logs java.net.SocketException: Connection reset, all as expected.
I tried to debug a little and noticed that right before the first attempt to upload through the Ftp.outboundAdapter, a breakpoint on RequestHandlerRetryAdvice.doInvoke() is hit. But when downloading through the Ftp.outboundGateway, that breakpoint is never hit.
Is there a problem with my configuration, could someone get the RequestHandlerRetryAdvice to work with Ftp.outboundGateway/AbstractRemoteFileOutboundGateway.Command.GET?
Sorry for the delay; we are at SpringOne Platform this week.
The problem is due to the fact that the gateway spec is a bean - the gateway ends up being initialized before the advice is applied.
I changed your code like this...
#Bean
public IntegrationFlow downloadFiles(SessionFactory<FTPFile> sessionFactory) {
return f -> f.handle(getRemoteFile(sessionFactory), getRetryAdvice())
.channel("gatewayDownloadsOutputChannel");
}
...
private RemoteFileOutboundGatewaySpec<FTPFile, FtpOutboundGatewaySpec> getRemoteFile(SessionFactory<FTPFile> sessionFactory) {
return Ftp.outboundGateway(sessionFactory,
AbstractRemoteFileOutboundGateway.Command.GET,
"payload")
.fileExistsMode(FileExistsMode.REPLACE)
.localDirectoryExpression("'/tmp'")
.autoCreateLocalDirectory(true);
}
...and it worked.
It's generally better to not deal with Specs directly and just have them inlined in the flow definition...
#Bean
public IntegrationFlow downloadFiles(SessionFactory<FTPFile> sessionFactory) {
return f -> f.handle(Ftp.outboundGateway(sessionFactory,
AbstractRemoteFileOutboundGateway.Command.GET,
"payload")
.fileExistsMode(FileExistsMode.REPLACE)
.localDirectoryExpression("'/tmp'")
.autoCreateLocalDirectory(true), getRetryAdvice())
.channel("gatewayDownloadsOutputChannel");
}

Unit test failing for custom processor's 'optional' properties

To create a custom processor, I followed the documentation.
I made the necessary code changes in the MyProcessor.java and the MyProcessorTest runs fine except when I try to use some 'optional' properties. Note : I tried all the builder methods like required(false), addValidator() etc. for the optional properties, in vain. Actually, a validator doesn't make sense for an optional property ...
MyProcessor.java
#Tags({ "example" })
#CapabilityDescription("Provide a description")
#SeeAlso({})
#ReadsAttributes({ #ReadsAttribute(attribute = "", description = "") })
#WritesAttributes({ #WritesAttribute(attribute = "", description = "") })
#Stateful(description = "After a db-level LSN is processed, the same should be persisted as the last processed LSN", scopes = { Scope.CLUSTER })
public class MyProcessor extends AbstractProcessor {
public static final Relationship REL_SUCCESS = new Relationship.Builder()
.name("success")
.description(
"Successfully created FlowFile from SQL query result set.")
.build();
public static final Relationship REL_FAILURE = new Relationship.Builder()
.name("failure").description("SQL query execution failed. ???")
.build();
/* Start : Mandatory properties */
public static final PropertyDescriptor DBCP_SERVICE = new PropertyDescriptor.Builder()
.name("Database Connection Pooling Service")
.description(
"The Controller Service that is used to obtain connection to database")
.required(true).identifiesControllerService(DBCPService.class)
.build();
public static final PropertyDescriptor CONTAINER_DB = new PropertyDescriptor.Builder()
.name("containerDB").displayName("Container Database")
.description("The name of the container database").required(true)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR).build();
...
...more mandatory properties
...
/* End : Mandatory properties */
/*Start : Optional properties */
public static final PropertyDescriptor CDC_TS_FROM = new PropertyDescriptor.Builder()
.name("cdcTSFrom").displayName("Load CDC on or after")
.description("The CDC on or after this datetime will be fetched.")
.required(false).defaultValue(null).build();
public static final PropertyDescriptor SCHEMA = new PropertyDescriptor.Builder()
.name("schema").displayName("DB Schema")
.description("The schema which contains the xxxxxx")
.defaultValue(null).required(false).build();
/*End : Optional properties */
private List<PropertyDescriptor> descriptors;
private Set<Relationship> relationships;
#Override
protected void init(final ProcessorInitializationContext context) {
final List<PropertyDescriptor> descriptors = new ArrayList<PropertyDescriptor>();
descriptors.add(CONTAINER_DB);
descriptors.add(DBCP_SERVICE);
...
...
...
descriptors.add(CDC_TS_FROM);
descriptors.add(SCHEMA);
...
...
...
this.descriptors = Collections.unmodifiableList(descriptors);
final Set<Relationship> relationships = new HashSet<Relationship>();
relationships.add(REL_FAILURE);
relationships.add(REL_SUCCESS);
this.relationships = Collections.unmodifiableSet(relationships);
}
#Override
public Set<Relationship> getRelationships() {
return this.relationships;
}
#Override
public final List<PropertyDescriptor> getSupportedPropertyDescriptors() {
return descriptors;
}
// TODO : Check if the component lifecycle methods esp. onScheduled() and
// onShutDown() are required
#Override
public void onTrigger(final ProcessContext context,
final ProcessSession session) throws ProcessException {
...
...
...
}
}
MyProcessorTest.java
public class MyProcessorTest {
private TestRunner testRunner;
private final String CONTAINER_DB = "test";
private final String DBCP_SERVICE = "test_dbcp";
...
...
...
private final String SCHEMA = "dbo";
private final String CDC_TS_FROM = "";
...
...
...
#Before
public void init() throws InitializationException {
testRunner = TestRunners.newTestRunner(MyProcessor.class);
final DBCPService dbcp = new DBCPServiceSQLServerImpl(...);
final Map<String, String> dbcpProperties = new HashMap<>();
testRunner = TestRunners.newTestRunner(MyProcessor.class);
testRunner.addControllerService(DBCP_SERVICE, dbcp, dbcpProperties);
testRunner.enableControllerService(dbcp);
testRunner.assertValid(dbcp);
testRunner.setProperty(MyProcessor.DBCP_SERVICE, DBCP_SERVICE);
testRunner.setProperty(MyProcessor.CONTAINER_DB, CONTAINER_DB);
...
...
...
testRunner.setProperty(MyProcessor.CDC_TS_FROM, CDC_TS_FROM);
testRunner.setProperty(MyProcessor.SCHEMA, SCHEMA);
...
...
...
}
#Test
public void testProcessor() {
testRunner.run();
}
/**
* Simple implementation only for MyProcessor processor testing.
*/
private class DBCPServiceSQLServerImpl extends AbstractControllerService
implements DBCPService {
private static final String SQL_SERVER_CONNECT_URL = "jdbc:sqlserver://%s;database=%s";
private String containerDB;
private String password;
private String userName;
private String dbHost;
public DBCPServiceSQLServerImpl(String containerDB, String password,
String userName, String dbHost) {
super();
this.containerDB = containerDB;
this.password = password;
this.userName = userName;
this.dbHost = dbHost;
}
#Override
public String getIdentifier() {
return DBCP_SERVICE;
}
#Override
public Connection getConnection() throws ProcessException {
try {
Connection connection = DriverManager.getConnection(String
.format(SQL_SERVER_CONNECT_URL, dbHost, containerDB),
userName, password);
return connection;
} catch (final Exception e) {
throw new ProcessException("getConnection failed: " + e);
}
}
}
}
Now if I comment the optional properties in the test class :
//testRunner.setProperty(MyProcessor.CDC_TS_FROM, CDC_TS_FROM);
//testRunner.setProperty(MyProcessor.SCHEMA, SCHEMA);
, the test completes normally but if I enable any or all of the optional properties, say, CDC_TS_FROM, then I the test case assertion fails, no matter what value I put for CDC_TS_FROM :
java.lang.AssertionError: Processor has 1 validation failures:
'cdcTSFrom' validated against '' is invalid because 'cdcTSFrom' is not a supported property
at org.junit.Assert.fail(Assert.java:88)
at org.apache.nifi.util.MockProcessContext.assertValid(MockProcessContext.java:251)
at org.apache.nifi.util.StandardProcessorTestRunner.run(StandardProcessorTestRunner.java:161)
at org.apache.nifi.util.StandardProcessorTestRunner.run(StandardProcessorTestRunner.java:152)
at org.apache.nifi.util.StandardProcessorTestRunner.run(StandardProcessorTestRunner.java:147)
at org.apache.nifi.util.StandardProcessorTestRunner.run(StandardProcessorTestRunner.java:142)
at org.apache.nifi.util.StandardProcessorTestRunner.run(StandardProcessorTestRunner.java:137)
at processors.NiFiCDCPoC.sqlserver.MyProcessorTest.testProcessor(MyProcessorTest.java:74)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
Edit-1 :
I added two(?) validators :
public static final PropertyDescriptor CDC_TS_FROM = new PropertyDescriptor.Builder()
.name("cdcTSFrom").displayName("Load CDC on or after")
.description("The CDC on or after this datetime will be fetched.")
.required(false).defaultValue(null).addValidator(Validator.VALID)
.addValidator(StandardValidators.TIME_PERIOD_VALIDATOR).build();
Error :
java.lang.AssertionError: Processor has 1 validation failures:
'cdcTSFrom' validated against '2017-03-06 10:00:00' is invalid because Must be of format <duration> <TimeUnit> where <duration> is a non-negative integer and TimeUnit is a supported Time Unit, such as: nanos, millis, secs, mins, hrs, days
All Property Descriptors (required or optional) must have a Validator set explicitly, otherwise it will return the error you are seeing. It appears you are not looking to perform validation, but you still must set a validator, so on your optional properties add the following to the builder:
.addValidator(Validator.VALID)
EDIT (see comments below): Marking the PropertyDescriptor as required(false) allows it to be an optional property and thus can have no value specified. If the user enters a value, and you want to validate that against certain rules, you can add that particular Validator (or write your own and add that). For a Time Period (2 seconds, e.g.), and for other cases, there are a set of built-in validators, for example allowing only values between 2 and 20 seconds:
.addValidator(StandardValidators.createTimePeriodValidator(
2, TimeUnit.SECONDS, 20, TimeUnit.SECONDS
))

Can't evict from Spring Cache in Guice DI app

I'm trying to use String Cache abstraction mechanism with guice modules.
I've created interceptors:
CacheManager cacheManager = createCacheManager();
bind(CacheManager.class).toInstance(cacheManager);
AppCacheInterceptor interceptor = new AppCacheInterceptor(
cacheManager,
createCacheOperationSource()
);
bindInterceptor(
Matchers.any(),
Matchers.annotatedWith(Cacheable.class),
interceptor
);
bindInterceptor(
Matchers.any(),
Matchers.annotatedWith(CacheEvict.class),
interceptor
);
Then, implemented Strings Cache interface and CacheManager, and finally annotated my DAO classes with #Cachable and #CacheEvict:
public class DaoTester {
QssandraConsumer qs;
#CachePut(value = "cached_consumers", key = "#consumer.id")
public void save(QssandraConsumer consumer) {
qs = consumer;
}
#Cacheable(value = "cached_consumers")
public QssandraConsumer get(String id) {
if (id != null) {
qs.getId();
}
return qs;
}
#CacheEvict(value = "cached_consumers", key = "#consumer.id")
public void remove(QssandraConsumer consumer) {
qs = consumer;
}}
Caching is simply fine - no problems here, but when i try to evict(calling remove method in this example), evrything crashes and I see:
Exception in thread "main" org.springframework.expression.spel.SpelEvaluationException: EL1007E:(pos 10): Field or property 'id' cannot be found on null
at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:205)
at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:72)
at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:57)
at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:93)
at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:88)
at org.springframework.cache.interceptor.ExpressionEvaluator.key(ExpressionEvaluator.java:80)
at org.springframework.cache.interceptor.CacheAspectSupport$CacheOperationContext.generateKey(CacheAspectSupport.java:464)
at org.springframework.cache.interceptor.CacheAspectSupport.inspectCacheEvicts(CacheAspectSupport.java:260)
at org.springframework.cache.interceptor.CacheAspectSupport.inspectAfterCacheEvicts(CacheAspectSupport.java:232)
at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:215)
at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:66)
at qiwi.qommon.deployment.dao.DaoTester.main(DaoTester.java:44)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
What's wrong here?!
BTW, cached object is:
public class QssandraConsumer implements Identifiable<String> {
private String id;
private String host;
#Override
public String getId() {
return id;
}
#Override
public void setId(String id) {
this.id = id;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
#Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (null == object) {
return false;
}
if (!(object instanceof QssandraConsumer)) {
return false;
}
QssandraConsumer o = (QssandraConsumer) object;
return
Objects.equal(id, o.id)
&& Objects.equal(host, o.host);
}
#Override
public int hashCode() {
return Objects.hashCode(
id, host
);
}
#Override
public String toString() {
return Objects.toStringHelper(this)
.addValue(id)
.addValue(host)
.toString();
}
}
Finally I figured out what was the reason of the problem:
when injecting a class that uses annotation(which are intercepted, like #Cachable or #CacheEvict) Guice enhances class (AOP make bytecode modification in runtime). So when CacheInterceptor tryed to evaluate key = "#consumer.id" it failed because couldn't find argument name in enhanced class (see: LocalVariableTableParameterNameDiscoverer#inspectClass).
So it will not work in Guice out of the box.
In spring the proxy class is created - so no problems here.

Resources