ArkUI Service Ability开发实战详解

系统 OpenHarmony
Ability是鸿蒙应用程序的重要组成部分,在鸿蒙开发FA(feature Ability)模型中,Ability分为PageAbility(FA)、ServiceAbility(PA)、DataAbility(PA)、FormAbility四种类型,PageAbility是我们熟知的UI交互页面。

​想了解更多关于开源的内容,请访问:​

​51CTO 开源基础软件社区​

​https://ost.51cto.com​

本篇的demo使用的是ArkUI的js开发,eTs的Service Ability开发与js流程基本一致,把 js 换成 ts 语言即可。

为了充分体验Service Ability的特性,这次的Demo由浅入深演示了三个功能的实现:

  • 一是调用service Ability来拼接字符串,即做一些数据处理业务。—同步实现
  • 二是结合了系统 公共事件与通知能力的Notification模块(js api) 来模拟Service在后台运行的场景,发送完就拉起特定页面。—异步实现
  • 三是使用下载接口request来尝试后台下载文件的场景,在实际场景中,可以实现应用在后台下载完一些文件或安装包后自动拉起特定的页面(不知道是不是接口的原因,监听不到下载完毕的事件回调,只能在在通知栏找到下载完毕的通知)。—异步实现​

相关知识

1、Service

Ability是鸿蒙应用程序的重要组成部分,在鸿蒙开发FA(feature Ability)模型中,Ability分为PageAbility(FA)、ServiceAbility(PA)、DataAbility(PA)、FormAbility四种类型,PageAbility是我们熟知的UI交互页面,DataAbility用于后台数据管理服务,FormAbility是服务卡片。ServiceAbility是PA类型一种,没有UI,它提供其他Ability调用自定义的服务,在后台运行。

在本机上,Service属于单实例、不会自动中断(除非系统回收资源)的系统后台服务,本机的应用可通过Service Ability等ability来使用它的能力,我们可以利用它的功能来实现像音乐播放、文件下载等需要在页面隐藏或者销毁还能继续运行的服务,是非常重要的一个能力,只不过在os的api9以前,ArkUI(js)想要创建Service Ability后台服务只能在通过js侧调用java侧来实现,但自从open Harmony api9(对应os api9)起,官方取消了js+java的混合开发模式,改为纯js开发,并更新了一批接口,于是就来尝试一下新接口的能力,打通一遍JS ServiceAbility的开发流程。

​Service Ability官方文档​​。

2、Service Ability生命周期

一共六个生命周期:

官方文档写到:根据调用方法的不同,其生命周期有两种路径。验证了一下后,大致地整理了一个流程图:

多个客户端可以绑定到相同Service,而且当所有绑定全部取消后,系统即会销毁该Service。官方文档中说可以人为通过调用stopAbility()来停止Service,但目前只在java侧可以找到这个接口,js侧还没有发布。

【FFH】ArkUI Service Ability开发实战详解-开源基础软件社区

3、RPC进程间通信

鸿蒙系统在不同进程间采取的是rpc通信的方式。ServiceAbility使用时,Service后台和PageAbility是两个隔离的进程,需要借助rpc接口来实现通信,两者通过IRemoteObject实例来传递数据,利用MessageParcel提供读写方法来包装数据,最后调用IRemoteObject的sendRequest方法实现信息的发送。

import rpc from ‘@ohos.rpc’。

本篇主要用到三个实例,后面会融入demo详细说说:

MessageParcel

用途

该类提供读写基础类型及数组、IPC对象、接口描述符和自定义序列化对象的方法。

包装数据对象

MessageOptions

用途

公共消息选项(int标志,int等待时间),使用标志中指定的标志构造指定的MessageOption对象。

配置:是否异步(默认同步1)、等待时间

IRemoteObject

用途

该接口可用于查询或获取接口描述符、添加或删除死亡通知、转储对象状态到特定文件、发送消息。

信息传输工具

​rpc通信接口官方文档​​。

代码实现

工程结构目录

【FFH】ArkUI Service Ability开发实战详解-开源基础软件社区

图中的ServiceAbility包便是编写ServiceAbility服务代码的地方。

一、拼接字符串实现流程

新建Service Ability

在entry右键->new->ability栏选择Service Ability。

