spring boot actuator connect jmx programmatically - spring-boot

I'd like to use the shutdown endpoint of my Spring Boot 2.0.1 application from the command line. For that I have only added the spring-boot-starter-actuator to my Gradle file and enabled the shutdown endpoint in the configuration.
I also created a very simple tool that tries to connect via JMX to the running application.
Snippet:
String url = "service:jmx:rmi:///jndi/rmi://127.0.01:<which port?>/jmxrmi";
JMXServiceURL serviceUrl = new JMXServiceURL(url);
JMXConnectorFactory.connect(serviceUrl, null); <-- KAPOW!
JMX is working because I can use jconsole to connect locally. I just have no clue how to do it programmatically.
Any other attempts to explicitly set a port as mentioned here didn't work. Any hints?

It's probably easier to enable jolokia rather than using RMI; then you can simply
curl http://localhost:8080/actuator/jolokia/exec/org.springframework.boot:type=Admin,name=SpringApplication/shutdown
EDIT
If you prefer to use RMI, refer to the Spring Framework JMX Documentation.
Server app:
#SpringBootApplication
public class So50392589Application {
public static void main(String[] args) {
SpringApplication.run(So50392589Application.class, args);
}
#Bean
public RmiRegistryFactoryBean rmi() {
RmiRegistryFactoryBean rmi = new RmiRegistryFactoryBean();
rmi.setPort(1099);
return rmi;
}
#Bean
public ConnectorServerFactoryBean server() throws Exception {
ConnectorServerFactoryBean fb = new ConnectorServerFactoryBean();
fb.setObjectName("connector:name=rmi");
fb.setServiceUrl("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/myconnector");
return fb;
}
}
Client app:
#SpringBootApplication
public class JmxClient {
public static void main(String[] args) {
new SpringApplicationBuilder(JmxClient.class)
.web(WebApplicationType.NONE)
.run(args);
}
#Bean
public ApplicationRunner runner(MBeanServerConnection jmxConnector) {
return args -> {
jmxConnector.invoke(new ObjectName("org.springframework.boot:type=Admin,name=SpringApplication"),
"shutdown", new Object[0], new String[0]);
};
}
#Bean
public MBeanServerConnectionFactoryBean jmxConnector() throws Exception {
MBeanServerConnectionFactoryBean jmx = new MBeanServerConnectionFactoryBean();
jmx.setServiceUrl("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/myconnector");
return jmx;
}
}

There is a much simpler approach if you do not need to connect to the app remotely using the jcmd tool introduced in Java SE 7, and the Attach API introduced in Java SE 6.
I've written a blog post that explains this in detail. It's too big to simply copy-paste here, following is the link to the relevant section.
https://blog.asarkar.com/technical/grpc-kubernetes-spring/#jmx
This is not a duplicate answer because when the question was asked, this answer didn't exist. This answer has already been tailored to this question; let's not get trigger happy mods.

Related

Spring Cloud Function (GCP adapter) throws Hibernate lazy could not initialize proxy - no session

This is a common error in Spring when tries to transform automatically an entity object whit some hibernate proxys but i dont't know how to load Jackson DataType Hibernate5 module in Spring cloud functions gcp adapter.
#SpringBootApplication
#Log4j2
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public WebMvcConfigurer corsConfigurer() {
log.info("configurando cors");
return new WebMvcConfigurer() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*");
}
};
}
#Bean
public Module datatypeHibernateModule() {
log.info("Cargando modulo hibernate jackson");
return new Hibernate5Module();
}
}
If i use the same code whit normal Spring boot project the module works but in this case i found on the log the adapter don't used Jackson and they implements Gson.
at com.google.gson.Gson.toJson(Gson.java:638)
at com.google.gson.Gson.toJson(Gson.java:618)
at org.springframework.cloud.function.json.GsonMapper.toJson(GsonMapper.java:70)
This is the entire log file
My first workaround is change the Page object for String and use manually jackson mapper.
public class ObtenerEstados implements Function<Void, String> {
#Autowired
private EstadoService estadoService;
#SneakyThrows
#Override
public String apply(Void unused) {
Page<Estado> page = estadoService.buscarTodos(0, 33);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new Hibernate5Module());
String objectAsString = objectMapper.writeValueAsString(page);
return objectAsString;
}
}
I created two branches on the Github repository
functions (this branch have the bug)
function-jackson-hibernate5 (this branch have the workaround)
If you haved already installed Docker and Docker Compose you can reproduce the bug easy.
Follow the next steps:
git clone https://github.com/ripper2hl/sepomex.git
cd sepomex
git checkout -b dev origin/functions
docker-compose pull db
docker-compose up -d db
export spring_profiles_active=local
mvn -Pgcp function:run
And execute with a curl or any REST client
curl http://localhost:8080/
I know the alternative for use a DTO object but i prefer not use this option
So whenever Gson is on the classpath it is given priority and of course with Google that is the case. Please set spring.http.converters.preferred-json-mapper=jackson property to force Jackson.
Finally i fixed with this fragment of code
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public JsonMessageConverter jsonMessageConverter() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new Hibernate5Module());
JacksonMapper jacksonMapper = new JacksonMapper(objectMapper);
return new JsonMessageConverter(jacksonMapper);
}
}
The documentation explain Gson is the default MessageConverter but it's not be clear how to change(more easy) gson to jackson.
https://docs.spring.io/spring-cloud-function/docs/current/reference/html/spring-cloud-function.html#_provided_messageconverters

