当前位置:首页 > 技术 > 正文内容

基于Spring Boot与CXF构建SOAP服务实践

访客 技术 2026年7月19日 1

背景与目标

现有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=true

schema.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);

实体与持久层

定义商品实体并采用NamedParameterJdbcTemplateSimpleJdbcInsert组合实现数据访问。

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响应体。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。