【FFH】ArkUI Service Ability开发实战详解-开源基础软件社区

自己起一个包名,这个包名是后面连接Service Ability会用到的。

【FFH】ArkUI Service Ability开发实战详解-开源基础软件社区

创建完后,在ServiceAbility包下会出现service.js的文件,js有官方给的模板代码,包含ServiceAbility的所有生命周期:onStart、onStop、onConnect、onReconnect、onDisconnect、onCommand。

Page Ability拉起Service Ability

因为Service Ability也是属于ability的范围,因此它可以像Page Ability一样通过featureAbility的startAbility、connectAbility方法拉起进程。

startAbility()方法

import featureAbility from '@ohos.ability.featureAbility'
var want = {
deviceId:'', //--- 可以不写,默认本机
bundleName: "com.example.servicedemo",
abilityName: "com.example.entry.ServiceAbility",
}
featureAbility.startAbility(want)

包名bundleName和类名abilityName都可以在config.json中找到。其中bundleName是直接给出的,abilityName是"package"+“.”+包名。

connectAbility()方法

onnectAbility比startAbility多出一个参数用于接收连接的回调

var options:{
onConnect: this.onConnectCallback,
onDisconnect: this.onDisconnectCallback,
onFailed: this.onFailedCallback
}
let connectionId = featureAbility.connectAbility(want,options) //--- 返回一个连接的ServiceAbilityID,connection是从0开始的递增数字,每多一个ability的连接,connection就加一,可在featureAbility.disConnectAbility(connection)使用。
  • 连接成功时onConnect。
  • 连接失败时onFailed。
  • 连接中断时onDisconnect。
    当我们调用connectAbility时触发onConnect就说明连接成功,可在onConnectCallback的回调参数中找到remoteObject实例。

Service Ability侧响应

Page Ability主动连接(connectAbility)时,触发onConnect,Service Ability就返回一个IPremoteObject实例给Page Ability使用。

class sendNotification extends rpc.RemoteObject{ } //--- 继承RemoteObject类
export default {
onStart(want) {
console.info('xxx--- ServiceAbility onStart');
},
onStop() {
console.info('xxx--- ServiceAbility onStop');
},
onConnect(want) {
console.info('xxx--- ServiceAbility onConnect, want:' + JSON.stringify(want));
return new sendNotification('service ability preload') //--- 返回一个PremoteObject实例给Page Ability使用
},
onReconnect(want) {
console.info('xxx--- ServiceAbility onReconnect');
},
onDisconnect() {
console.info('xxx--- ServiceAbility onDisconnect');
},
onCommand(want, restart, startId) {
console.info('xxx--- ServiceAbility onCommand');
},
};

封装ServiceModel.js工具类

封装上面提到的接口:

import prompt from '@ohos.prompt'
import featureAbility from '@ohos.ability.featureAbility'
import rpc from "@ohos.rpc"
let mRemote = null //--- 接收remoteObject实例
let connection = -1 //--- 接收返回的一个代表此通道的connection
export class ServiceModel {
onConnectCallback(element, remote) { //--- 回调参数remote为remoteObject实例
console.info(`xxx--- onConnectLocalService onConnectDone element:${element}`)
console.info(`xxx--- onConnectLocalService onConnectDone remote:${remote}`)
if (remote === null) {
console.info('xxx--- onConnectCallback fail:' + mRemote)
return
}
mRemote = remote //--- 接收remoteObject实例
console.info('xxx--- onConnectCallback success:' + mRemote)
}
onDisconnectCallback(element) {
console.info(`xxx--- onConnectLocalService onDisconnectDone element:${element}`)
message: "Disconnect service success"
}
onFailedCallback(code) {
console.info(`xxx--- onConnectLocalService onFailed errCode:${code}`)
message: "Connect local service onFailed"
}
disConnect() { //--- 断开连接
featureAbility.disconnectAbility(connection)
console.info('xxx--- disConnect service ID : ' + connection)
}
start() { //--- 拉起ability
featureAbility.startAbility({
want: {
bundleName: "com.example.servicedemo",
abilityName: "com.example.entry.ServiceAbility",
}
});
}
connect() { //--- 连接ability,建立通信通道
connection = featureAbility.connectAbility(
{
deviceId: '',
bundleName: "com.example.servicedemo",
abilityName: "com.example.entry.ServiceAbility",
},
{
onConnect: this.onConnectCallback,
onDisconnect: this.onDisconnectCallback,
onFailed: this.onFailedCallback
}
)
console.info('xxx--- connection:' + connection)
}
getRemoteObject() { //--- 返回建立好的remoteObject通道
return mRemote
}
}

