Spring boot server port range setting - spring-boot

Is it possible to set an acceptable range for the server.port in the application.yml file for a spring boot application.
I have taken to setting server.port=0 to get an automatically assigned port rather than a hard coded one.
Our network ops people want to restrict the available range for this port assignment.
Any idea?

Following both user1289300 and Dave Syer, I used the answers to formulate one solution. It is supplied as a configuration that reads from the application.yml file for the server section. I supplied a port range min and max to choose from.
Thanks again
#Configuration
#ConfigurationProperties("server")
public class EmbeddedServletConfiguration{
/*
Added EmbeddedServletContainer as Tomcat currently. Need to change in future if EmbeddedServletContainer get changed
*/
private final int MIN_PORT = 1100;
private final int MAX_PORT = 65535;
/**
* this is the read port from the applcation.yml file
*/
private int port;
/**
* this is the min port number that can be selected and is filled in from the application yml fil if it exists
*/
private int maxPort = MIN_PORT;
/**
* this is the max port number that can be selected and is filled
*/
private int minPort = MAX_PORT;
/**
* Added EmbeddedServletContainer as Tomcat currently. Need to change in future if EmbeddedServletContainer get changed
*
* #return the container factory
*/
#Bean
public EmbeddedServletContainerFactory servletContainer() {
return new TomcatEmbeddedServletContainerFactory();
}
#Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
return new EmbeddedServletContainerCustomizer() {
#Override
public void customize(ConfigurableEmbeddedServletContainer container) {
// this only applies if someone has requested automatic port assignment
if (port == 0) {
// make sure the ports are correct and min > max
validatePorts();
int port = SocketUtils.findAvailableTcpPort(minPort, maxPort);
container.setPort(port);
}
container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/404"));
container.addErrorPages(new ErrorPage(HttpStatus.FORBIDDEN, "/403"));
}
};
}
/**
* validate the port choices
* - the ports must be sensible numbers and within the alowable range and we fix them if not
* - the max port must be greater than the min port and we set it if not
*/
private void validatePorts() {
if (minPort < MIN_PORT || minPort > MAX_PORT - 1) {
minPort = MIN_PORT;
}
if (maxPort < MIN_PORT + 1 || maxPort > MAX_PORT) {
maxPort = MAX_PORT;
}
if (minPort > maxPort) {
maxPort = minPort + 1;
}
}
}

Just implement EmbeddedServletContainerCustomizer
http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-web-applications.html#boot-features-programmatic-embedded-container-customization
Of course you can make improvements to public static boolean available(int port) below that checks availability of the port because some ports though available are sometimes denied like port 1024, OS dependent, also range can be read from some properties file but not with Spring because range is set before context is loaded, but that should not be a problem, I put everything in one file to show approach not to make it look pretty
#Configuration
#ComponentScan
#EnableAutoConfiguration
public class DemoApplication {
private static final int MIN_PORT = 1100; // to by set according to your
private static final int MAX_PORT = 9000; // needs or uploaded from
public static int myPort; // properties whatever suits you
public static void main(String[] args) {
int availablePort = MIN_PORT;
for (availablePort=MIN_PORT; availablePort < MAX_PORT; availablePort++) {
if (available(availablePort)) {
break;
}
}
if (availablePort == MIN_PORT && !available(availablePort)) {
throw new IllegalArgumentException("Cant start container for port: " + myPort);
}
DemoApplication.myPort = availablePort;
SpringApplication.run(DemoApplication.class, args);
}
public static boolean available(int port) {
System.out.println("TRY PORT " + port);
// if you have some range for denied ports you can also check it
// here just add proper checking and return
// false if port checked within that range
ServerSocket ss = null;
DatagramSocket ds = null;
try {
ss = new ServerSocket(port);
ss.setReuseAddress(true);
ds = new DatagramSocket(port);
ds.setReuseAddress(true);
return true;
} catch (IOException e) {
} finally {
if (ds != null) {
ds.close();
}
if (ss != null) {
try {
ss.close();
} catch (IOException e) {
/* should not be thrown */
}
}
}
return false;
}
}
and this is most important part:
#Component
class CustomizationBean implements EmbeddedServletContainerCustomizer {
#Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.setPort(DemoApplication.myPort);
}
}

The easiest way to configure is using the following in application.properties.
Here i mentioned 8084 as the minimum range and 8100 as the maximum range.
server.port=${random.int[8084,8100]}