How to hand over credentials with quarkus as main application without application.properties

I have a quarkus application which is a main app. Here is an simplified example:
#QuarkusMain
public class MyXYZTool {
public static void main(String... args) {
Quarkus.run(MyXYZTool.class, args);
}
public static class CacheApp implements QuarkusApplication {
#Inject
AgroalDataSource dataSource;
#Override
public int run(String... args) throws Exception {
readCommandLineParameter(args);
//and so on...
return 0;
}
}
};
I have also an application.properties which contains the credentials to access to an Azure cloud blobstorage.
Secrets.BlobStorages.xyz.ConnectionString=abcaccesstoken
I start my app from command line like this:
//start from command line
java -jar ./target/xyz-runner.jar
My app works also so far.
But I want to hand over the credentials to access an Azure cloud blob storage over the command line. It should not be hardcoded in the application.properties. This has security reasons.
I ask me how I can hand over Secrets.BlobStorages.xyz.ConnectionString over the command line and activate this.
Can somebody help me?
I have found an answer to my question by this webside from Quarkus:
https://quarkus.io/guides/config#overriding-properties-at-runtime
I tried it out and it works.

Is it possible to run SpringFox' swagger and swagger-ui on a different port than the main application?

We are using SpringBoot and SpringFox using #EnableSwagger2 to expose the swagger-ui.html API documentation (we don't need it to automate client code, just as documentation and test ui).
Is it possible to run all swagger related endpoints under a different port (for example the spring boot management/monitoring port) than the main application?
I researched a bit, but did not find a way in swagger's/springfox' configuration to do it. Is there a spring way to do this?
Yes, there is a Spring way of doing this:
Step 1. Adding an additional Tomcat connector
To add a port to the embedded server an additional connector needs to be configured.
We will do it by providing custom WebServerFactoryCustomizer:
#Component
public class TomcatContainerCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
#Value("${swagger.port}")
private int swaggerPort;
#Override
public void customize(TomcatServletWebServerFactory factory) {
Connector swaggerConnector = new Connector();
swaggerConnector.setPort(swaggerPort);
factory.addAdditionalTomcatConnectors(swaggerConnector);
}
}
Now Tomcat listens on two ports but it serves the same content on both of them. We need to filter it.
Step 2. Adding a filter
Adding a servlet filter is pretty straightforward with a FilterRegistrationBean.
It can be created anywhere, I added it directly to the TomcatContainerCustomizer.
#Component
public class TomcatContainerCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
#Value("${swagger.port}")
private int swaggerPort;
#Value("${swagger.paths}")
private List<String> swaggerPaths;
#Override
public void customize(TomcatServletWebServerFactory factory) {
Connector swaggerConnector = new Connector();
swaggerConnector.setPort(swaggerPort);
factory.addAdditionalTomcatConnectors(swaggerConnector);
}
#Bean
public FilterRegistrationBean<SwaggerFilter> swaggerFilterRegistrationBean() {
FilterRegistrationBean<SwaggerFilter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new SwaggerFilter());
filterRegistrationBean.setOrder(-100);
filterRegistrationBean.setName("SwaggerFilter");
return filterRegistrationBean;
}
private class SwaggerFilter extends OncePerRequestFilter {
private AntPathMatcher pathMatcher = new AntPathMatcher();
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
boolean isSwaggerPath = swaggerPaths.stream()
.anyMatch(path -> pathMatcher.match(path, httpServletRequest.getServletPath()));
boolean isSwaggerPort = httpServletRequest.getLocalPort() == swaggerPort;
if(isSwaggerPath == isSwaggerPort) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
} else {
httpServletResponse.sendError(404);
}
}
}
}
The properties swagger.port and swagger.paths are configured in the application.yaml:
server.port: 8080
swagger:
port: 8088
paths: |
/swagger-ui.html,
/webjars/springfox-swagger-ui/**/*,
/swagger-resources,
/swagger-resources/**/*,
/v2/api-docs
So far so good: the swagger-ui is served on the port 8088, our api on the 8080.
But there is a problem: when we try to connect to the api from the swagger-ui,
the requests are sent to the 8088 instead of 8080.
Step 3. Adjusting SpringFox config.
Swagger assumes that the api runs on the same port as the swagger-ui.
We need to explicitly specify the port:
#Value("${server.port}")
private int serverPort;
#Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.host("localhost:" + serverPort);
}
And the last problem: as the ui runs on a different port than the api,
the requests are considered cross-origin. We need to unblock them.
It can be done globally:
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**/*").allowedOrigins("http://localhost:" + swaggerPort);
}
};
}
or by adding annotations to the controllers:
#CrossOrigin(origins = "http://localhost:${swagger.port}")
Versions used: SpringBoot 2.2.2.RELEASE, springfox-swagger2 2.9.2
For a working example see https://github.com/mafor/swagger-ui-port
I don't think so. When you're setting the Spring Boot management port (management.server.port), a second application server gets started to serve the actuator stuff. As far as I know there is no possibility (apart from custom actuator endpoints) to publish something on that server.
What is your use case exactly? Do you want to prevent access to Swagger in production or for non-authenticated users?

How to trigger a Spring cloud task from an external application?

I have created a spring cloud task that will perform some specific task based on the requirement. I wanted to call this task from another spring boot application. Please let me know is there any way of calling the below task from an external application.
#SpringBootApplication
#EnableTask
public class FileGenerationTaskApplication {
#Autowired
private DataSource dataSource;
public class FileGeneratorTaskConfigurer extends DefaultTaskConfigurer {
public FileGeneratorTaskConfigurer(DataSource dataSource){
super(dataSource);
}
}
#Bean()
public FileGeneratorTaskConfigurer getTaskConfigurer() {
return new FileGeneratorTaskConfigurer(dataSource);
}
public static void main(String[] args) {
SpringApplication.run(FileGenerationTaskApplication.class, args);
}
#Component
public static class FileGeneratorTaskRunner implements ApplicationRunner {
#Autowired
private FulfillmentFileGenerationService service;
public void run(ApplicationArguments args) throws Exception {
System.out.println("FileGeneratorTaskRunner from Spring Cloud Task!");
service.fulFillmentFileGenerationTask();
}
}
}
Can we create a REST api to call the spring cloud task?
It would be nice to have the Task registered on Spring Cloud Dataflow.
After you have your Task registered, you can make REST calls to trigger the Task. Check this example out.
You can also use Spring Cloud Dataflow Rest Client
DataFlowOperations dataFlowOperations = new DataFlowTemplate(URI.create(springDataFlowUri));
TaskOperations operations = dataFlowOperations.taskOperations();
Then you can start launching the Tasks previously got using the API Rest.
In case you do not want to use Spring Cloud DataFlow, remember when you create a Task, this is a Spring Boot Application by itself, so you can expose end points to trigger the Task.

How to redirect automatically to https with Spring Boot

How I can easily configure the embedded tomcat server to redirect all http traffic to https? I have Spring Boot running on an ec2 instance that is behind an elastic load balancer. I have configured the ELB to handle ssl for me (which is awesome) and it sets the X-FORWARDED-PROTO header to "https". I want to detect when that isn't set, and redirect the user to force them to use https if they aren't already.
So far, I have tried adding the following to my application.properties file with no luck:
server.tomcat.protocol-header=x-forwarded-proto
security.require-ssl=true
My answer is a little late but I just recently had this problem and want to post a solution which worked for me.
Originally, I thought that setting tomcat up to use the X-Forwarded headers would suffice but the RemoteIPValve from Tomcat, which should normally handle this case, didnt work for me.
My solution was to add an EmbeddedServletContainerCustomizer and add a ConnectorCustomizer:
(note that I am using Tomcat 8 here)
#Component
public class TomcatContainerCustomizer implements EmbeddedServletContainerCustomizer {
private static final Logger LOGGER = LoggerFactory.getLogger(TomcatContainerCustomizer.class);
#Override
public void customize(final ConfigurableEmbeddedServletContainer container) {
if (container instanceof TomcatEmbeddedServletContainerFactory) {
final TomcatEmbeddedServletContainerFactory tomcat = (TomcatEmbeddedServletContainerFactory) container;
tomcat.addConnectorCustomizers(connector -> {
connector.setScheme("https");
connector.setProxyPort(443);
});
LOGGER.info("Enabled secure scheme (https).");
} else {
LOGGER.warn("Could not change protocol scheme because Tomcat is not used as servlet container.");
}
}
}
The important thing is that you not only set the Scheme to https but also the ProxyPort without which all internal redirects from Spring Boot were routed to port 80.
The configuration property security.require-ssl doesn't work when basic authentication is disabled (at least on old versions of Spring Boot). So you probably need to secure all the requests manually with code similar to this one:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Inject private SecurityProperties securityProperties;
#Override
protected void configure(HttpSecurity http) throws Exception {
if (securityProperties.isRequireSsl()) http.requiresChannel().anyRequest().requiresSecure();
}
}
You can check my full answer here: Spring Boot redirect HTTP to HTTPS
You will need a keystore file and few config classes.
The below link explains it in detail.
Https on embedded tomcat
Spring Boot 2.0 redirection of http to https:
Add the following to the #Configuration
#Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
#Override
protected void postProcessContext(Context context) {
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
securityConstraint.addCollection(collection);
context.addConstraint(securityConstraint);
}
};
tomcat.addAdditionalTomcatConnectors(redirectConnector());
return tomcat;
}
private Connector redirectConnector() {
Connector connector = new Connector(
TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
connector.setScheme("http");
connector.setPort(8080);
connector.setSecure(false);
connector.setRedirectPort(8443);
return connector;
}

Resources