From 9bf1fe4685b034243d756ad58f6c26addd534fb5 Mon Sep 17 00:00:00 2001 From: Ryan Baxter Date: Tue, 27 Jun 2017 15:47:11 -0400 Subject: [PATCH] Implementation of factories to provide Apache HTTP Clients --- spring-cloud-commons/pom.xml | 23 +++++ ...cheHttpClientConnectionManagerFactory.java | 47 ++++++++++ .../httpclient/ApacheHttpClientFactory.java | 38 ++++++++ ...cheHttpClientConnectionManagerFactory.java | 85 ++++++++++++++++++ .../DefaultApacheHttpClientFactory.java | 22 +++++ .../httpclient/HttpClientConfiguration.java | 30 +++++++ .../main/resources/META-INF/spring.factories | 3 +- .../CustomHttpClientConfigurationTests.java | 87 +++++++++++++++++++ ...tpClientConnectionManagerFactoryTests.java | 46 ++++++++++ .../DefaultApacheHttpClientFactoryTests.java | 42 +++++++++ .../DefaultHttpClientConfigurationTests.java | 46 ++++++++++ 11 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/ApacheHttpClientConnectionManagerFactory.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/ApacheHttpClientFactory.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientConnectionManagerFactory.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientFactory.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/HttpClientConfiguration.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/CustomHttpClientConfigurationTests.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientConnectionManagerFactoryTests.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientFactoryTests.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/DefaultHttpClientConfigurationTests.java diff --git a/spring-cloud-commons/pom.xml b/spring-cloud-commons/pom.xml index 7b120d36..82a678a9 100644 --- a/spring-cloud-commons/pom.xml +++ b/spring-cloud-commons/pom.xml @@ -35,6 +35,11 @@ + + + 3.6.0 + 4.5.1 + org.springframework.boot @@ -102,6 +107,24 @@ compile true + + com.squareup.okhttp3 + okhttp + ${okhttp3.version} + true + + + com.squareup.okhttp3 + logging-interceptor + ${okhttp3.version} + true + + + org.apache.httpcomponents + httpclient + ${apachehttpclient.version} + true + org.springframework.boot spring-boot-starter-test diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/ApacheHttpClientConnectionManagerFactory.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/ApacheHttpClientConnectionManagerFactory.java new file mode 100644 index 00000000..55e1b167 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/ApacheHttpClientConnectionManagerFactory.java @@ -0,0 +1,47 @@ +/* + * + * * Copyright 2013-2016 the original author or authors. + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.springframework.cloud.commons.httpclient; + +import java.util.concurrent.TimeUnit; + +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.HttpClientConnectionManager; + +/** + * Interface for creating an {@link HttpClientConnectionManager}. + * @author Ryan Baxter + */ +public interface ApacheHttpClientConnectionManagerFactory { + public static final String HTTP_SCHEME = "http"; + public static final String HTTPS_SCHEME = "https"; + + /** + * Creates a new {@link HttpClientConnectionManager}. + * @param disableSslValidation True to disable SSL validation, false otherwise + * @param maxTotalConnections The total number of connections + * @param maxConnectionsPerRoute The total number of connections per route + * @param timeToLive The time a connection is allowed to exist + * @param timeUnit The time unit for the time to live value + * @param registryBuilder The {@link RegistryBuilder} to use in the connection manager + * @return A new {@link HttpClientConnectionManager} + */ + public HttpClientConnectionManager newConnectionManager(boolean disableSslValidation, + int maxTotalConnections, int maxConnectionsPerRoute, + long timeToLive, TimeUnit timeUnit, RegistryBuilder registryBuilder); +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/ApacheHttpClientFactory.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/ApacheHttpClientFactory.java new file mode 100644 index 00000000..6dfa3a3c --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/ApacheHttpClientFactory.java @@ -0,0 +1,38 @@ +/* + * + * * Copyright 2013-2016 the original author or authors. + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.springframework.cloud.commons.httpclient; + +import org.apache.http.client.config.RequestConfig; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.impl.client.CloseableHttpClient; + +/** + * Factory for creating a new {@link CloseableHttpClient}. + * @author Ryan Baxter + */ +public interface ApacheHttpClientFactory { + + /** + * Creates a new {@link CloseableHttpClient}. + * @param requestConfig Configuration to be used for all requests by default + * @param connectionManager The {@link HttpClientConnectionManager} to use for the client + * @return A new {@link CloseableHttpClient} + */ + public CloseableHttpClient createClient(RequestConfig requestConfig, HttpClientConnectionManager connectionManager); +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientConnectionManagerFactory.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientConnectionManagerFactory.java new file mode 100644 index 00000000..18939595 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientConnectionManagerFactory.java @@ -0,0 +1,85 @@ +package org.springframework.cloud.commons.httpclient; + +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.apache.commons.logging.LogFactory; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.commons.logging.Log; + +/** + * Default implementation of {@link ApacheHttpClientConnectionManagerFactory}. + * @author Ryan Baxter + */ +public class DefaultApacheHttpClientConnectionManagerFactory implements ApacheHttpClientConnectionManagerFactory{ + + private static final Log LOG = LogFactory.getLog(DefaultApacheHttpClientConnectionManagerFactory.class); + + public HttpClientConnectionManager newConnectionManager(boolean disableSslValidation, + int maxTotalConnections, + int maxConnectionsPerRoute) { + return newConnectionManager(disableSslValidation, maxTotalConnections, maxConnectionsPerRoute, + -1, TimeUnit.MILLISECONDS, null); + } + + @Override + public HttpClientConnectionManager newConnectionManager(boolean disableSslValidation, + int maxTotalConnections, int maxConnectionsPerRoute, long timeToLive, + TimeUnit timeUnit, RegistryBuilder registryBuilder) { + if (registryBuilder == null) { + registryBuilder = RegistryBuilder. create() + .register(HTTP_SCHEME, PlainConnectionSocketFactory.INSTANCE); + } + if (disableSslValidation) { + try { + final SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, new TrustManager[] { new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, + String s) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, + String s) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + } }, new SecureRandom()); + registryBuilder.register(HTTPS_SCHEME, new SSLConnectionSocketFactory( + sslContext, NoopHostnameVerifier.INSTANCE)); + } + catch (NoSuchAlgorithmException e) { + LOG.warn("Error creating SSLContext", e); + } + catch (KeyManagementException e) { + LOG.warn("Error creating SSLContext", e); + } + + } + final Registry registry = registryBuilder.build(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager( + registry, null, null, null, timeToLive, timeUnit); + connectionManager.setMaxTotal(maxTotalConnections); + connectionManager.setDefaultMaxPerRoute(maxConnectionsPerRoute); + + return connectionManager; + } +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientFactory.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientFactory.java new file mode 100644 index 00000000..c4e895d8 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientFactory.java @@ -0,0 +1,22 @@ +package org.springframework.cloud.commons.httpclient; + +import org.apache.http.client.config.RequestConfig; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; + +/** + * Default implementation of {@link ApacheHttpClientFactory}. + * @author Ryan Baxter + */ +public class DefaultApacheHttpClientFactory implements ApacheHttpClientFactory { + + public CloseableHttpClient createClient(RequestConfig requestConfig, HttpClientConnectionManager connectionManager) { + return HttpClientBuilder.create().disableContentCompression() + .setDefaultRequestConfig(requestConfig) + .setConnectionManager(connectionManager) + .disableCookieManagement() + .useSystemProperties() + .build(); + } +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/HttpClientConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/HttpClientConfiguration.java new file mode 100644 index 00000000..d0e70c3c --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/httpclient/HttpClientConfiguration.java @@ -0,0 +1,30 @@ +package org.springframework.cloud.commons.httpclient; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Ryan Baxter + */ +@Configuration +public class HttpClientConfiguration { + + @Configuration + @ConditionalOnProperty(name = "spring.cloud.httpclient.apache.enabled", matchIfMissing = true) + static class ApacheHttpClientConfiguration { + + @Bean + @ConditionalOnMissingBean + public ApacheHttpClientConnectionManagerFactory connManFactory() { + return new DefaultApacheHttpClientConnectionManagerFactory(); + } + + @Bean + @ConditionalOnMissingBean + public ApacheHttpClientFactory apacheHttpClientFactory() { + return new DefaultApacheHttpClientFactory(); + } + } +} diff --git a/spring-cloud-commons/src/main/resources/META-INF/spring.factories b/spring-cloud-commons/src/main/resources/META-INF/spring.factories index 4e17ff9a..5a433f60 100644 --- a/spring-cloud-commons/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-commons/src/main/resources/META-INF/spring.factories @@ -7,7 +7,8 @@ org.springframework.cloud.client.loadbalancer.AsyncLoadBalancerAutoConfiguration org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration,\ org.springframework.cloud.client.serviceregistry.ServiceRegistryAutoConfiguration,\ org.springframework.cloud.commons.util.UtilAutoConfiguration,\ -org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration +org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration,\ +org.springframework.cloud.commons.httpclient.HttpClientConfiguration # Environment Post Processors diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/CustomHttpClientConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/CustomHttpClientConfigurationTests.java new file mode 100644 index 00000000..4bd1167c --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/CustomHttpClientConfigurationTests.java @@ -0,0 +1,87 @@ +package org.springframework.cloud.commons.httpclient; + +import java.util.concurrent.TimeUnit; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.impl.client.CloseableHttpClient; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.assertTrue; + +/** + * @author Ryan Baxter + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = CustomApplication.class) +public class CustomHttpClientConfigurationTests { + + @Autowired + ApacheHttpClientFactory httpClientFactory; + + @Autowired + ApacheHttpClientConnectionManagerFactory connectionManagerFactory; + + @Test + public void connManFactory() throws Exception { + assertTrue(ApacheHttpClientConnectionManagerFactory.class.isInstance(connectionManagerFactory)); + assertTrue(CustomApplication.MyApacheHttpClientConnectionManagerFactory.class.isInstance(connectionManagerFactory)); + } + + @Test + public void apacheHttpClientFactory() throws Exception { + assertTrue(ApacheHttpClientFactory.class.isInstance(httpClientFactory)); + assertTrue(CustomApplication.MyApacheHttpClientFactory.class.isInstance(httpClientFactory)); + } + + +} + +@Configuration +@EnableAutoConfiguration +class CustomApplication { + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + + @Configuration + static class MyConfig { + + @Bean + public ApacheHttpClientFactory clientFactory() { + return new MyApacheHttpClientFactory(); + } + + @Bean ApacheHttpClientConnectionManagerFactory connectionManagerFactory() { + return new MyApacheHttpClientConnectionManagerFactory(); + } + + } + + static class MyApacheHttpClientFactory implements ApacheHttpClientFactory { + + @Override + public CloseableHttpClient createClient(RequestConfig requestConfig, HttpClientConnectionManager connectionManager) { + return null; + } + } + + static class MyApacheHttpClientConnectionManagerFactory implements ApacheHttpClientConnectionManagerFactory { + + @Override + public HttpClientConnectionManager newConnectionManager(boolean disableSslValidation, int maxTotalConnections, int maxConnectionsPerRoute, long timeToLive, TimeUnit timeUnit, RegistryBuilder registryBuilder) { + return null; + } + } +} + + diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientConnectionManagerFactoryTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientConnectionManagerFactoryTests.java new file mode 100644 index 00000000..a202438a --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientConnectionManagerFactoryTests.java @@ -0,0 +1,46 @@ +package org.springframework.cloud.commons.httpclient; + +import java.lang.reflect.Field; +import java.util.concurrent.TimeUnit; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.junit.Test; +import org.springframework.util.ReflectionUtils; + +import static org.junit.Assert.*; + +/** + * @author Ryan Baxter + */ +public class DefaultApacheHttpClientConnectionManagerFactoryTests { + @Test + public void newConnectionManager() throws Exception { + HttpClientConnectionManager connectionManager = new DefaultApacheHttpClientConnectionManagerFactory().newConnectionManager( + false, 2, 6); + assertEquals(6, ((PoolingHttpClientConnectionManager)connectionManager).getDefaultMaxPerRoute()); + assertEquals(2,((PoolingHttpClientConnectionManager)connectionManager).getMaxTotal()); + Object pool = getField(((PoolingHttpClientConnectionManager)connectionManager), "pool"); + assertEquals(-1l, getField(pool, "timeToLive")); + TimeUnit timeUnit = getField(pool, "tunit"); + assertEquals(TimeUnit.MILLISECONDS, timeUnit); + } + + @Test + public void newConnectionManagerWithTTL() throws Exception { + HttpClientConnectionManager connectionManager = new DefaultApacheHttpClientConnectionManagerFactory().newConnectionManager( + false, 2, 6, 56l, TimeUnit.DAYS, null); + assertEquals(6, ((PoolingHttpClientConnectionManager)connectionManager).getDefaultMaxPerRoute()); + assertEquals(2,((PoolingHttpClientConnectionManager)connectionManager).getMaxTotal()); + Object pool = getField(((PoolingHttpClientConnectionManager)connectionManager), "pool"); + assertEquals(56l, getField(pool, "timeToLive")); + TimeUnit timeUnit = getField(pool, "tunit"); + assertEquals(TimeUnit.DAYS, timeUnit); + } + + protected T getField(Object target, String name) { + Field field = ReflectionUtils.findField(target.getClass(), name); + ReflectionUtils.makeAccessible(field); + Object value = ReflectionUtils.getField(field, target); + return (T)value; + } +} \ No newline at end of file diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientFactoryTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientFactoryTests.java new file mode 100644 index 00000000..dd7d2398 --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/DefaultApacheHttpClientFactoryTests.java @@ -0,0 +1,42 @@ +package org.springframework.cloud.commons.httpclient; + +import java.lang.reflect.Field; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.Configurable; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.impl.client.CloseableHttpClient; +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.springframework.util.ReflectionUtils; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +/** + * @author Ryan Baxter + */ +public class DefaultApacheHttpClientFactoryTests { + @Test + public void createClient() throws Exception { + final RequestConfig requestConfig = RequestConfig.custom() + .setSocketTimeout(100) + .setConnectTimeout(200) + .setCookieSpec(CookieSpecs.IGNORE_COOKIES).build(); + CloseableHttpClient httpClient = new DefaultApacheHttpClientFactory().createClient(requestConfig, + mock(HttpClientConnectionManager.class)); + Assertions.assertThat(httpClient).isInstanceOf(Configurable.class); + RequestConfig config = ((Configurable) httpClient).getConfig(); + assertEquals(100, config.getSocketTimeout()); + assertEquals(200, config.getConnectTimeout()); + assertEquals(CookieSpecs.IGNORE_COOKIES, config.getCookieSpec()); + } + + protected T getField(Object target, String name) { + Field field = ReflectionUtils.findField(target.getClass(), name); + ReflectionUtils.makeAccessible(field); + Object value = ReflectionUtils.getField(field, target); + return (T)value; + } + +} \ No newline at end of file diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/DefaultHttpClientConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/DefaultHttpClientConfigurationTests.java new file mode 100644 index 00000000..b4cfe27a --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/httpclient/DefaultHttpClientConfigurationTests.java @@ -0,0 +1,46 @@ +package org.springframework.cloud.commons.httpclient; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.*; + +/** + * @author Ryan Baxter + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = MyApplication.class) +public class DefaultHttpClientConfigurationTests { + @Autowired + ApacheHttpClientFactory httpClientFactory; + + @Autowired + ApacheHttpClientConnectionManagerFactory connectionManagerFactory; + + @Test + public void connManFactory() throws Exception { + assertTrue(ApacheHttpClientConnectionManagerFactory.class.isInstance(connectionManagerFactory)); + assertTrue(DefaultApacheHttpClientConnectionManagerFactory.class.isInstance(connectionManagerFactory)); + } + + @Test + public void apacheHttpClientFactory() throws Exception { + assertTrue(ApacheHttpClientFactory.class.isInstance(httpClientFactory)); + assertTrue(DefaultApacheHttpClientFactory.class.isInstance(httpClientFactory)); + } +} + +@Configuration +@EnableAutoConfiguration +class MyApplication { + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } +} \ No newline at end of file