Retrofit之解析xml (详细)

前言

在开发中, 一般都使用json解析, 但在最近搞的一个项目中, 需要接入固有的老接口,必须用xml进行解析. 搜索网上关于xml解析的文章不多, 也不够详细, 所以在经过一系列采坑之后, 我决心贡献自己的微薄经验. retrofit自带的json解析是GsonConverterFactory, xml解析时需替换为SimpleXmlConverterFactory, 此项目我采用了retrofit + rxjava2 + dagger2 + mvp + xml

准备工作:建议先使用postman测试接口


本文侧重于介绍xml解析

1、依赖库

compile('com.squareup.retrofit2:converter-simplexml:2.3.0') {
        exclude group: 'xpp3', module: 'xpp3'
        exclude group: 'stax', module: 'stax-api'
        exclude group: 'stax', module: 'stax'
    }

2、请求体实例

可以看到根节点为soap:Envelope, 第二层节点有soap:Header和soap:Body; 在soap:Header内部包含两层节点为kios:kioskSoapHeader以及最内层两个元素; 在soap:Body内部也包含两层节点为kios:assignRoom和其内部AssignRoom

以下为soap:Envelope节点写法

@Root(name = "soap:Envelope", strict = false)
@NamespaceList({
        @Namespace(reference = "http://www.w3.org/2003/05/soap-envelope", prefix = "soap"),
        @Namespace(reference = "http://kunlun.shijinet.cn/project/kiosk/", prefix = "kios")
})
@Default
@Order(elements = {
        "soap:Header/kios:kioskSoapHeader[1]/hardwareId",
        "soap:Header/kios:kioskSoapHeader[1]/stationId",
        "soap:Body/kios:assignRoom/AssignRoom"
})
public class AssignRoomParams {

    public AssignRoomParams(String hardwareId, String stationId, AssignRoomKey assignRoom) {
        this.hardwareId = hardwareId;
        this.stationId = stationId;
        this.AssignRoom = assignRoom;
    }

    @Element
    @Path("soap:Header/kios:kioskSoapHeader[1]/")
    public String hardwareId;
    @Element
    @Path("soap:Header/kios:kioskSoapHeader[1]/")
    public String stationId;
    @Element
    @Path("soap:Body/kios:assignRoom/")
    public AssignRoomKey AssignRoom;
}

因为xml请求时是无序的, 有可能造成soap:Body在soap:Header节点上部, 造成请求参数错误; 所以此处采用了@Order与@path结合, 是为了保证soap:Header和soap:Body的上下次序; @Root注解用来指定节点名称, @NamespaceList用来指定多个命名空间, @Element用来指定子节点名称.
此处需注意@path内部路径指向其下部的变量名, 此处变量名代表节点名称, 大小写必须与节点名称相一致, 所以可以看到此处第三个变量首字母为大写.

以下为AssignRoom节点写法

@Root(name = "AssignRoom", strict = false)
public class AssignRoomKey {

    public AssignRoomKey(String reservationNumber, String roomRequest) {
        this.reservationNumber = reservationNumber;
        this.roomRequest = roomRequest;
    }

    @Attribute(name = "ReservationNumber")
    public String reservationNumber;
    @Attribute(name = "RoomRequest")
    public String roomRequest;
}

3.响应体实例

以下为soap:Envelope节点写法

@Root(name = "soap:Envelope", strict = false)
@NamespaceList({
        @Namespace(reference = "http://www.w3.org/2003/05/soap-envelope", prefix = "soap")
})
public class AssignRoomBean {
    @Element(name = "Body")
    public AssignRoomBody body;
}

此处注意@Root中name为soap:Envelope,与节点名称一致, 但是@Element中name为Body,需要将soap:去掉, 这点很容易忽略, 在请求体中不能去掉, 而在响应体内部需要去掉这些头目

以下为Body节点写法

@Root(name = "Body", strict = false)
public class AssignRoomBody {
    @Element(name = "assignRoomResponse")
    public AssignRoomResponse response;
}