There is challenges in spring boot project, we can not add this feature to spring boot at the moment, If you have any solution please contribute.
Spring boot server port range support Pull Request

With this solution, the application choosing her own Port. I don't understand why it get "-1", because it runs perfect.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.SocketUtils;
#Configuration
class PortRangeCustomizerBean implements EmbeddedServletContainerCustomizer
{
private final Logger logger = LoggerFactory.getLogger(this.getClass());
#Value("${spring.port.range.min}")
private int MIN_PORT;
#Value("${spring.port.range.max}")
private int MAX_PORT;
#Override
public void customize(ConfigurableEmbeddedServletContainer container) {
int port = SocketUtils.findAvailableTcpPort(MIN_PORT, MAX_PORT);
logger.info("Started with PORT:\t " + port);
container.setPort(port);
}
}

We have done this in Spring Boot 1.5.9 using EmbeddedServletContainerCustomizer and something as follows:
#Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
return (container -> {
try {
// use defaults if we can't talk to config server
Integer minPort = env.getProperty("minPort")!=null ? Integer.parseInt(env.getProperty("minPort")) : 7500;
Integer maxPort = env.getProperty("maxPort")!=null ? Integer.parseInt(env.getProperty("maxPort")) : 9500;
int port = SocketUtils.findAvailableTcpPort(minPort,maxPort);
System.getProperties().put("server.port", port);
container.setPort(port);
} catch (Exception e) {
log.error("Error occured while reading the min & max port form properties : " + e);
throw new ProductServiceException(e);
}
});
}
However this does not seem to be possible in Spring Boot 2.0.0.M7 and we are looking for an alternative way.

Related

Spring integration TCP Server multiple connections of more than 5

