Restore support for empty persistence unit with fat jars

Due to the layout format change in 1.4, Spring Framework is no longer
able to compute a default persistence unit root URL. If a Spring Boot 1.4
application has JPA but does not have any entity, the application started
from a fat jar now fails with a quite cryptic exception.

This commit introduces `ApplicationInfo` as a general replacement for
the `ApplicationArguments` and `Banner` singleton beans that
`SpringApplication` registers on startup. `ApplicationInfo` also defines
the detected "main" `Class` that can be used to compute a last resort
URL that makes sense.

If such bean is available, `EntityManagerFactoryBuilder` now sets the
default persistence unit root location, preventing Spring Framework to
attempt to resolve an unknown location. Note that in our case the
persistence unit root location is actually useless: given the way the
persistence unit is created, nothing actually uses it but Hibernate, as a
compliant JPA provider, has to make sure this setting is set to a valid
URL nevertheless.

Closes gh-6635
This commit is contained in:
Stephane Nicoll 2016-08-23 08:28:55 +02:00
parent 86757efcdf
commit b9104c9337
8 changed files with 145 additions and 6 deletions

View File

@ -27,6 +27,7 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.ApplicationInfo;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@ -105,6 +106,9 @@ public abstract class JpaBaseConfiguration implements BeanFactoryAware {
jpaVendorAdapter, this.properties.getProperties(),
persistenceUnitManagerProvider.getIfAvailable());
builder.setCallback(getVendorCallback());
if (this.beanFactory.containsBean("springApplicationInfo")) {
builder.setApplicationInfo(this.beanFactory.getBean(ApplicationInfo.class));
}
return builder;
}

View File

@ -94,8 +94,8 @@ You can also use the `spring.main.banner-mode` property to determine if the bann
to be printed on `System.out` (`console`), using the configured logger (`log`) or not
at all (`off`).
The printed banner will be registered as a singleton bean under the name
`springBootBanner`.
The printed banner will be available via an `ApplicationInfo` registered as a singleton
bean under the name `springApplicationInfo`.
[NOTE]
====

View File