注意此处@Root内部name一致去掉soap:头目, 以下不再提示注意这点,

以下为assignRoomResponse节点写法

@Root(name = "assignRoomResponse", strict = false)
@NamespaceList({
        @Namespace(reference = "http://kunlun.shijinet.cn/project/kiosk/", prefix = "ns2"),
        @Namespace(reference = "http://webservices.micros.com/kiosk/3.0/", prefix = "ns3")
})
public class AssignRoomResponse {
    @Element(name = "AssignRoomResult")
    public AssignRoomResult result;
}

以下为AssignRoomResult节点写法

@Root(name = "AssignRoomResult", strict = false)
public class AssignRoomResult {
    @Element(name = "Result", required = false)
    public Result Result;
    @Element(name = "Room", required = false)
    public Room Room;
}

注意此处在@Element中required = false表示此元素在响应体中可能存在也可能不存在, 而在@Root中的strict = false也表示内部有些元素可能在响应体中不存在

以下为Result 节点写法

@Root(name = "Result", strict = false)
public class Result {
    @Attribute(name = "ResultType")
    public String ResultType;
    @Attribute(name = "ResultCode")
    public String ResultCode;
}

以下为Room节点写法

@Root(name = "Room", strict = false)
public class Room {
    @Attribute(name = "Number")
    public String Number;
    @Attribute(name = "RoomClass")
    public String RoomClass;
    @Attribute(name = "RoomType")
    public String RoomType;
    @Attribute(name = "Status")
    public String Status;
    @Element(name = "Features", required = false)
    public Features Features;
}

以下为Features 节点写法

@Root(name = "Features", strict = false)
public class Features {
    @ElementList(required = false,inline = true,entry = "Feature")
    public List<Feature> Feature;
}

注意此处使用@ElementList,代表集合,表示内部可能会有多个Feature元素.

以下为Feature节点写法

@Root(name = "Feature", strict = false)
public class Feature {
    @Attribute(name = "Code", required = false)
    public String Code;
}
此处为重要参考链接:

参考博客,对我内容的全面补充
SimpleXml的文档

创建Interface

@POST("kunlun-kiosk-new/KioskWebService?wsdl")
Observable<AssignRoomBean> assignRoom(@Body AssignRoomParams params);

创建Contract

public interface AssignRoomContract {
    interface IAssignRoomModel {
        Observable<AssignRoomBean> assignRoom(AssignRoomParams params);
    }

    interface IAssignRoomView extends IBaseView {
        void success(AssignRoomBean bean);
    }
}

创建Model

public class AssignRoomModel implements AssignRoomContract.IAssignRoomModel {
    private ApiService apiService;

    public AssignRoomModel(ApiService apiService) {
        this.apiService = apiService;
    }

    @Override
    public Observable<AssignRoomBean> assignRoom(AssignRoomParams params) {
        return apiService.assignRoom(params);
    }
}

创建Module

@Module
public class AssignRoomModule {
    private AssignRoomContract.IAssignRoomView view;

    public AssignRoomModule(AssignRoomContract.IAssignRoomView view) {
        this.view = view;
    }

    @Provides
    public AssignRoomContract.IAssignRoomModel provideModel(ApiService apiService) {
        return new AssignRoomModel(apiService);
    }

    @Provides
    public AssignRoomContract.IAssignRoomView provideView() {
        return view;
    }
}

创建Presenter

public class AssignRoomPresenter extends BasePresenter<AssignRoomContract.IAssignRoomModel, AssignRoomContract.IAssignRoomView> {
    @Inject
    public AssignRoomPresenter(AssignRoomContract.IAssignRoomModel model, AssignRoomContract.IAssignRoomView view) {
        super(model, view);
    }

    public void assignRoom(AssignRoomParams params) {
        mModel.assignRoom(params)
                .compose(RxSchedulers.<AssignRoomBean>io_main())
                .subscribe(new ErrorHandleObserver<AssignRoomBean>(mContext) {
                    @Override
                    public void onNext(@NonNull AssignRoomBean assignRoomBean) {
                       mView.success(assignRoomBean);
                    }
                });
    }
}

