基于Spring Boot与CXF构建SOAP服务实践
背景与目标
现有ERP系统采用紧耦合架构,依赖大量产品化组件对外暴露WebService。为降低耦合度,计划剥离原有组件,直接通过数据库访问层自建服务。本文演示如何借助Spring Boot与Apache CXF搭建独立运行的SOAP服务,并解决JDK 11环境下JAX-WS标准库移除带来的兼容性问题。
技术栈与版本约束
- JDK 11(移除了Java EE模块)
- Gradle 4.9
- Spring Boot 2.1.3
- Apache CXF 3.2.4
- H2嵌入式数据库
- Lombok
构建配置
由于Gradle版本限制,Spring Boot无法使用最新版,需显式指定兼容版本。同时JDK 11不再内置JAX-WS相关API,需手动引入替代实现。
plugins {
id 'org.springframework.boot' version '2.1.3.RELEASE'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
}
group = 'com.demo.soap'
version = '1.0.0-SNAPSHOT'
sourceCompatibility = '11'
repositories {
maven { url 'https://maven.aliyun.com/repository/public/' }
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.apache.cxf:cxf-spring-boot-starter-jaxws:3.2.4'
implementation 'com.sun.xml.ws:jaxws-ri:2.3.3'
implementation 'javax.xml.ws:jaxws-api:2.3.1'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}数据源与初始化
采用H2内存数据库简化测试环境,通过SQL脚本自动完成表结构创建与基础数据植入。
application.properties配置:
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.platform=h2
spring.h2.console.enabled=true
spring.h2.console.path=/h2
spring.h2.console.settings.web-allow-others=trueschema.sql:
CREATE TABLE IF NOT EXISTS PRODUCT (
PID BIGINT AUTO_INCREMENT PRIMARY KEY,
PNAME VARCHAR(128) NOT NULL,
PRICE DECIMAL(10,2)
);data.sql:
INSERT INTO PRODUCT (PNAME, PRICE) VALUES ('Wireless Mouse', 59.90);
INSERT INTO PRODUCT (PNAME, PRICE) VALUES ('Mechanical Keyboard', 299.00);实体与持久层
定义商品实体并采用NamedParameterJdbcTemplate与SimpleJdbcInsert组合实现数据访问。
package com.demo.soap.entity;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class Product {
private Long pid;
private String pname;
private Double price;
}package com.demo.soap.repository;
import com.demo.soap.entity.Product;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
@RequiredArgsConstructor
public class ProductRepository {
private final NamedParameterJdbcTemplate namedTemplate;
private final SimpleJdbcInsert jdbcInsert;
private final RowMapper<Product> rowMapper = (rs, rowNum) -> Product.builder()
.pid(rs.getLong("PID"))
.pname(rs.getString("PNAME"))
.price(rs.getDouble("PRICE"))
.build();
public Product fetchById(Long pid) {
String sql = "SELECT * FROM PRODUCT WHERE PID = :pid";
MapSqlParameterSource params = new MapSqlParameterSource("pid", pid);
return namedTemplate.queryForObject(sql, params, rowMapper);
}
public List<Product> fetchAll() {
return namedTemplate.query("SELECT * FROM PRODUCT", rowMapper);
}
public Long createProduct(String name, Double price) {
return jdbcInsert.executeAndReturnKey(
new MapSqlParameterSource()
.addValue("PNAME", name)
.addValue("PRICE", price)
).longValue();
}
}启动类配置
注册JdbcTemplate相关Bean,启用SimpleJdbcInsert的自动生成主键功能。
package com.demo.soap;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import javax.sql.DataSource;
@SpringBootApplication
public class SoapServiceApplication {
@Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource ds) {
return new NamedParameterJdbcTemplate(ds);
}
@Bean
public SimpleJdbcInsert simpleJdbcInsert(JdbcTemplate template) {
return new SimpleJdbcInsert(template)
.withTableName("PRODUCT")
.usingGeneratedKeyColumns("PID");
}
public static void main(String[] args) {
SpringApplication.run(SoapServiceApplication.class, args);
}
}服务接口定义
通过JWS注解标注SOAP服务契约,targetNamespace避免命名冲突。
package com.demo.soap.service;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
@WebService(targetNamespace = "http://service.soap.demo.com/")
public interface ProductCatalog {
@WebMethod
String retrieveProduct(@WebParam(name = "productId") Long id);
@WebMethod
String addProduct(
@WebParam(name = "productName") String name,
@WebParam(name = "productPrice") Double price
);
}服务实现与发布
实现类需同时标注Spring的@Service与JWS的@WebService,并通过endpointInterface指向接口全限定名。
package com.demo.soap.service.impl;
import com.demo.soap.entity.Product;
import com.demo.soap.repository.ProductRepository;
import com.demo.soap.service.ProductCatalog;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.jws.WebService;
@Slf4j
@Service
@RequiredArgsConstructor
@WebService(
serviceName = "ProductCatalog",
targetNamespace = "http://service.soap.demo.com/",
endpointInterface = "com.demo.soap.service.ProductCatalog"
)
public class ProductCatalogImpl implements ProductCatalog {
private final ProductRepository repository;
private final ObjectMapper jsonMapper = new ObjectMapper();
@Override
public String retrieveProduct(Long id) {
try {
Product item = repository.fetchById(id);
return jsonMapper.writeValueAsString(
new Response<>(200, item)
);
} catch (Exception ex) {
log.error("查询异常", ex);
return "{\"code\":500,\"message\":\"" + ex.getMessage() + "\"}";
}
}
@Override
public String addProduct(String name, Double price) {
try {
Long newId = repository.createProduct(name, price);
return jsonMapper.writeValueAsString(
new Response<>(200, "Created ID: " + newId)
);
} catch (Exception ex) {
log.error("写入异常", ex);
return "{\"code\":500,\"message\":\"" + ex.getMessage() + "\"}";
}
}
private record Response<T>(int code, T payload) {}
}CXF端点注册
利用CXF内置的Bus总线将Spring管理的Bean发布为SOAP端点,默认路径前缀为/services。
package com.demo.soap.config;
import com.demo.soap.service.ProductCatalog;
import lombok.RequiredArgsConstructor;
import org.apache.cxf.Bus;
import org.apache.cxf.jaxws.EndpointImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.xml.ws.Endpoint;
@Configuration
@RequiredArgsConstructor
public class WebServiceConfig {
private final Bus cxfBus;
private final ProductCatalog catalogService;
@Bean
public Endpoint productEndpoint() {
EndpointImpl ep = new EndpointImpl(cxfBus, catalogService);
ep.publish("/catalog");
return ep;
}
}服务验证
启动应用后访问 http://localhost:8080/services 可查看已发布的服务列表。WSDL地址为 http://localhost:8080/services/catalog?wsdl。
使用SOAP UI或类似工具构造请求测试:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ser="http://service.soap.demo.com/">
<soapenv:Header/>
<soapenv:Body>
<ser:retrieveProduct>
<productId>1</productId>
</ser:retrieveProduct>
</soapenv:Body>
</soapenv:Envelope>预期返回包含商品信息的JSON响应体。