I'm using the following version of Spring Boot and Spring integration now.
spring.boot.version 2.3.4.RELEASE
spring-integration 5.3.2.RELEASE
My requirement is to create a TCP client server communication and i'm using spring integration for the same. The spike works fine for a single communication between client and server and also works fine for exactly 5 concurrent client connections.
The moment i have increased the concurrent client connections from 5 to any arbitary numbers, it doesn't work but the TCP server accepts only 5 connections.
I have used the 'ThreadAffinityClientConnectionFactory' mentioned by #Gary Russell in one of the earlier comments ( for similar requirements ) but still doesn't work.
Below is the code i have at the moment.
#Slf4j
#Configuration
#EnableIntegration
#IntegrationComponentScan
public class SocketConfig {
#Value("${socket.host}")
private String clientSocketHost;
#Value("${socket.port}")
private Integer clientSocketPort;
#Bean
public TcpOutboundGateway tcpOutGate(AbstractClientConnectionFactory connectionFactory) {
TcpOutboundGateway gate = new TcpOutboundGateway();
//connectionFactory.setTaskExecutor(taskExecutor());
gate.setConnectionFactory(clientCF());
return gate;
}
#Bean
public TcpInboundGateway tcpInGate(AbstractServerConnectionFactory connectionFactory) {
TcpInboundGateway inGate = new TcpInboundGateway();
inGate.setConnectionFactory(connectionFactory);
inGate.setRequestChannel(fromTcp());
return inGate;
}
#Bean
public MessageChannel fromTcp() {
return new DirectChannel();
}
// Outgoing requests
#Bean
public ThreadAffinityClientConnectionFactory clientCF() {
TcpNetClientConnectionFactory tcpNetClientConnectionFactory = new TcpNetClientConnectionFactory(clientSocketHost, serverCF().getPort());
tcpNetClientConnectionFactory.setSingleUse(true);
ThreadAffinityClientConnectionFactory threadAffinityClientConnectionFactory = new ThreadAffinityClientConnectionFactory(
tcpNetClientConnectionFactory);
// Tested with the below too.
// threadAffinityClientConnectionFactory.setTaskExecutor(taskExecutor());
return threadAffinityClientConnectionFactory;
}
// Incoming requests
#Bean
public AbstractServerConnectionFactory serverCF() {
log.info("Server Connection Factory");
TcpNetServerConnectionFactory tcpNetServerConnectionFactory = new TcpNetServerConnectionFactory(clientSocketPort);
tcpNetServerConnectionFactory.setSerializer(new CustomSerializer());
tcpNetServerConnectionFactory.setDeserializer(new CustomDeserializer());
tcpNetServerConnectionFactory.setSingleUse(true);
return tcpNetServerConnectionFactory;
}
#Bean
public TaskExecutor taskExecutor () {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(50);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(50);
executor.setAllowCoreThreadTimeOut(true);
executor.setKeepAliveSeconds(120);
return executor;
}
}
Did anyone had the same issue with having multiple concurrent Tcp client connections of more than 5 ?
Thanks
Client Code:
#Component
#Slf4j
#RequiredArgsConstructor
public class ScheduledTaskService {
// Timeout in milliseconds
private static final int SOCKET_TIME_OUT = 18000;
private static final int BUFFER_SIZE = 32000;
private static final int ETX = 0x03;
private static final String HEADER = "ABCDEF ";
private static final String data = "FIXED DARATA"
private final AtomicInteger atomicInteger = new AtomicInteger();
#Async
#Scheduled(fixedDelay = 100000)
public void sendDataMessage() throws IOException, InterruptedException {
int numberOfRequests = 10;
Callable<String> executeMultipleSuccessfulRequestTask = () -> socketSendNReceive();
final Collection<Callable<String>> callables = new ArrayList<>();
IntStream.rangeClosed(1, numberOfRequests).forEach(i-> {
callables.add(executeMultipleSuccessfulRequestTask);
});
ExecutorService executorService = Executors.newFixedThreadPool(numberOfRequests);
List<Future<String>> taskFutureList = executorService.invokeAll(callables);
List<String> strings = taskFutureList.stream().map(future -> {
try {
return future.get(20000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
return "";
}).collect(Collectors.toList());
strings.forEach(string -> log.info("Message received from the server: {} ", string));
}
public String socketSendNReceive() throws IOException{
int requestCounter = atomicInteger.incrementAndGet();
String host = "localhost";
int port = 8000;
Socket socket = new Socket();
InetSocketAddress address = new InetSocketAddress(host, port);
socket.connect(address, SOCKET_TIME_OUT);
socket.setSoTimeout(SOCKET_TIME_OUT);
//Send the message to the server
OutputStream os = socket.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(os);
bos.write(HEADER.getBytes());
bos.write(data.getBytes());
bos.write(ETX);
bos.flush();
// log.info("Message sent to the server : {} ", envio);
//Get the return message from the server
InputStream is = socket.getInputStream();
String response = receber(is);
log.info("Received response");
return response;
}
private String receber(InputStream in) throws IOException {
final StringBuffer stringBuffer = new StringBuffer();
int readLength;
byte[] buffer;
buffer = new byte[BUFFER_SIZE];
do {
if(Objects.nonNull(in)) {
log.info("Input Stream not null");
}
readLength = in.read(buffer);
log.info("readLength : {} ", readLength);
if(readLength > 0){
stringBuffer.append(new String(buffer),0,readLength);
log.info("String ******");
}
} while (buffer[readLength-1] != ETX);
buffer = null;
stringBuffer.deleteCharAt(resposta.length()-1);
return stringBuffer.toString();
}
}
Since you are opening the connections all at the same time, you need to increase the backlog property on the server connection factory.
It defaults to 5.
/**
* The number of sockets in the connection backlog. Default 5;
* increase if you expect high connection rates.
* #param backlog The backlog to set.
*/
public void setBacklog(int backlog) {

How to start multiple boot apps for end-to-end tests?

I'd like to write end-to-end tests to validate two boot apps work well together with various profiles.
What already works:
create a third maven module (e2e) for end-to-end tests, in addition to the two tested apps (authorization-server and resource-server)
write tests using TestResTemplate
Test work fine if I start authorization-server and resource-server manually.
What I now want to do is automate the tested boot apps startup and shutdown with the right profiles for each test.
I tried:
adding maven dependencies to tested apps in e2e module
using SpringApplication in new threads for each app to start
But I face miss-configuration issues as all resources and dependencies end in the same shared classpath...
Is there a way to sort this out?
I'm also considering starting two separate java -jar ... processes, but then, how to ensure tested apps fat-jars are built before 2e2 unit-tests run?
Current app start/shutdown code sample which fails as soon as I had maven dependency to second app to start:
private Service startAuthorizationServer(boolean isJwtActive) throws InterruptedException {
return new Service(
AuthorizationServer.class,
isJwtActive ? new String[]{ "jwt" } : new String[]{} );
}
private static final class Service {
private ConfigurableApplicationContext context;
private final Thread thread;
public Service(Class<?> appClass, String... profiles) throws InterruptedException {
thread = new Thread(() -> {
SpringApplication app = new SpringApplicationBuilder(appClass).profiles(profiles).build();
context = app.run();
});
thread.setDaemon(false);
thread.start();
while (context == null || !context.isRunning()) {
Thread.sleep(1000);
};
}
#PreDestroy
public void stop() {
if (context != null) {
SpringApplication.exit(context);
}
if (thread != null) {
thread.interrupt();
}
}
}
I think your case, running the two applications via a docker compose can be a good idea.
This article shows how you can set up some integration tests using a docker compose image: https://blog.codecentric.de/en/2017/03/writing-integration-tests-docker-compose-junit/
Also, take a look at this post from Martin Fowler: https://martinfowler.com/articles/microservice-testing/
I got things working with second solution:
end-to-end tests projects has no other maven dependency than what is required to run spring-tests with TestRestClient
test config initialises environment, running mvn packageon required modules in separate processes
test cases run (re)start apps with chosen profiles in separate java -jar ... processes
Here is the helper class I wrote for this (taken from there):
class ActuatorApp {
private final int port;
private final String actuatorEndpoint;
private final File jarFile;
private final TestRestTemplate actuatorClient;
private Process process;
private ActuatorApp(File jarFile, int port, TestRestTemplate actuatorClient) {
this.port = port;
this.actuatorEndpoint = getBaseUri() + "actuator/";
this.actuatorClient = actuatorClient;
this.jarFile = jarFile;
Assert.isTrue(jarFile.exists(), jarFile.getAbsolutePath() + " does not exist");
}
public void start(List<String> profiles, List<String> additionalArgs) throws InterruptedException, IOException {
if (isUp()) {
stop();
}
this.process = Runtime.getRuntime().exec(appStartCmd(jarFile, profiles, additionalArgs));
Executors.newSingleThreadExecutor().submit(new ProcessStdOutPrinter(process));
for (int i = 0; i < 10 && !isUp(); ++i) {
Thread.sleep(5000);
}
}
public void start(String... profiles) throws InterruptedException, IOException {
this.start(Arrays.asList(profiles), List.of());
}
public void stop() throws InterruptedException {
if (isUp()) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
headers.setAccept(List.of(MediaType.APPLICATION_JSON_UTF8));
actuatorClient.postForEntity(actuatorEndpoint + "shutdown", new HttpEntity<>(headers), Object.class);
Thread.sleep(5000);
}
if (process != null) {
process.destroy();
}
}
private String[] appStartCmd(File jarFile, List<String> profiles, List<String> additionalArgs) {
final List<String> cmd = new ArrayList<>(
List.of(
"java",
"-jar",
jarFile.getAbsolutePath(),
"--server.port=" + port,
"--management.endpoint.heath.enabled=true",
"--management.endpoint.shutdown.enabled=true",
"--management.endpoints.web.exposure.include=*",
"--management.endpoints.web.base-path=/actuator"));
if (profiles.size() > 0) {
cmd.add("--spring.profiles.active=" + profiles.stream().collect(Collectors.joining(",")));
}
if (additionalArgs != null) {
cmd.addAll(additionalArgs);
}
return cmd.toArray(new String[0]);
}
private boolean isUp() {
try {
final ResponseEntity<HealthResponse> response =
actuatorClient.getForEntity(actuatorEndpoint + "health", HealthResponse.class);
return response.getStatusCode().is2xxSuccessful() && response.getBody().getStatus().equals("UP");
} catch (ResourceAccessException e) {
return false;
}
}
public static Builder builder(String moduleName, String moduleVersion) {
return new Builder(moduleName, moduleVersion);
}
/**
* Configure and build a spring-boot app
*
* #author Ch4mp
*
*/
public static class Builder {
private String moduleParentDirectory = "..";
private final String moduleName;
private final String moduleVersion;
private int port = SocketUtils.findAvailableTcpPort(8080);
private String actuatorClientId = "actuator";
private String actuatorClientSecret = "secret";
public Builder(String moduleName, String moduleVersion) {
this.moduleName = moduleName;
this.moduleVersion = moduleVersion;
}
public Builder moduleParentDirectory(String moduleParentDirectory) {
this.moduleParentDirectory = moduleParentDirectory;
return this;
}
public Builder port(int port) {
this.port = port;
return this;
}
public Builder actuatorClientId(String actuatorClientId) {
this.actuatorClientId = actuatorClientId;
return this;
}
public Builder actuatorClientSecret(String actuatorClientSecret) {
this.actuatorClientSecret = actuatorClientSecret;
return this;
}
/**
* Ensures the app module is found and packaged
* #return app ready to be started
* #throws IOException if module packaging throws one
* #throws InterruptedException if module packaging throws one
*/
public ActuatorApp build() throws IOException, InterruptedException {
final File moduleDir = new File(moduleParentDirectory, moduleName);
packageModule(moduleDir);
final File jarFile = new File(new File(moduleDir, "target"), moduleName + "-" + moduleVersion + ".jar");
return new ActuatorApp(jarFile, port, new TestRestTemplate(actuatorClientId, actuatorClientSecret));
}
private void packageModule(File moduleDir) throws IOException, InterruptedException {
Assert.isTrue(moduleDir.exists(), "could not find module. " + moduleDir + " does not exist.");
String[] cmd = new File(moduleDir, "pom.xml").exists() ?
new String[] { "mvn", "-DskipTests=true", "package" } :
new String[] { "./gradlew", "bootJar" };
Process mvnProcess = new ProcessBuilder().directory(moduleDir).command(cmd).start();
Executors.newSingleThreadExecutor().submit(new ProcessStdOutPrinter(mvnProcess));
Assert.isTrue(mvnProcess.waitFor() == 0, "module packaging exited with error status.");
}
}
private static class ProcessStdOutPrinter implements Runnable {
private InputStream inputStream;
public ProcessStdOutPrinter(Process process) {
this.inputStream = process.getInputStream();
}
#Override
public void run() {
new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(System.out::println);
}
}
public String getBaseUri() {
return "https://localhost:" + port;
}
}

How to set the default JDBC URL value in H2 console using spring boot

I have enable the H2 console in spring boot. However, when I open the console connection page the default url is the one staved in the H2 console history. How can i configure the project to populate the URL to be the same as spring.datasource.url on project start? Currently I set the url in the console manually but I would like to have it setup automatically by the project itself.
yaml:
spring:
h2:
console:
enabled: true
path: /admin/h2
datasource:
url: jdbc:h2:mem:foobar
update:
I know that the last connection settings are saved to ~/.h2.server.properties but what I need is to set the properties from the starting application potentially, potentially switching between several of them
There is no hook provided to fill in settings.
The good news is that we can change that with a bit of code.
Current state
The Login screen is created in WebApp.index()
String[] settingNames = server.getSettingNames();
String setting = attributes.getProperty("setting");
if (setting == null && settingNames.length > 0) {
setting = settingNames[0];
}
String combobox = getComboBox(settingNames, setting);
session.put("settingsList", combobox);
ConnectionInfo info = server.getSetting(setting);
if (info == null) {
info = new ConnectionInfo();
}
session.put("setting", PageParser.escapeHtmlData(setting));
session.put("name", PageParser.escapeHtmlData(setting));
session.put("driver", PageParser.escapeHtmlData(info.driver));
session.put("url", PageParser.escapeHtmlData(info.url));
session.put("user", PageParser.escapeHtmlData(info.user));
return "index.jsp";
We want to tap into server.getSettingNames(), and precisely into server.getSettings() used underneath.
synchronized ArrayList<ConnectionInfo> getSettings() {
ArrayList<ConnectionInfo> settings = new ArrayList<>();
if (connInfoMap.size() == 0) {
Properties prop = loadProperties();
if (prop.size() == 0) {
for (String gen : GENERIC) {
ConnectionInfo info = new ConnectionInfo(gen);
settings.add(info);
updateSetting(info);
}
} else {
for (int i = 0;; i++) {
String data = prop.getProperty(Integer.toString(i));
if (data == null) {
break;
}
ConnectionInfo info = new ConnectionInfo(data);
settings.add(info);
updateSetting(info);
}
}
} else {
settings.addAll(connInfoMap.values());
}
Collections.sort(settings);
return settings;
}
The plan
disable ServletRegistrationBean<WebServlet> created by H2ConsoleAutoConfiguration
replace it with our config class with a subclass of WebServlet
our CustomH2WebServlet will override init and register CustomH2WebServer (subclass of WebServer)
in CustomH2WebServer we override getSettings() and we are done
The Code
#EnableConfigurationProperties({H2ConsoleProperties.class, DataSourceProperties.class})
#Configuration
public class H2Config {
private final H2ConsoleProperties h2ConsoleProperties;
private final DataSourceProperties dataSourceProperties;
public H2Config(H2ConsoleProperties h2ConsoleProperties, DataSourceProperties dataSourceProperties) {
this.h2ConsoleProperties = h2ConsoleProperties;
this.dataSourceProperties = dataSourceProperties;
}
#Bean
public ServletRegistrationBean<WebServlet> h2Console() {
String path = this.h2ConsoleProperties.getPath();
String urlMapping = path + (path.endsWith("/") ? "*" : "/*");
ServletRegistrationBean<WebServlet> registration = new ServletRegistrationBean<>(
new CustomH2WebServlet(this.dataSourceProperties.getUrl()), urlMapping);
H2ConsoleProperties.Settings settings = this.h2ConsoleProperties.getSettings();
if (settings.isTrace()) {
registration.addInitParameter("trace", "");
}
if (settings.isWebAllowOthers()) {
registration.addInitParameter("webAllowOthers", "");
}
return registration;
}
}
package org.h2.server.web;
import javax.servlet.ServletConfig;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Enumeration;
public class CustomH2WebServlet extends WebServlet {
private final String dbUrl;
public CustomH2WebServlet(String dbUrl) {
this.dbUrl = dbUrl;
}
#Override
public void init() {
ServletConfig config = getServletConfig();
Enumeration<?> en = config.getInitParameterNames();
ArrayList<String> list = new ArrayList<>();
while (en.hasMoreElements()) {
String name = en.nextElement().toString();
String value = config.getInitParameter(name);
if (!name.startsWith("-")) {
name = "-" + name;
}
list.add(name);
if (value.length() > 0) {
list.add(value);
}
}
String[] args = list.toArray(new String[0]);
WebServer server = new CustomH2WebServer(dbUrl);
server.setAllowChunked(false);
server.init(args);
setServerWithReflection(this, server);
}
private static void setServerWithReflection(final WebServlet classInstance, final WebServer newValue) {
try {
final Field field = WebServlet.class.getDeclaredField("server");
field.setAccessible(true);
field.set(classInstance, newValue);
}
catch (SecurityException|NoSuchFieldException|IllegalArgumentException|IllegalAccessException ex) {
throw new RuntimeException(ex);
}
}
}
package org.h2.server.web;
import java.util.ArrayList;
import java.util.Collections;
class CustomH2WebServer extends WebServer {
private final String connectionInfo;
CustomH2WebServer(String dbUrl) {
this.connectionInfo = "Test H2 (Embedded)|org.h2.Driver|" +dbUrl+"|sa";
}
synchronized ArrayList<ConnectionInfo> getSettings() {
ArrayList<ConnectionInfo> settings = new ArrayList<>();
ConnectionInfo info = new ConnectionInfo(connectionInfo);
settings.add(info);
updateSetting(info);
Collections.sort(settings);
return settings;
}
}
spring.h2.console.enabled=false
spring.datasource.url=jdbc:h2:mem:foobar
Everything went smoolthly, except of one private field that needed to be set via reflection.
The code provided works with H2 1.4.199
Inspired in #Lesiak's answer, using a simple and easier configuration class
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import lombok.extern.apachecommons.CommonsLog;
import org.h2.server.web.ConnectionInfo;
import org.h2.server.web.WebServer;
import org.h2.server.web.WebServlet;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#CommonsLog
#Configuration
#ConditionalOnProperty(prefix = "spring.h2.console", name = "enabled", havingValue = "true", matchIfMissing = false)
public class H2ConsoleConfiguration {
#Bean
ServletRegistrationBean<WebServlet> h2ConsoleRegistrationBean(final ServletRegistrationBean<WebServlet> h2Console, final DataSourceProperties dataSourceProperties) {
h2Console.setServlet(new WebServlet() {
#Override
public void init() {
super.init();
updateWebServlet(this, dataSourceProperties);
}
});
return h2Console;
}
public static void updateWebServlet(final WebServlet webServlet, DataSourceProperties dataSourceProperties) {
try {
updateWebServer(getWebServer(webServlet), dataSourceProperties);
} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException | NoSuchMethodException | InvocationTargetException | NullPointerException ex) {
log.error("Unable to set a custom ConnectionInfo for H2 console", ex);
}
}
public static WebServer getWebServer(final WebServlet webServlet) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
final Field field = WebServlet.class.getDeclaredField("server");
field.setAccessible(true);
return (WebServer) field.get(webServlet);
}
public static void updateWebServer(final WebServer webServer, final DataSourceProperties dataSourceProperties) throws NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
final ConnectionInfo connectionInfo = new ConnectionInfo(String.format("Generic Spring Datasource|%s|%s|%s", dataSourceProperties.determineDriverClassName(), dataSourceProperties.determineUrl(), dataSourceProperties.determineUsername()));
final Method method = WebServer.class.getDeclaredMethod("updateSetting", ConnectionInfo.class);
method.setAccessible(true);
method.invoke(webServer, connectionInfo);
}
}