@ -11,9 +11,9 @@ welcome = { ->
// Try to print using the banner interface
if (beanFactory != null) {
try {
def banner = beanFactory.getBean("springBootBanner")
def appInfo = beanFactory.getBean("springApplicationInfo")
def out = new java.io.ByteArrayOutputStream()
banner.printBanner(environment, null, new java.io.PrintStream(out))
appInfo.banner.printBanner(environment, null, new java.io.PrintStream(out))
return out.toString()
} catch (Exception ex) {
// Ignore

View File

@ -0,0 +1,64 @@
/*
* Copyright 2012-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.boot;
/**
* Provide application-related information such as the {@link ApplicationArguments} and
* the {@link Banner}.
*
* @author Stephane Nicoll
* @since 1.4.1
*/
public final class ApplicationInfo {
private final Class<?> mainApplicationClass;
private final ApplicationArguments applicationArguments;
private final Banner banner;
protected ApplicationInfo(SpringApplication application, ApplicationArguments applicationArguments, Banner banner) {
this.mainApplicationClass = application.getMainApplicationClass();
this.applicationArguments = applicationArguments;
this.banner = banner;
}
/**
* Returns the main application class that has been deduced or explicitly configured.
* @return the main application class or {@code null}
*/
public Class<?> getMainApplicationClass() {
return this.mainApplicationClass;
}
/**
* Returns the {@link ApplicationArguments} used to start this instance.
* @return the application arguments
*/
public ApplicationArguments getApplicationArguments() {
return this.applicationArguments;
}
/**
* Returns the {@link Banner} used by this instance.
* @return the banner or {@code null}
*/
public Banner getBanner() {
return this.banner;
}
}

View File

@ -359,6 +359,10 @@ public class SpringApplication {
if (printedBanner != null) {
context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
}
ApplicationInfo applicationInfo = new ApplicationInfo(this, applicationArguments,
printedBanner);
context.getBeanFactory().registerSingleton("springApplicationInfo",
applicationInfo);
// Load the sources
Set<Object> sources = getSources();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-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.
@ -16,6 +16,7 @@
package org.springframework.boot.orm.jpa;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
@ -24,10 +25,15 @@ import java.util.Set;
import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.ApplicationInfo;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager;
import org.springframework.util.ClassUtils;
import org.springframework.util.ResourceUtils;
/**
* Convenient builder for JPA EntityManagerFactory instances. Collects common
@ -39,10 +45,13 @@ import org.springframework.util.ClassUtils;
*
* @author Dave Syer
* @author Phillip Webb
* @author Stephane Nicoll
* @since 1.3.0
*/
public class EntityManagerFactoryBuilder {
private static final Log logger = LogFactory.getLog(EntityManagerFactoryBuilder.class);
private JpaVendorAdapter jpaVendorAdapter;
private PersistenceUnitManager persistenceUnitManager;
@ -51,6 +60,8 @@ public class EntityManagerFactoryBuilder {
private EntityManagerFactoryBeanCallback callback;
private Class<?> applicationClass;
/**
* Create a new instance passing in the common pieces that will be shared if multiple
* EntityManagerFactory instances are created.
@ -78,6 +89,34 @@ public class EntityManagerFactoryBuilder {
this.callback = callback;
}
/**
* An optional {@link ApplicationInfo} used to further tune the entity manager.
* @param applicationInfo the application info
*/
public void setApplicationInfo(ApplicationInfo applicationInfo) {
this.applicationClass = applicationInfo.getMainApplicationClass();
}
/**
* Determine a persistence unit root location to use if no {@code persistence.xml} or
* {@code orm.xml} are present in the project.
* @return the persistence unit root location or {@code null}
*/
protected String determinePersistenceUnitRootLocation() {
if (this.applicationClass != null) {
try {
URL mainLocation = this.applicationClass.getProtectionDomain().
getCodeSource().getLocation();
return ResourceUtils.extractJarFileURL(mainLocation).toString();
}
catch (Exception ex) {
logger.info("Could not determine persistence unit root location: " + ex);
}
}
return null;
}
/**
* A fluent builder for a LocalContainerEntityManagerFactoryBean.
*/
@ -181,6 +220,10 @@ public class EntityManagerFactoryBuilder {
entityManagerFactoryBean.getJpaPropertyMap()
.putAll(EntityManagerFactoryBuilder.this.jpaProperties);
entityManagerFactoryBean.getJpaPropertyMap().putAll(this.properties);
String rootLocation = determinePersistenceUnitRootLocation();
if (rootLocation != null) {
entityManagerFactoryBean.setPersistenceUnitRootLocation(rootLocation);
}
if (EntityManagerFactoryBuilder.this.callback != null) {
EntityManagerFactoryBuilder.this.callback
.execute(entityManagerFactoryBean);

View File

@ -100,6 +100,9 @@ public class BannerTests {
application.setWebEnvironment(false);
this.context = application.run();
assertThat(this.context.containsBean("springBootBanner")).isTrue();
assertThat(this.context.containsBean("springApplicationInfo")).isTrue();
assertThat(this.context.getBean(
"springApplicationInfo", ApplicationInfo.class).getBanner()).isNotNull();
}
@Test
@ -109,7 +112,9 @@ public class BannerTests {
Banner banner = mock(Banner.class);
application.setBanner(banner);
this.context = application.run();
Banner printedBanner = (Banner) this.context.getBean("springBootBanner");
ApplicationInfo applicationInfo = this.context.getBean("springApplicationInfo",
ApplicationInfo.class);
Banner printedBanner = applicationInfo.getBanner();
assertThat(ReflectionTestUtils.getField(printedBanner, "banner"))
.isEqualTo(banner);
verify(banner).printBanner(any(Environment.class),
@ -127,6 +132,8 @@ public class BannerTests {
application.setWebEnvironment(false);
this.context = application.run();
assertThat(this.context.containsBean("springBootBanner")).isFalse();
assertThat(this.context.getBean("springApplicationInfo", ApplicationInfo.class).
getBanner()).isNull();
}
@Test
@ -143,6 +150,8 @@ public class BannerTests {
this.context = application.run();
assertThat(this.out.toString()).contains("I printed a deprecated banner");
assertThat(this.context.containsBean("springBootBanner")).isFalse();
assertThat(this.context.getBean("springApplicationInfo", ApplicationInfo.class).
getBanner()).isNull();
}
static class DummyBanner implements Banner {

View File

@ -778,6 +778,21 @@ public class SpringApplicationTests {
assertThat(System.getProperty("java.awt.headless")).isEqualTo("false");
}
@Test
public void getApplicationInfo() {
TestSpringApplication application = new TestSpringApplication(
ExampleConfig.class);
application.setWebEnvironment(false);
this.context = application.run("foo");
ApplicationInfo applicationInfo = this.context.getBean(ApplicationInfo.class);
assertThat(application.getMainApplicationClass()).isEqualTo(application
.getMainApplicationClass());
assertThat(applicationInfo.getApplicationArguments()).isNotNull();
assertThat(applicationInfo.getApplicationArguments().getNonOptionArgs())
.containsExactly("foo");
assertThat(applicationInfo.getBanner()).isNotNull();
}
@Test
public void getApplicationArgumentsBean() throws Exception {
TestSpringApplication application = new TestSpringApplication(