Page Ability与Service Ability实现通信

先来个流程测试:前端发送一段字符串到Service Ability,拼接处理后返回前端。

发送方启动Service Ability

startAbility或connectAbility都可以拉起ability,其中的区别参考上文的生命周期流程图

由接收方建立通信通道(remoteObject)并把通道返回给发送方

remoteObject就是上面提到的用于传递信息的通道,它提供的接口很多,其中最主要的两个方法是sendRequest和onRemoteRequest:

  • sendRequest为发送方调用,发送数据。
  • onRemoteRequest为接收方调用,监听发送的数据。

在service.js中增加一个类,super继承父类rpc.RemoteObject,并重写onRemoteRequest方法:

class sendNotification extends rpc.RemoteObject {
constructor(des) {
super(des)
}
onRemoteRequest(code, data, reply, option) {
console.info('xxx--- onRemoteRequest code:' + code)
if (code === 2) {
let string = data.readString()
console.info(`xxx--- string=${string}`)
let result = string + ' “我是拼接内容”'
console.info(`xxx--- result=${result}`)
reply.writeString(result)
}
return true;
}
}

当Page Ability发起连接请求时(connectAbility),Service Ability会收到请求并触发生命周期onConnect,对应接收方的onConnectCallback回调,我们在回调参数结果中获取到remoteObject赋值给全局变量mRemote。

onConnectCallback(element, remoteObject) { //--- 回调参数remoteObject即是生命周期onConnect返回的通信通道
console.info(`xxx--- onConnectLocalService onConnectDone element:${element}`)
console.info(`xxx--- onConnectLocalService onConnectDone remote:${remote}`)
if (remote === null) {
console.info('xxx--- onConnectCallback fail:' + mRemote)
return
}
mRemote = remoteObject
console.info('xxx--- onConnectCallback success:' + mRemote)
}

发送方获取通信通道

获取remoteObject实例:

import {ServiceModel} from '../../common/model/ServiceModel'
let ServiceModel = new ServiceModel()
ServiceModel.connect()
let mRemote = ServiceModel.getRemoteObject() //--- 需要等待connect完成,延迟调用

发送数据

sendRequest(code:number, data:rpc.MessageParcel, reply:rpc.MessageParcel, option:rpc.MessageOption)。

async addString(){
let data = rpc.MessageParcel.create()
let reply = rpc.MessageParcel.create()
let option = new rpc.MessageOption()
let beforeString = "待拼接内容"
data.writeString(beforeString)
mRemote.sendRequest(2, data, reply, option)
}
  • data和reply都是MessageParcel对象,是对数据的包装,对外提供读写方法。data和reply可看作两条单向读写通道,data只对发送方可写,reply只对接收方可写。
  • MessageOption是对此次通信的配置,可配置的选项:

setFlags

setWaitTime

同步1(默认)、异步0

最长等待响应时间(reply)

Service Ability接收数据并处理

通过code识别发送方的意图:

class sendNotification extends rpc.RemoteObject {
constructor(des) {
super(des)
}
onRemoteRequest(code, data, reply, option) {
console.info('xxx--- onRemoteRequest code:' + code)
if (code === 2) {
let string = data.readString() //--- 读取data
console.info(`xxx--- string=${string}`)
let result = string + ' “我是拼接内容”'
console.info(`xxx--- result=${result}`)
reply.writeString(result) //--- 结果写入reply
}
return true;
}
}

前端获取处理完的数据

在reply拿到处理完的数据(这一步用于需要同步的数据处理)。

async addString(){
let data = rpc.MessageParcel.create()
let reply = rpc.MessageParcel.create()
let option = new rpc.MessageOption()
let beforeString = "待拼接内容"
data.writeString(beforeString)
await mRemote.sendRequest(2, data, reply, option)
let afterString = reply.readString() //--- 在reply拿到处理完的数据
console.info('xxx--- service addString success:msg ' + this.afterString)
}