Spring: How to Resolve a Property When There Are Multiple Resolvers?

We are building several Microservices using the Spring Cloud framework. One of the services has dependencies on some legacy shared libraries, and imports various XML files for bean configuration. The problem we are facing is that through these imports, multiple property resolvers are brought in and thus the following code in AbstractBeanFactory is failing to resolve spring.application.name because the value comes in as ${spring.application.name:unknown} that the first resolver fails to resolve and thus sets result to unknown. embeddedValueResolver does have a resolver than can resolve the property but because the property is set to it's default by a previous resolver, it doesn't get a chance. This is causing the service registration with Eureka to fail with a NPE.
#Override
public String resolveEmbeddedValue(String value) {
String result = value;
for (StringValueResolver resolver : this.embeddedValueResolvers) {
if (result == null) {
return null;
}
result = resolver.resolveStringValue(result);
}
return result;
}
Answering my own question, I fixed the issue using a BeanDefinitionRegistryPostProcessor. Related JIRA SPR-6428 had been filed by another user but was closed.
/**
* Removes {#link org.springframework.beans.factory.config.PropertyPlaceholderConfigurer} classes that come before
* {#link PropertySourcesPlaceholderConfigurer} and fail to resolve Spring Cloud properties, thus setting them to default.
* One such property is {#code spring.application.name} that gets set to 'unknown' thus causing registration with
* discovery service to fail. This class collects the {#code locations} from these offending
* {#code PropertyPlaceholderConfigurer} and later adds to the end of property sources available from
* {#link org.springframework.core.env.Environment}.
* <p>
* c.f. https://jira.spring.io/browse/SPR-6428
*
* #author Abhijit Sarkar
*/
#Component
#Slf4j
public class PropertyPlaceholderConfigurerPostProcessor implements BeanDefinitionRegistryPostProcessor {
private final Set<String> locations = new HashSet<>();
#Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
String[] beanDefinitionNames = beanDefinitionRegistry.getBeanDefinitionNames();
List<String> propertyPlaceholderConfigurers = Arrays.stream(beanDefinitionNames)
.filter(name -> name.contains("PropertyPlaceholderConfigurer"))
.collect(toList());
for (String name : propertyPlaceholderConfigurers) {
BeanDefinition beanDefinition = beanDefinitionRegistry.getBeanDefinition(name);
TypedStringValue location = (TypedStringValue) beanDefinition.getPropertyValues().get("location");
if (location != null) {
String value = location.getValue();
log.info("Found location: {}.", location);
/* Remove 'classpath:' prefix, if present. It later creates problem with reading the file. */
locations.add(removeClasspathPrefixIfPresent(value));
log.info("Removing bean definition: {}.", name);
beanDefinitionRegistry.removeBeanDefinition(name);
}
}
}
private String removeClasspathPrefixIfPresent(String location) {
int classpathPrefixIdx = location.lastIndexOf(':');
return classpathPrefixIdx > 0 ? location.substring(++classpathPrefixIdx) : location;
}
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
PropertySourcesPlaceholderConfigurer configurer =
beanFactory.getBean(PropertySourcesPlaceholderConfigurer.class);
MutablePropertySources propertySources = getPropertySources(configurer);
locations.stream()
.map(locationToPropertySrc)
.forEach(propertySources::addLast);
}
private MutablePropertySources getPropertySources(PropertySourcesPlaceholderConfigurer configurer) {
/* I don't like this but PropertySourcesPlaceholderConfigurer has no getter for environment. */
Field envField = null;
try {
envField = PropertySourcesPlaceholderConfigurer.class.getDeclaredField("environment");
envField.setAccessible(true);
ConfigurableEnvironment env = (ConfigurableEnvironment) envField.get(configurer);
return env.getPropertySources();
} catch (ReflectiveOperationException e) {
throw new ApplicationContextException("Our little hack didn't work. Failed to read field: environment.", e);
}
}
Function<String, PropertySource> locationToPropertySrc = location -> {
ClassPathResource resource = new ClassPathResource(location);
try {
Properties props = PropertiesLoaderUtils.loadProperties(resource);
String filename = getFilename(location);
log.debug("Adding property source with name: {} and location: {}.", filename, location);
return new PropertiesPropertySource(filename, props);
} catch (IOException e) {
throw new ApplicationContextException(
String.format("Failed to read from location: %s.", location), e);
}
};
private String getFilename(String location) {
return location.substring(location.lastIndexOf('/') + 1);
}
}