在fragement中调用

 @Override
    protected void initxChild() {
        String reservationNumber = "15021";
        String roomRequest = "1001";
        AssignRoomKey assignRoom = new AssignRoomKey(reservationNumber, roomRequest);
        AssignRoomParams params = new AssignRoomParams(Constant.HARDWARE_ID, Constant.STATION_ID, assignRoom);
        presenter.assignRoom(params);
    }

相关代码

public static final String HARDWARE_ID = "83DBDBAF1B8DE6FF3A3510598BE76B3975807475C4270A9F3B2A98229DB
D29E3";  public static final String STATION_ID = "X20-A";
@NormalScope
@Component(modules = AssignRoomModule.class, dependencies = AppComponent.class)
public interface MainComponent {
    void inject(MainFragment fragment);
}
@Singleton
@Component(modules = {AppModule.class, HttpModule.class})
public interface AppComponent {
    ApiService getApiService();
    BoermanApplication getWomoApplication();
    RxErrorHandler getRxErrorHandler();
}
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface NormalScope {
}
@Module
public class HttpModule {
    @Provides
    @Singleton
    public OkHttpClient provideOkHttpClient(BoermanApplication application) {
        boolean debug = PropertyUtil.isLogAvailable(application);
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        if (debug) {
            HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
                @Override
                public void log(String message) {
                    LogUtil.logger(message);
                }
            });
            loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
            builder .addInterceptor(loggingInterceptor);                 
        }
        return builder
                .connectTimeout(8, TimeUnit.SECONDS)
                .readTimeout(8, TimeUnit.SECONDS)
                .build();
    }

    @Provides
    @Singleton
    public Retrofit provideRetrofit(OkHttpClient okHttpClient, BoermanApplication application) {
        boolean debug = PropertyUtil.isLogAvailable(application);
        Retrofit.Builder builder = new Retrofit.Builder();
        if (debug) {
            builder = builder.baseUrl(ApiService.BASE_URL_TEST);
        } else {
            builder = builder.baseUrl(ApiService.BASE_URL_LINE);
        }
        return builder
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(SimpleXmlConverterFactory.create())
                .client(okHttpClient)
                .build();
    }

    @Singleton
    @Provides
    public ApiService provideApiService(Retrofit retrofit) {
        return retrofit.create(ApiService.class);
    }

    @Singleton
    @Provides
    public RxErrorHandler provideRxHandler(BoermanApplication application) {
        return new RxErrorHandler(application);
    }
}
public class RxSchedulers {
    public static <T> ObservableTransformer<T, T> io_main() {
        return new ObservableTransformer<T, T>() {
            @Override
            public ObservableSource<T> apply(Observable<T> upstream) {
                return upstream.subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread());
            }
        };
    }
}
public abstract class ErrorHandleObserver<T> extends DefaultObserver<T> {
    private Context context;
    protected RxErrorHandler rxErrorHandler;

    public ErrorHandleObserver(Context context) {
        this.context = context;
        this.rxErrorHandler = new RxErrorHandler(context);
    }

    @Override
    public void onSubscribe(@NonNull Disposable d) {
    }

    @Override
    public void onError(@NonNull Throwable e) {       
        BaseException baseException = rxErrorHandler.onErrorHandle(e);
        if (baseException == null) {
            LogUtil.logger(e.getMessage());
        } else {
            rxErrorHandler.showErrorMessage(baseException);
        }
    }

    @Override
    public void onComplete() {
    }
}

总结

xml解析的bean类需要手写, 容易出错, 建议解析接口出错时,可以使用抓包工具Fiddler分析, 还是挺好用的; 当然通过拦截器打印的logger日志也是一样的作用, 但本人还是喜欢用Fidder
Fiddler抓包工具使用连接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349