How to build a SOAP WS with Apache CXF + Spring Boot in Gradle? - spring-boot

The assignment was simple: A SOAP web service implemented with spring boot, JDBC using Gradle.
After some time looking around the discovery was made that "Spring-WS" only works with a contract-first development style.
And we didn't want that, so we dig a little further and found out what we already know, we had to use Apache CXF for a Contract Last development style.
So off we went to search, code and test; but once the data access and facades were done we couldn’t figure out how to wire the Apache CXF WS with the Spring Boot service Façade.
So… how is it done?

This is more of a rhetorical question, because after looking around we could not find an example of Spring Boot & Apache CXF working seamlessly together, so for anyone who may be searching, here is a simple example.
First the dependencies used by the Gradle project
build.gradle file
buildscript {
ext {
springBootVersion = '2.0.1.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse-wtp'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'war'
group = 'com.telcel'
version = '0.0.1-RC'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
configurations {
providedRuntime
}
dependencies {
// Apache CXF
compile(group: 'org.apache.cxf', name: 'cxf-spring-boot-starter-jaxws', version: '3.1.15') {
exclude(module: 'spring-boot-starter-tomcat')
}
// JDBC support
compile('org.springframework.boot:spring-boot-starter-jdbc')
// embedded servlet container
compile group: 'org.springframework.boot', name: 'spring-boot-starter-undertow', version: '1.5.4.RELEASE'
runtime group: 'com.ibm.informix', name: 'jdbc', version: '4.10.10.0'
testCompile('org.springframework.boot:spring-boot-starter-test')
testRuntime group: 'com.ibm.informix', name: 'jdbc', version: '4.10.10.0'
}
Then, we need some basic things for the CXF config.
application.properties file:
cxf.path=/service
server.address=0.0.0.0
We needed Spring Boot to create a CXF Endpoint, and we also needed that Endpoint to use our Spring aware Facade... this is where the wiring magic happened.
WebServiceConfig.java
package com.telcel.validaserie;
import com.telcel.validaserie.ui.ValidaSerieEndpoint;
import org.apache.cxf.Bus;
import org.apache.cxf.jaxws.EndpointImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.xml.ws.Endpoint;
#Configuration
public class WebServiceConfig {
#Autowired
private Bus bus;
#Autowired
private ValidaSerieEndpoint validaSerieEndpoint;
#Bean
public Endpoint endpoint() {
EndpointImpl endpoint = new EndpointImpl(bus, validaSerieEndpoint);
endpoint.publish("/");
return endpoint;
}
}
Notice the autowired ValidaSerieEndpoint that goes as a parameter into the EndpointImpl constructor, that's the trick, plain simple.
Finally just a simple web service implementation exposed as a Spring Bean (notice the Spring #Service stereotype)
ValidaSerieEndpoint.class
package com.telcel.validaserie.ui;
import com.telcel.validaserie.servicios.ValidaSeriesFacade;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
#Service
#WebService
public class ValidaSerieEndpoint {
#Autowired
private ValidaSeriesFacade validaSeriesFacade;
#WebMethod
public String validaTelefonoIccid(#WebParam(name = "iccid") String iccid) {
return validaSeriesFacade.validaTelefonoIccid(iccid);
}
#WebMethod
public String validaTelefonoImei(#WebParam(name = "imei") String imei) {
return validaSeriesFacade.validaTelefonoImei(imei);
}
#WebMethod
public int validaFacturaIccid(#WebParam(name = "iccid") String iccid, #WebParam(name = "fuerza-venta") String fuerzaVenta) {
return validaSeriesFacade.validaFacturaIccid(iccid, fuerzaVenta);
}
#WebMethod
public int validaFacturaImei(#WebParam(name = "imei") String imei, #WebParam(name = "fuerza-venta") String fuerzaVenta) {
return validaSeriesFacade.validaFacturaImei(imei, fuerzaVenta);
}
}
And that's it quite simple after you look at it... hope this helps.

Related

How resolve error injecting bean MapStruct in Spring

I'm trying to Inject my mapper using mapstruct, but spring doesn't recognize the bean.
There is my mapper
package com.api.gestioncartera.Services.Mappers;
import org.mapstruct.Mapper;
import org.springframework.stereotype.Component;
import com.api.gestioncartera.Entities.CollectionCompany;
import com.api.gestioncartera.Services.DTO.CollectionCompanyDto;
#Mapper(componentModel = "spring")
public interface CollectionCompanyMapper {
CollectionCompanyDto collectionCompanyToCollectionCompanyDto(CollectionCompany collectionCompany);
}
There is my Service where I'm trying to inject it
#Service
#Transactional
public class CollectionCompanyServiceImp implements CollectionCompanyService{
#Autowired
private CollectionCompanyMapper companyMapper;
}
My gradle config
plugins {
id 'org.springframework.boot' version '2.5.6'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
...
dependencies {
...
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}
compileJava {
options.compilerArgs += [
'-Amapstruct.suppressGeneratorTimestamp=true',
'-Amapstruct.suppressGeneratorVersionInfoComment=true',
'-Amapstruct.verbose=true',
'-Amapstruct.defaultComponentModel=spring'
]
}
I also enable enable annotation processing in the IDE
Properties in the IDE
The error is:
Consider defining a bean of type 'com.api.gestioncartera.Services.Mappers.CollectionCompanyMapper' in your configuration.
I noticed that I don't have any plugin referencing mapstruct, can be this the problem? Image:
I'm using Spring Tool Suite 4 (Eclipse) + Gradle 6.8 + SrpingBoot 2.5.6
Please help!!
Eclipse has its problems with annotation processing.
I solved the issue with my projects using this plugin:
https://plugins.gradle.org/plugin/
Add this to the top of your gradle plugins.
plugins {
id "eclipse"
id "com.diffplug.eclipse.apt" version "3.37.1"
}
then do a gradle refresh.
If it‘s still not working, run
./gradlew eclipse eclipseJdtApt eclipseFactorypath
Hope this helps!

Default profile values are not returned when specific profile selected in Spring Cloud Config

I'm moving from a local application.yml configuration file to a config server managed configuration.
My application.yml file contains 2 (or more) profiles in the same file, in the format:
spring.application.name: config-client
app.myvar1: "Var 1 in default profile"
app.myvar2: "Var 2 in default profile"
---
spring.config.activate.on-profile: docker
app.myvar1: "Var 1 in docker profile"
When I test this configuration file in a NOT config-server environment, I the result reading from the specific profile, and if not found, reading from default. Sample
Correct result when I test without config-server
Profile: docker
MyVar1=Var 1 in docker profile
MyVar2=Var 2 in default profile
For testing I'm using a simple program:
package com.bthinking.configclient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#SpringBootApplication
#RestController
public class ConfigClientApplication {
#Autowired
private Environment environment;
#Value("${app.myvar1:MyVar1 not found}")
String myVar1;
#Value("${app.myvar2:MyVar2 not found}")
String myVar2;
public static void main(String[] args) {
SpringApplication.run(ConfigClientApplication.class, args);
}
#RequestMapping("/")
public String index() {
StringBuffer b = new StringBuffer();
for (String profile : environment.getActiveProfiles()) {
b.append(String.format("Profile: %s</br>", profile));
}
b.append(String.format("MyVar1=%s</br>", myVar1));
b.append(String.format("MyVar2=%s</br>", myVar2));
return b.toString();
}
}
And I start the program with (I use gradle):
gradle :config-client:bootRun --args='--spring.profiles.active=docker'
But, when I migrate to config server, moving the file to a config-repo (I'm using file based repor), I get the invalid result (it's unable to read the variable in the default section). I have also tried with --spring.profiles.active=docker,default with no change
Profile: docker
MyVar1=Var 1 in docker profile
MyVar2=MyVar2 not found
For reference, my config-server has the following configuration:
server.port: 8888
spring.application.name: config-server
spring.cloud.config.server.native.searchLocations: file:${PWD}/config-repo
# spring.cloud.config.server.native.searchLocations: file:/Users/jmalbarran/Projects/BTH/BTH/SPB_SpringBoot/bugs/config/config-repo
logging.level:
root: debug
---
spring.config.activate.on-profile: docker
spring.cloud.config.server.native.searchLocations: file:/config-repo
The main class
package com.bthinking.configserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
#EnableConfigServer
#SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
And the build.gradle
plugins {
id 'org.springframework.boot' version '2.4.1'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
}
group = 'com.b-thinking'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
}
ext {
set('springCloudVersion', "2020.0.0")
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-config-server'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
test {
useJUnitPlatform()
}
And I start it with:
gradle :config-server:bootRun --args='--spring.profiles.active=native'
NOTE: As I think this a bug, I have also created an issue in github. Check in Issue
After the #spencergibb (Thanks!) comment, I tried with the version 3.0.1, and it solves the problem.
This is now my build.gradle
plugins {
id 'org.springframework.boot' version '2.4.1'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
}
group = 'com.b-thinking'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
}
ext {
set('springCloudVersion', "2020.0.0")
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-config-server:3.0.1'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
test {
useJUnitPlatform()
}
It's amazing, because the version 3.0.1 is reported solving the opposite bug (the default configuration override the specific), but I imagine that the review solved both.
For references, this were the related issues
Issue#1777 Profile not correct with SpringBoot 2.4.1 + Ilford 2020.0.0 (working with 2.4.1 + Ilford 2020.0.0-RC1)
Issue#1778 multidocument files from config server have the same property name

Dependencies for Spring Integration Amqp in Spring Boot

In order to use Spring Integration Amqp in a Spring Boot application, what are the dependencies I need to include?
Spring Boot version is 2.0.5.
Current dependencies I have are spring-boot-starter-integration and spring-integration-amqp
Error messages are classes like SimpleMessageListenerContainer and AmqpInboundChannelAdapter are not found on the classpath.
UPDATE:
My build.gradle entries --
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.5.RELEASE")
}
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-integration')
compile('org.springframework.boot:spring-boot-starter-amqp')
compile('org.springframework.integration:spring-integration-amqp')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
I had to add the following dependencies to resolve the classes in question (the last in the list did it, using latest spring initalizr, spring-boot 2.0.5)
dependencies {
implementation('org.springframework.boot:spring-boot-starter-amqp')
implementation('org.springframework.boot:spring-boot-starter-integration')
testImplementation('org.springframework.boot:spring-boot-starter-test')
compile 'org.springframework.integration:spring-integration-amqp'
}
To be fair, this answer was already given, just not for gradle.
I am using gradle 4.10.2 on a linux machine, spring-boot initialzr with the options RabbitMQ and Spring-Integration. Here are the changed files:
build.gradle
buildscript {
ext {
springBootVersion = '2.0.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
implementation('org.springframework.boot:spring-boot-starter-amqp')
implementation('org.springframework.boot:spring-boot-starter-integration')
testImplementation('org.springframework.boot:spring-boot-starter-test')
compile 'org.springframework.integration:spring-integration-amqp'
}
Implementation of Example 12.2.1 Configuring with Java Configuration from the Spring Integration docs:
package com.example.integrationamqp;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter;
import org.springframework.integration.amqp.inbound.AmqpInboundGateway;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
#SpringBootApplication
public class IntegrationAmqpApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(IntegrationAmqpApplication.class)
.web(WebApplicationType.NONE)
.run(args);
}
#Bean
public MessageChannel amqpInputChannel() {
return new DirectChannel();
}
#Bean
public AmqpInboundChannelAdapter inbound(SimpleMessageListenerContainer listenerContainer,
#Qualifier("amqpInputChannel") MessageChannel channel) {
AmqpInboundChannelAdapter adapter = new AmqpInboundChannelAdapter(listenerContainer);
adapter.setOutputChannel(channel);
return adapter;
}
#Bean
public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container =
new SimpleMessageListenerContainer(connectionFactory);
container.setQueueNames("foo");
container.setConcurrentConsumers(2);
// ...
return container;
}
#Bean
#ServiceActivator(inputChannel = "amqpInputChannel")
public MessageHandler handler() {
return new MessageHandler() {
#Override
public void handleMessage(Message<?> message) throws MessagingException {
System.out.println(message.getPayload());
}
};
}
}
Add this dependency:
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
And are you sure you have this one?:
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-amqp</artifactId>

Gradle build with fatJar plugin and SpringBoot application gives 'Application startup failed'

Everything was working fine when starting my app using Intellij. But when I made fatJar (with gradle plugin: eu.appsatori.fatjar) and execute:
java -jar myapp.jar
I'm getting something like this:
11:41:01.224 [main] ERROR org.springframework.boot.SpringApplication - Application startup failed
org.springframework.beans.factory.BeanDefinitionStoreException: Failed to process import candidates for configuration class [my.testing.MyAppMain]; nested exception is java.lang.IllegalArgumentException: No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.
at org.springframework.context.annotation.ConfigurationClassParser.processDeferredImportSelectors(ConfigurationClassParser.java:482)
at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:184)
...
It looks like it didn't found auto configuration classes in META-INF/spring.factories.
How to add this file? And what should be the content of it?
I've got following build script:
apply plugin: "java";
apply plugin: "idea";
apply plugin: 'application'
apply plugin: 'eu.appsatori.fatjar'
apply plugin: 'org.springframework.boot'
repositories {
mavenCentral()
}
buildscript {
ext {
springBootVersion = '1.4.3.RELEASE'
}
repositories {
mavenCentral()
jcenter()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
classpath "eu.appsatori:gradle-fatjar-plugin:0.3"
}
}
sourceSets {
main {
java {
srcDir 'src/main/java'
}
resources {
srcDir 'src/main/resources'
}
}
test {
java {
srcDir 'src/test/java'
}
}
}
fatJar {
manifest {
attributes("Main-Class": 'my.testing.MyAppMain')
}
exclude 'META-INF/*.DSA'
exclude 'META-INF/*.SF'
exclude 'META-INF/*.RSA'
}
dependencies {
compile 'org.springframework.boot:spring-boot-starter-jdbc'
runtime 'mysql:mysql-connector-java'
testCompile 'org.springframework.boot:spring-boot-starter-test'
}
And my example code is:
package my.testing;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
#SpringBootApplication
public class MyAppMain {
private ConfigurableApplicationContext springContext;
#Autowired
private SimpleDao dao;
public static void main(String[] args) throws Exception {
MyAppMain test = new MyAppMain();
try {
test.init();
test.doWhatYouGotToDo();
} finally {
test.stop();
}
}
private void doWhatYouGotToDo() {
System.out.println("Auto-wired dao: " + dao.hashCode());
System.out.println("Auto-wired jdbcTemplate: " + dao.jdbcTemplate.hashCode());
}
private void init() throws Exception {
springContext = SpringApplication.run(MyAppMain.class);
springContext.getAutowireCapableBeanFactory().autowireBean(this);
}
private void stop() throws Exception {
springContext.close();
}
}
#Component
class SimpleDao {
#Autowired
JdbcTemplate jdbcTemplate;
}
application.properties file:
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/some_db?useSSL=false&serverTimezone=UTC
spring.datasource.username = some_user
spring.datasource.password = some_pass
NOTE: This question is based on SpringBoot - making jar files - No auto configuration classes found in META-INF/spring.factories
where are all answers are referring to building with Maven. Please put only answers related to Gradle here.
Although I mostly use Maven for Spring and Gradle for Android, but here is the gradle way for a Spring project:
gradle clean build
gradle bootRepackage
Result:
Here is my build.gradle file:

Spring Boot 1.5 validated ConfigurationProperties

Jump to the bottom for the motivations and the solutions to this issue!
In the process of upgrading from Spring Boot 1.4 to 1.5 I read (source: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-1.5-Release-Notes#upgrading-from-spring-boot-14)
If you have #ConfigurationProperties classes that use JSR-303 constraint annotations, you should now additionally annotate them with #Validated. Existing validation will currently continue to work, however, a warning will be logged. In the future, classes without #Validated will not be validated at all.
So, diligently, I add #Validated to all of mine configuration properties. Now I have a specific use case that breaks, aka the property is not loaded anymore (I summarize first, then add code).
If I use a template property defined in application.properties file and then try to override the value for specific profiles, then the application is not starting.
Here is some sample code to reproduce (relevant files):
build.gradle
buildscript {
ext {
springBootVersion = '1.5.1.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
application.properties : demo.prop=${profile.prop}
application-demo.properties : profile.prop=demo
DemoApplication.java
package package;
import javax.validation.constraints.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#org.springframework.web.bind.annotation.RestController
public static class RestController {
#Autowired
private DemoProperties properties;
#GetMapping
public String get() {
return properties.prop == null ? "null" : properties.prop;
}
}
#Component
#ConfigurationProperties(prefix = "demo")
// #Validated
public static class DemoProperties {
#NotNull
private String prop;
public void setProp(String prop) {
this.prop = prop;
}
public String getProp() {
return prop;
}
}
}
As it stands, my application produces the expected result when run with -Dspring.profiles.active=demo
curl "http://localhost:8080"
demo
however, uncommenting //#validated and running the application as before produces
curl "http://localhost:8080"
null
Full application available at https://github.com/ThanksForAllTheFish/boot-props (including a test case showing that defining profile.prop in config/application.properties fails as well with #validated but succeeds without).
I guess it is a bug in Spring Boot, but it may me not understanding something, so SoF first (as hinted in Spring Boot issues manager on github).
This github issue seems related: https://github.com/spring-projects/spring-boot/issues/8173
As I found the solution to my issue (some time ago already, but added as explanation in the question itself), I figured it may be more helpful to copy my findings here.
The problem with my sample code is that #Validated wraps the real class with a proxy, so that validation concerns can be injected, therefore return properties.prop == null ? "null" : properties.prop; is actually trying to access the prop field of the proxy. Changing to getProp() is the fix. Pretty obvious once found out.
Regarding production code: the issue was related to https://github.com/spring-projects/spring-boot/issues/8173, or more precisely to https://github.com/spring-cloud/spring-cloud-commons/issues/177, as we use spring-cloud. Basically, there was a conflict in BeanPostProcessor between spring-cloud and spring-boot (details in the ticket on github) that was solved in Dalston.RELEASE of spring-cloud. Just updating the dependency in our project solved the issue in production as well. Lot of digging and testing to just change 7 characters in our codebase.

Resources