Schedule a task with Cron which allows dynamic update

I use sprint boot 1.3, spring 4.2
In this class
#Service
public class PaymentServiceImpl implements PaymentService {
....
#Transactional
#Override
public void processPayment() {
List<Payment> payments = paymentRepository.findDuePayment();
processCreditCardPayment(payments);
}
}
I would like to call processPayment every x moment.
This x moment is set in a database.
The user can modify it.
So i think i can't use anotation.
I started to this this
#EntityScan(basePackageClasses = {MyApp.class, Jsr310JpaConverters.class})
#SpringBootApplication
#EnableCaching
#EnableScheduling
public class MyApp {
#Autowired
private DefaultConfigService defaultConfigService;
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
#Bean
public TaskScheduler poolScheduler() {
SimpleAsyncTaskExecutor taskScheduler = new SimpleAsyncTaskExecutor();
DefaultConfigDto defaultConfigDto = defaultConfigService.getByFieldName("payment-cron-task");
String cronTabExpression = "0 0 4 * * ?";
if (defaultConfigDto != null && !defaultConfigDto.getFieldValue().isEmpty()) {
cronTabExpression = "0 0 4 * * ?";
}
appContext.getBean("scheduler");
taskScheduler.schedule(task, new CronTrigger(cronTabExpression));
return scheduler;
}
Maybe it's not the good way.
Any suggestion?
Don't know if to get my context if i need to create a property like
#Autowired
ConfigurableApplicationContext context;
and after in the main
public static void main(String[] args) {
context = SpringApplication.run(MyApp.class, args);
}
Looking at the question seems like you want to update the scheduler, without restart.
The code you have shared only ensures the config is picked from DB, but it will not refresh without application restart.
The following code will use the default scheduler available in the spring context and dynamically compute the next execution time based on the available cron setting in the DB:
Here is the sample code:
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
#SpringBootApplication
#EnableScheduling
public class Perses implements SchedulingConfigurer {
private static final Logger log = LoggerFactory.getLogger(Perses.class);
#Autowired
private DefaultConfigService defaultConfigService;
#Autowired
private PaymentService paymentService;
public static void main(String[] args) {
SpringApplication.run(Perses.class, args);
}
private String cronConfig() {
String cronTabExpression = "*/5 * * * * *";
if (defaultConfigDto != null && !defaultConfigDto.getFieldValue().isEmpty()) {
cronTabExpression = "0 0 4 * * ?";
}
return cronTabExpression;
}
#Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(new Runnable() {
#Override
public void run() {
paymentService.processPayment();
}
}, new Trigger() {
#Override
public Date nextExecutionTime(TriggerContext triggerContext) {
String cron = cronConfig();
log.info(cron);
CronTrigger trigger = new CronTrigger(cron);
Date nextExec = trigger.nextExecutionTime(triggerContext);
return nextExec;
}
});
}
}
Just if someone still having this issue a better solution getting value from database whenever you want without many changes would be run cron every minute and get mod between current minute versus a configurated value delta from database, if this mod is equals to 0 means it has to run like if it is a mathematical multiple, so if you want it to run every 5 minutes for example delta should be 5.
A sample:
#Scheduled(cron = "0 */1 * * * *") //fire every minute
public void perform() {
//running
Integer delta = 5;//get this value from databse
Integer minutes = getField(Calendar.MINUTE)//calendar for java 7;
Boolean toRun = true;//you can also get this one from database to make it active or disabled
toRun = toRun && (minutes % delta == 0);
if (toRun && (!isRunning)) {
isRunning = true;
try {
//do your logic here
} catch (Exception e) { }
isRunning = false;
}
}
public Integer getField(int field) {
Calendar now = Calendar.getInstance();
if(field == Calendar.MONTH) {
return now.get(field)+ 1; // Note: zero based!
}else {
return now.get(field);
}
}
Hope this help :D

Resources