I am trying to read non-root elements from an XML file using Spring Batch.
The batch configuration I am using contains:
a StaxEventItemReader configured to read <dependency> elements
a Jaxb2Marshaller bound to JAXB-generated classes
How do I configure either StAX or JAXB to parse non-root elements as single Spring Batch items?
For example, let's say I need to process <dependency> elements from a Maven POM:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<packaging>...</packaging>
<dependencies>
<dependency>...</dependency>
<dependency>...</dependency>
<dependency>...</dependency>
...
</dependencies>
</project>
With the following code (I am showing only the relevant parts):
#Configuration
#EnableBatchProcessing
public class BatchConfiguration {
#Bean
public ItemReader<Dependency> reader(Jaxb2Marshaller marshaller) {
return new StaxEventItemReaderBuilder<Dependency>().name("itemReader")
.resource(inputFile)
.addFragmentRootElements("dependency")
.unmarshaller(marshaller)
.build();
}
#Bean
public Jaxb2Marshaller marshaller() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setPackagesToScan("org.apache.maven.pom._4_0");
return marshaller;
}
}
But I am getting the following error:
javax.xml.bind.UnmarshalException: unexpected element (uri:"http://maven.apache.org/POM/4.0.0", local:"dependency"). Expected elements are <{http://maven.apache.org/POM/4.0.0}project>
What am I missing?
I found a solution: I needed to call Jaxb2Marshaller.setMappedClass to enable partial unmarshalling:
#Bean
public Jaxb2Marshaller marshaller() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setPackagesToScan("org.apache.maven.pom._4_0");
marshaller.setMappedClass(Dependency.class); // ADD THIS LINE
return marshaller;
}
The pom defines a custom namespace xmlns="http://maven.apache.org/POM/4.0.0", so you need to prefix the fragment root element name with it in your reader's bean definition:
#Bean
public ItemReader<Dependency> reader(Jaxb2Marshaller marshaller) {
return new StaxEventItemReaderBuilder<Dependency>().name("itemReader")
.resource(inputFile)
.addFragmentRootElements("{http://maven.apache.org/POM/4.0.0}dependency")
.unmarshaller(marshaller)
.build();
}
Related
I am facing a issue with the circuit breaker implementation using Spring Cloud Resilience4j.
Following some tutorial, I have tried to add the necessary dependencies in the project.
Also, tried to add the configurations but, still the circuit is not opening and fallback method is not getting called.
For the use case, I am calling an external API from my service and if that external API is down then after few calls I need to enable the circuit breaker.
Please find the code pieces from the different files.
I am a newbie to circuit breaker pattern. Any help will be highly appreciated.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>11</java.version>
<spring-cloud.version>2020.0.4</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependencies>
</project>
Application properties
resilience4j.circuitbreaker.instances.test-api.register-health-indicator=true
resilience4j.circuitbreaker.instances.test-api.minimum-number-of-calls=4
resilience4j.circuitbreaker.instances.test-api.failure-rate-threshold=50
resilience4j.circuitbreaker.instances.test-api.permitted-number-of-calls-in-half-open-state=3
resilience4j.circuitbreaker.instances.test-api.wait-duration-in-open-state=30s
resilience4j.circuitbreaker.instances.test-api.automatic-transition-from-open-to-half-open-enabled=true
resilience4j.circuitbreaker.instances.test-api.record-exceptions=com.testapi.exception.ServiceUnavailableError
Service Class Code Piece
#CircuitBreaker(name = "test-api", fallbackMethod = "storeResponseFallback")
public TestResponse storeResponse(String apiURL, HttpEntity<String> entityrequest) {
TestResponse testResponse = new TestResponse();
Optional<ResponseEntity<TestResponse>> response = Optional.empty();
Future<ResponseEntity<TestResponse>> responseFuture;
ExecutorService executor = Executors.newFixedThreadPool(10);
log.debug("Calling Extrenal API, Request Body: {}", entityrequest.toString());
try {
//Service call returns a future
responseFuture = executor.submit(() -> restTemplate.postForEntity(apiURL, entityrequest, TestResponse.class));
response = Optional.ofNullable(responseFuture.get());
log.info("Got response from external API");
if ((response.isPresent()) && (response.get().hasBody())) {
testResponse = response.get().getBody();
}
} catch (Exception exception) {
log.error("External api call got failed with an error");
Thread.currentThread().interrupt();
throw new ServiceUnavailableError();
}
return testResponse;
}
public TestResponse storeResponseFallback(ServiceUnavailableError ex) {
log.error("Executing Fallback Method For General exceptions");
throw new ServiceUnavailableError();
}
ServiceUnavailableError Java file
#Data
#AllArgsConstructor
#NoArgsConstructor
public class ServiceUnavailableError extends RuntimeException{
private static final long serialVersionUID = 2382122402994502766L;
private String message;
}
The signature of your fallback method is wrong. It should contain all the parameters of the actual method ( in your case storeResponseFallback is the fallback method and storeResponse is the actual method), along with the exception. Please make sure to remove the try catch block. You do not want to handle the exception yourself, rather you should let circuit breaker to handle it for you.
Please take a look at the following code which is from given link
https://resilience4j.readme.io/docs/getting-started-3
#CircuitBreaker(name = BACKEND, fallbackMethod = "fallback")
public Mono<String> method(String param1) {
return Mono.error(new NumberFormatException());
}
private Mono<String> fallback(String param1, IllegalArgumentException e) {
return Mono.just("test");
}
Try using the following yaml file
I used the following configuration with your existing code,I used yaml instead of properties file. this seems to stay in open state and call only the fallback method.
resilience4j.circuitbreaker:
configs:
default:
slidingWindowSize: 4
permittedNumberOfCallsInHalfOpenState: 10
waitDurationInOpenState: 10000
failureRateThreshold: 60
eventConsumerBufferSize: 10
registerHealthIndicator: true
someShared:
slidingWindowSize: 3
permittedNumberOfCallsInHalfOpenState: 10
instances:
test-api:
baseConfig: default
waitDurationInOpenState: 500000
backendB:
baseConfig: someShared
Here is the updated fallback method
public TestResponse storeResponseFallback(String apiURL, String entityrequest, java.lang.Throwable t) {
log.error("Executing Fallback Method For General exceptions "+t.getMessage());
return new TestResponse("Frm Fallback");// Making sure to send a blank response
}
I have prepared the video, where I have defined main service and target service and I am preparing the bean from config and making use of Try.of() please check the video if it help.
you will see that the fallback method is working fine.
https://www.youtube.com/watch?v=8yJ0xek6l6Y&t=31s
After adding cache2k to my project some #SpringBootTest's stopped working with an error:
java.lang.IllegalStateException: Cache already created: 'cache'
Below I provide the minimal example to reproduce:
Go to start.spring.io and create a simplest Maven project with Cache starter, then add cache2k dependencies:
<properties>
<java.version>1.8</java.version>
<cache2k-version>1.2.2.Final</cache2k-version>
</properties>
<dependencies>
<dependency>
<groupId>org.cache2k</groupId>
<artifactId>cache2k-api</artifactId>
<version>${cache2k-version}</version>
</dependency>
<dependency>
<groupId>org.cache2k</groupId>
<artifactId>cache2k-core</artifactId>
<version>${cache2k-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.cache2k</groupId>
<artifactId>cache2k-spring</artifactId>
<version>${cache2k-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Now configure the simplest cache:
#SpringBootApplication
#EnableCaching
public class CachingDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CachingDemoApplication.class, args);
}
#Bean
public CacheManager springCacheManager() {
SpringCache2kCacheManager cacheManager = new SpringCache2kCacheManager();
cacheManager.addCaches(b -> b.name("cache"));
return cacheManager;
}
}
And add any service (which we will #MockBean in one of our tests:
#Service
public class SomeService {
public String getString() {
System.out.println("Executing service method");
return "foo";
}
}
Now two #SpringBootTest tests are required to reproduce the issue:
#SpringBootTest
#RunWith(SpringRunner.class)
public class SpringBootAppTest {
#Test
public void getString() {
System.out.println("Empty test");
}
}
#RunWith(SpringRunner.class)
#SpringBootTest
public class WithMockedBeanTest {
#MockBean
SomeService service;
#Test
public void contextLoads() {
}
}
Notice that the 2nd test has mocked #MockBean. This causes an error (stacktrace below).
Caused by: java.lang.IllegalStateException: Cache already created: 'cache'
at org.cache2k.core.CacheManagerImpl.newCache(CacheManagerImpl.java:174)
at org.cache2k.core.InternalCache2kBuilder.buildAsIs(InternalCache2kBuilder.java:239)
at org.cache2k.core.InternalCache2kBuilder.build(InternalCache2kBuilder.java:182)
at org.cache2k.core.Cache2kCoreProviderImpl.createCache(Cache2kCoreProviderImpl.java:215)
at org.cache2k.Cache2kBuilder.build(Cache2kBuilder.java:837)
at org.cache2k.extra.spring.SpringCache2kCacheManager.buildAndWrap(SpringCache2kCacheManager.java:205)
at org.cache2k.extra.spring.SpringCache2kCacheManager.lambda$addCache$2(SpringCache2kCacheManager.java:143)
at java.util.concurrent.ConcurrentHashMap.compute(ConcurrentHashMap.java:1853)
at org.cache2k.extra.spring.SpringCache2kCacheManager.addCache(SpringCache2kCacheManager.java:141)
at org.cache2k.extra.spring.SpringCache2kCacheManager.addCaches(SpringCache2kCacheManager.java:132)
at com.example.cachingdemo.CachingDemoApplication.springCacheManager(CachingDemoApplication.java:23)
at com.example.cachingdemo.CachingDemoApplication$$EnhancerBySpringCGLIB$$2dce99ca.CGLIB$springCacheManager$0(<generated>)
at com.example.cachingdemo.CachingDemoApplication$$EnhancerBySpringCGLIB$$2dce99ca$$FastClassBySpringCGLIB$$bbd240c0.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244)
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:363)
at com.example.cachingdemo.CachingDemoApplication$$EnhancerBySpringCGLIB$$2dce99ca.springCacheManager(<generated>)
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:498)
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154)
... 52 more
If you remove #MockBean, both tests will pass.
How can I avoid this error in my test suite?
Your second test represents a different ApplicationContext altogether so the test framework will initiate a dedicated one for it. If cache2k is stateful (for instance sharing the CacheManager for a given classloader if it already exists), the second context will attempt to create a new CacheManager while the first one is still active.
You either need to flag one of the test as dirty (see #DirtiesContext) which will close the context and shut down the CacheManager, or you can replace the cache infrastructure by an option that does not require all that, see #AutoConfigureCache.
If cache2k works in such a way that it requires you to dirty the context, I'd highly recommend to swap it using the later options.
Since I do not want any custom behavior in test, but just want to get rid of this error, the solution is to create CacheManager using unique name like this:
#Bean
public CacheManager springCacheManager() {
SpringCache2kCacheManager cacheManager = new SpringCache2kCacheManager("spring-" + hashCode());
cacheManager.addCaches(b -> b.name("cache"));
return cacheManager;
}
I encountered the same error when using cache2k with Spring Dev Tools, and ended up with the following code as the solution:
#Bean
public CacheManager cacheManager() {
SpringCache2kCacheManager cacheManager = new SpringCache2kCacheManager();
// To avoid the "Caused by: java.lang.IllegalStateException: Cache already created:"
// error when Spring DevTools is enabled and code reloaded
if (cacheManager.getCacheNames().stream()
.filter(name -> name.equals("cache"))
.count() == 0) {
cacheManager.addCaches(
b -> b.name("cache")
);
}
return cacheManager;
}
I have 2 Eclipse projects and each one is has services managed by Spring. I use Spring Boot starter dependencies for each of them. Each one works properly, and can be tested with JUnit launched via SpringRunner.class and #SpringBootTest.
Now, I want to call some services from project 1 in project 2, so I add a dependency in project 2 pom.xml and I add
#ComponentScan(basePackages="com.project1")
From then on, I can't launch any JUnit, it complains about dataSource not being set, like if configs where mixing randomly.
My question is : what are the recommended practices when you create a Spring Boot App and you want to isolate some features in a separate project (here XML features) ? If u can't have 2 spring boot app with one dependant of the other, what are the spring dependencies you need so the spring boot project can deal with the non spring boot dependency, and so that u can still launch JUnit using Spring runner locally ?
Do I need to pick Spring dependencies one by one (core, bean, context, test, log4j, slf4j, junit, hamcrest, ...) like before Spring boot exist to do this ?
See my comment on why the possible duplicate is different.
After removing all Spring boot dependencies from my module project, I still have the error as soon as I add the "ComponentScan" to scan the module services.
Here is my DB config (main project depending on a xml module) to be clear on the package config. This config WORKS perfectly until I add the ComponentScan on a package from the module project :
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(basePackages="fr.my.project.repository")
class PersistenceContext {
private static final String[] ENTITY_PACKAGES = { "fr.my.project.model" };
private static final String PROP_DB_DRIVER_CLASS = "db.driver";
private static final String PROP_DB_PASSWORD = "db.password";
private static final String PROP_DB_URL = "db.url";
private static final String PROP_DB_USER = "db.username";
private static final String PROP_HIBERNATE_DIALECT = "hibernate.dialect";
private static final String PROP_HIBERNATE_FORMAT_SQL = "hibernate.format_sql";
private static final String PROP_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto";
private static final String PROP_HIBERNATE_SHOW_SQL = "hibernate.show_sql";
/**
* Creates and configures the HikariCP datasource bean.
*
* #param env
* The runtime environment of our application.
* #return
*/
#Bean(destroyMethod = "close")
DataSource dataSource(Environment env) {
HikariConfig dataSourceConfig = new HikariConfig();
dataSourceConfig.setDriverClassName(env.getRequiredProperty(PROP_DB_DRIVER_CLASS));
dataSourceConfig.setJdbcUrl(env.getRequiredProperty(PROP_DB_URL));
dataSourceConfig.setUsername(env.getRequiredProperty(PROP_DB_USER));
dataSourceConfig.setPassword(env.getRequiredProperty(PROP_DB_PASSWORD));
return new HikariDataSource(dataSourceConfig);
}
/**
* Creates the bean that creates the JPA entity manager factory.
*
* #param dataSource
* The datasource that provides the database connections.
* #param env
* The runtime environment of our application.
* #return
*/
#Bean
LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, Environment env) {
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource);
entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
entityManagerFactoryBean.setPackagesToScan(ENTITY_PACKAGES);
Properties jpaProperties = new Properties();
// Configures the used database dialect. This allows Hibernate to create SQL
// that is optimized for the used database.
jpaProperties.put(PROP_HIBERNATE_DIALECT, env.getRequiredProperty(PROP_HIBERNATE_DIALECT));
// Specifies the action that is invoked to the database when the Hibernate
// SessionFactory is created or closed.
jpaProperties.put(PROP_HIBERNATE_HBM2DDL_AUTO, env.getRequiredProperty(PROP_HIBERNATE_HBM2DDL_AUTO));
// If the value of this property is true, Hibernate writes all SQL
// statements to the console.
jpaProperties.put(PROP_HIBERNATE_SHOW_SQL, env.getRequiredProperty(PROP_HIBERNATE_SHOW_SQL));
// If the value of this property is true, Hibernate will use prettyprint
// when it writes SQL to the console.
jpaProperties.put(PROP_HIBERNATE_FORMAT_SQL, env.getRequiredProperty(PROP_HIBERNATE_FORMAT_SQL));
entityManagerFactoryBean.setJpaProperties(jpaProperties);
return entityManagerFactoryBean;
}
/**
* Creates the transaction manager bean that integrates the used JPA provider with the Spring transaction mechanism.
*
* #param entityManagerFactory
* The used JPA entity manager factory.
* #return
*/
#Bean
JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
}
and after adding :
#ComponentScan(basePackages="fr.my.module.xml.service")
I get this error when launching any Junit :
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.apache.tomcat.jdbc.pool.DataSource]: Factory method 'dataSource' threw exception; nested exception is org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Cannot determine embedded database driver class for database type NONE. If you want an embedded database please put a supported one on the classpath. If you have database settings to be loaded from a particular profile you may need to active it (no profiles are currently active).
Here is a temporary answer on how to configure the dependency project, but I hope some easier way benefiting of Spring Boot shortcuts for all app modules exist.
pom.xml with manual minimal dependencies :
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.14.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.14.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.11</version>
</dependency>
Manual test config :
#RunWith(SpringRunner.class)
#ContextConfiguration(loader=AnnotationConfigContextLoader.class, classes=AppConfig.class)
public class XmlTest {
Manual app config :
#Configuration
#ComponentScan(basePackages="my.package.xml")
public class AppConfig {
}
Sooooooo after doing all these tries, Spring Boot may not be the cause of this problem at all.
The thing is I was adding #ComponentScan(basePackages="fr.package.xml") hoping to complete the default package scanning, but it was overriding it.
The proper way to add a package, is to redeclare explicitely the default package before adding the new package :
#ComponentScan(basePackages={"fr.package.xml", "fr.package.persistence"})
My other answer was about setting up manual minimal dependencies for a module in a Spring Boot app. But here is an example of using Spring boot special dependencies in the module which is not the main app :
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Then, you don't declare "#SpringBootApplication" in a main class in src/main/java where it may break the global packaging, but you set it up inside your test class :
#RunWith(SpringRunner.class)
#SpringBootTest("service.message=Hello")
public class MyServiceTest {
#Autowired
private MyService myService;
#Test
public void contextLoads() {
assertThat(myService.message()).isNotNull();
}
#SpringBootApplication
static class TestConfiguration {
}
}
source : https://github.com/spring-guides/gs-multi-module/tree/master/complete
For my spring boot application I use annotation based configuration and a WebApplicationInitalizer.
One of my dependencies provides a spring configuration in an xml, included in the jar. I use #ImportResource to load the context xml. This seems to work, except for the fact that inside this xml, there are property placeholders, for example ${poolsize:10}
Apparently, spring does not automatically replace these placeholders (I get a NumberFormatException). Is there some extra configuration I need to add?
Our startup class:
public class Application implements WebApplicationInitializer {
#Override
public void onStartup(ServletContext container) throws ServletException {
// Create the 'root' Spring application context
AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
// config
rootContext.register(JmsConfiguration.class);
// Manage the lifecycle of the root application context
container.addListener(new ContextLoaderListener(rootContext));
}
}
And the configuration class (we use spring-jms):
#Configuration
#EnableJms
#ComponentScan(basePackages = { "..." })
public class JmsConfiguration implements JmsListenerConfigurer {
// config for jms listener and jaxb, nothing to do with property handling
}
Perhaps I'm mistaken in thinking that using a WebapplicationInitializer is how to use spring boot. Perhaps we don't even need spring boot? The only spring boot related dependency we use is:
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Spring dependencies we use:
org.springframework:spring-context:jar:4.3.8.RELEASE:compile
org.springframework:spring-jms:jar:4.3.8.RELEASE:compile
org.springframework:spring-oxm:jar:4.3.8.RELEASE:compile
org.springframework:spring-context:jar:4.3.8.RELEASE:compile
org.springframework:spring-beans:jar:4.3.8.RELEASE:compile
I figured it out thanks #M. Deinum
We don't need spring boot to use the WebApplicationInitializer, but (at least without spring boot) we have to declare our own PropertySourcesPlaceholderConfigurer:
#Configuration
#ImportResource(locations = {"classpath*:/library-context.xml"})
public class MyConfiguration {
#Bean
public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
NB. This works out of the box in spring boot thanks to #EnableAutoConfiguration and #PropertyPlaceholderAutoConfiguration which contains the exact same PropertySourcesPlaceholderConfigurer bean
I'm generating a plethora of Java files from http://www.ncpdp.org's XSD files (only available to members). After generating them, I'd like to use them in my Spring Controllers, but I'm having problems getting responses converted to XML.
I've tried returning the element itself, as well as JAXBElement<T>, but neither seems to work. The test below fails:
java.lang.AssertionError: Status
Expected :200
Actual :406
#Test
public void testHelloWorld() throws Exception {
mockMvc.perform(get("/api/message")
.accept(MediaType.APPLICATION_XML)
.contentType(MediaType.APPLICATION_XML))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_XML));
}
Here's my Controller:
import org.ncpdp.schema.transport.MessageType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class HelloWorldController {
#RequestMapping(value = "/api/message", method = RequestMethod.GET)
public MessageType messageType() {
return new MessageType();
}
}
I've tried creating a MvcConfig to override Spring Boot's MVC config, but it doesn't seem to be working.
#Configuration
#EnableWebMvc
public class MvcConfig extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(marshallingHttpMessageConverter());
}
#Bean
public MarshallingHttpMessageConverter marshallingHttpMessageConverter() {
Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
jaxb2Marshaller.setPackagesToScan(new String[]{"com.ncpdb.schema.transport"});
MarshallingHttpMessageConverter converter = new MarshallingHttpMessageConverter();
converter.setMarshaller(jaxb2Marshaller);
converter.setUnmarshaller(jaxb2Marshaller);
converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_XML));
return converter;
}
}
What do I need to do to get Spring MVC to marshall my generated JAXB objects as XML?
I was able to solve this by creating a bindings.xjb file in the same directory as my schemas. This causes JAXB to generate #XmlRootElement on classes.
<?xml version="1.0"?>
<jxb:bindings version="1.0"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:jxb="http://java.sun.com/xml/ns/jaxb"
xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_0.xsd">
<jxb:bindings schemaLocation="transport.xsd" node="/xsd:schema">
<jxb:globalBindings>
<xjc:simple/>
</jxb:globalBindings>
</jxb:bindings>
</jxb:bindings>
To add namespaces prefixes to the returned XML, I had to modify the maven-jaxb2-plugin to add a couple arguments.
<arg>-extension</arg>
<arg>-Xnamespace-prefix</arg>
And add a dependency:
<dependencies>
<dependency>
<groupId>org.jvnet.jaxb2_commons</groupId>
<artifactId>jaxb2-namespace-prefix</artifactId>
<version>1.1</version>
</dependency>
</dependencies>
Then modify my bindings.xjb to include this:
<?xml version="1.0"?>
<jxb:bindings version="1.0"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:jxb="http://java.sun.com/xml/ns/jaxb"
xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:namespace="http://jaxb2-commons.dev.java.net/namespace-prefix"
xsi:schemaLocation="http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_0.xsd
http://jaxb2-commons.dev.java.net/namespace-prefix http://java.net/projects/jaxb2-commons/sources/svn/content/namespace-prefix/trunk/src/main/resources/prefix-namespace-schema.xsd">
<jxb:bindings schemaLocation="transport.xsd" node="/xsd:schema">
<jxb:globalBindings>
<xjc:simple/>
</jxb:globalBindings>
<jxb:schemaBindings>
<jxb:package name="org.ncpdp.schema.transport"/>
</jxb:schemaBindings>
<jxb:bindings>
<namespace:prefix name="transport"/>
</jxb:bindings>
</jxb:bindings>
</jxb:bindings>
I learned how to do this from https://java.net/projects/jaxb2-commons/pages/Namespace-prefix. I also found http://blog.frankel.ch/customize-your-jaxb-bindings to be a good resource on how to customize JAXB bindings.