效果展示

【FFH】ArkUI Service Ability开发实战详解-开源基础软件社区

控制台打印:

【FFH】ArkUI Service Ability开发实战详解-开源基础软件社区

二、后台发送通知实现流程

后台发送通知 与上文的 拼接字符串 的区别在于:

  • 体现后台执行的特点。
  • 使用异步执行,数据不需要同步,执行完自动后拉起某个页面。

效果演示

【FFH】ArkUI Service Ability开发实战详解-开源基础软件社区

Page Ability侧

因为后台发送,option.setFlags设置为异步,并且先销毁当前页面 featureAbility.terminateSelf()。

sendMsg() {
console.info('xxx--- sendMsg start mRemote:'+this.mRemote)
let number = 9 //--- 发送通知数量
let data = rpc.MessageParcel.create()
data.writeInt(number)
let reply = rpc.MessageParcel.create()
let option = new rpc.MessageOption()
option.setFlags(1)
featureAbility.terminateSelf()
this.mRemote.sendRequest(1, data, reply, option)
},

Service Ability侧

Service Ability拉起、连接与上文 拼接字符串 的流程一致。

只需增加不同的code来识别。

onRemoteRequest(code, data, reply, option) {
console.info('xxx--- onRemoteRequest code:' + code)
if (code === 1) { //--- 发送通知意图
console.info('xxx--- sendMsg service')
let num = data.readInt() //--- 读取准备发送的通知数量,
console.info('xxx--- number:'+num)
this.sendMsgFor(0,num)
}
return true;
}

简单介绍一下公共事件与通知能力的Notification模块。

config.json配置权限:

“reqPermissions”: [{
“name”: “SystemCapability.Notification.Notification”
}]

简单的发送普通文本通知示例:

import Notification from '@ohos.notification';
Notification.requestEnableNotification(()=>{ //--- 在使用能力前先设置使能开关
console.info('xxx--- requestEnableNotification --------------');
})
var notificationRequest = { //---通知的类型和内容配置
id: 1,
content: {
contentType: Notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, //--- 普通文本类型的通知,还有更多的类型可参考文档,这里就不细说了
normal: {
title: "信息1",
text: "通知发送成功!",
additionalText: "您收到一条通知"
}
}
}
Notification.publish(notificationRequest) //--- 发送通知

【FFH】ArkUI Service Ability开发实战详解-开源基础软件社区

sendMsgFor(i,number){
let self = this
if(i<number) {
var notificationRequest = {
id: i,content: {
contentType: Notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, //--- 普通文本类型的通知
normal: {
title: "信息 " + i,text: "通知发送成功!",additionalText: "您收到一条通知"
}}
}
//通知发送
Notification.publish(notificationRequest).then((data) => {
console.info('xxx--- publish promise success req.id : ' + notificationRequest.id);
self.sendMsgFor(i + 1, number) //------------- 递归方式发送通知 ----------------------
}).catch((err) => {
console.info('xxx--- publish promise failed because ' + JSON.stringify(err));
});
}
else{ //------------- 发送完就拉起SecondAbility页面
featureAbility.startAbility({
want: {
bundleName: "com.example.servicetest",
abilityName: "com.example.entry.SecondAbility",
}
});
return
}

}

关于为什么用递归方式发送通知而不用定时器,因为Service Ability使用不了这种用在前端的函数,不仅包括setTimeout、setInterval,还有像new Image()、new offscreen()之类的也用不了,可能是官方有意为之。

三、后台下载实现流程

效果演示

【FFH】ArkUI Service Ability开发实战详解-开源基础软件社区

Page Ability侧

同样地设置异步执行并销毁当前页面。

download(){
console.info('xxx--- download start')
let data = rpc.MessageParcel.create()
let reply = rpc.MessageParcel.create()
let option = new rpc.MessageOption()
option.setFlags(1)
featureAbility.terminateSelf()
this.mRemote.sendRequest(3, data, reply, option)
}

Service Ability侧

原本想下载大一点的文件但是效果不太给力,所以就简单地调用一下request的接口下载一张图片。

​request接口详细文档​​。

配置网络权限:

“reqPermissions”: [{
“name”: “ohos.permission.INTERNET”
}]
import request from '@ohos.request';
onRemoteRequest(code, data, reply, option) {
console.info('xxx--- onRemoteRequest code:' + code)
if (code === 3) { //--- 发送通知意图
this.download()
}
return true;
}
download(){
let downloadTask;
request.download({ url: 'xxxxxxxxxxxx' }).then((data) => {
downloadTask = data;
console.info('xxx--- downloadTask')
}).catch((err) => {
console.info('Failed to request the download. Cause: ' + JSON.stringify(err));
})
}

完整代码

import request from '@ohos.request';
import featureAbility from '@ohos.ability.featureAbility';
import rpc from "@ohos.rpc"
import Notification from '@ohos.notification';
class sendNotification extends rpc.RemoteObject {
constructor(des) {
super(des)
}
onRemoteRequest(code, data, reply, option) {
console.info('xxx--- onRemoteRequest code:' + code)
if (code === 1) {
console.info('xxx--- sendMsg service')
let num = data.readInt()
console.info('xxx--- number:'+num)
this.sendMsgFor(0,num)
} else if (code === 2) {
let string = data.readString()
console.info(`xxx--- beforeStirng=${string}`)
let result = string + ' “我是拼接内容”'
console.info(`xxx--- afterStirng=${result}`)
reply.writeString(result)
}else if(code === 3){
this.download()
}
return true;
}
sendMsg(number){ //--- 单个通知
var notificationRequest = { //--- 构造NotificationRequest对象
id: number,
content: {contentType: Notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: "信息 "+number,
text: "文件下载成功!",
additionalText: "您收到一条通知"
}
}}
Notification.publish(notificationRequest).then((data) => { //--- 通知发送
console.info('xxx--- publish promise success req.id : ' + notificationRequest.id);
}).catch((err) => {
console.info('xxx--- publish promise failed because ' + JSON.stringify(err));
});
}
sendMsgFor(i,number){ //--- 多个通知
let self = this
console.info('xxx--- sendMsg:'+number)
if(i<number) {
var notificationRequest = {
id: i,content: {
contentType: Notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: "信息 " + i,
text: "通知发送成功!",
additionalText: "您收到一条通知"
}
}}
//通知发送
Notification.publish(notificationRequest).then((data) => {
console.info('xxx--- publish promise success req.id : ' + notificationRequest.id);
self.sendMsgFor(i + 1, number)
}).catch((err) => {
console.info('xxx--- publish promise failed because ' + JSON.stringify(err));
});
}else{
featureAbility.startAbility({
want: {
bundleName: "com.example.servicetest",
abilityName: "com.example.entry.SecondAbility",
}});
return
}
}
download(){
let downloadTask;
request.download({ url: 'xxxxxxxxxx' }).then((data) => {
downloadTask = data;
console.info('xxx--- downloadTask')
}).catch((err) => {
console.info('Failed to request the download. Cause: ' + JSON.stringify(err));
})
}
}

export default { //------------- 创建Service Ability生命周期 --------------
onStart(want) {
console.info('xxx--- ServiceAbility onStart');
},
onStop() {
console.info('xxx--- ServiceAbility onStop');
},
onConnect(want) {
console.info('xxx--- ServiceAbility onConnect, want:' + JSON.stringify(want));
return new sendNotification('service ability preload')
},
onReconnect(want) {
console.info('xxx--- ServiceAbility onReconnect');
},
onDisconnect() {
console.info('xxx--- ServiceAbility onDisconnect');
},
onCommand(want, restart, startId) {
console.info('xxx--- ServiceAbility onCommand');
},
};

UI界面:

import rpc from '@ohos.rpc';
import featureAbility from '@ohos.ability.featureAbility';
import Notification from '@ohos.notification';
import {ServiceModel} from '../../common/model/ServiceModel'
export default {
data: {
deviceList:[],
connection: -1,
beforeString:' ',
afterString:'',
ServiceModel:new ServiceModel(),
showString:false,
mRemote:undefined
},
onInit() {
this.grantPermission()
},
//获取用户权限
grantPermission() {
Notification.requestEnableNotification(()=>{
console.info('xxx--- requestEnableNotification --------------');
})
},
inputChange(e){
if(e.value!==''){
this.beforeString = e.value
}else this.beforeString = 'example'

console.info('xxx--- text change:'+e.value)
},
async addString() {
this.showString = true
console.info('xxx--- addString start mRemote:'+this.mRemote)
let data = rpc.MessageParcel.create()
data.writeString(this.beforeString)
let reply = rpc.MessageParcel.create()
let option = new rpc.MessageOption()
option.setFlags(0)
option.setWaitTime(5)
await this.mRemote.sendRequest(2, data, reply, option)
this.afterString = reply.readString()
console.info('xxx--- service addString success:msg ' + this.afterString)
},
sendMsg() {
console.info('xxx--- sendMsg start mRemote:'+this.mRemote)
let number = 7
let data = rpc.MessageParcel.create()
data.writeInt(number)
let reply = rpc.MessageParcel.create()
let option = new rpc.MessageOption()
option.setFlags(1)
featureAbility.terminateSelf()
this.mRemote.sendRequest(1, data, reply, option)
},
download(){
console.info('xxx--- download start')
let data = rpc.MessageParcel.create()
let reply = rpc.MessageParcel.create()
let option = new rpc.MessageOption()
option.setFlags(1)
featureAbility.terminateSelf()
this.mRemote.sendRequest(3, data, reply, option)
},
disConnect() {
this.ServiceModel.disConnect()
},
start() {
this.ServiceModel.start()
},
connect() {
let self = this
this.ServiceModel.connect()
setTimeout(()=>{
self.mRemote = self.ServiceModel.getRemoteObject()
},1000)
},
stop(){
this.ServiceModel.stop()
}
}
<div class="container">
<input onchange="inputChange" type="text" placeholder="输入一段字符串" class="input"></input>
<div if="{{ showString }}" style="position : absolute; width : 100%;
flex-direction : column;
justify-content : center;
align-items : center; top : 13%;">
<text style="margin-top : 10vp; font-size : 27vp;">拼接后的字符串</text>
<text style="width : 85%; height : 7%; font-size : 27vp; background-color : #ff00cafd;">{{ afterString }}</text>
</div>
<div class="serviceContainer">
<button onclick="start" class="serviceBtn">开启服务</button>
<button onclick="connect" class="serviceBtn">连接服务</button>
<button onclick="disConnect" class="serviceBtn">关闭连接</button>
<button onclick="addString" class="serviceBtn">拼接字符串</button>
<button onclick="sendMsg" class="serviceBtn">发送通知</button>
<button onclick="download" class="serviceBtn">下载文件</button>
</div>
</div>

结语

这次Js Service Ability的开发条过程也是啃着文档,但最后总算是成功打通了一遍流程,了解到它的一些原理。希望这篇文章能给准备尝试Js的Service Ability的人提供一些帮助。

Service Ability是很实用的一种应用后台ability,它能牵涉到鸿蒙系统的各种各样的子系统功能,学习Service Ability同时也是深入学习鸿蒙系统的原理、架构、接口能力等技术的途径。

​想了解更多关于开源的内容,请访问:​

​51CTO 开源基础软件社区​

​https://ost.51cto.com​​。

责任编辑:jianghua 来源: ​​51CTO开源基础软件社区
相关推荐

2022-07-11 16:26:37

eTS计算鸿蒙

2009-07-09 17:33:39

2021-10-28 14:58:15

鸿蒙HarmonyOS应用

2009-12-08 17:48:28

Web Service

2022-02-17 21:05:26

AbilityJS FAJava PA

2023-08-17 15:04:22

2009-12-09 09:58:07

ibmdwService

2022-08-23 16:07:02

ArkUI鸿蒙

2022-06-27 14:12:32

css鸿蒙自定义

2009-10-13 10:21:58

VB.NET实现Web

2023-12-20 17:28:48

水波纹ArkUI动画开发

2022-07-20 15:32:25

时钟翻页Text组件

2022-08-24 16:08:22

ETS鸿蒙

2009-07-31 16:57:19

ibmdwiWidget

2009-06-19 19:11:05

ibmdwlotus

2021-12-03 09:49:59

鸿蒙HarmonyOS应用

2022-06-30 13:56:05

Rating鸿蒙

2017-04-26 08:51:36

MongoDB集群实战

2023-11-07 10:22:26

自动驾驶技术

2012-05-03 11:21:58

ApacheCXFJava
点赞
收藏

51CTO技术栈公众号