Retrofit实战(一)一个简单的示例
2017-05-01 00:25
471 查看
一、Retrofit简介
Retrofit是一个Http请求库,和其它Http库最大区别在于通过大范围使用注解简化Http请求。目前Retrofit 2.0底层是依赖OkHttp实现的,也就是说Retrofit本质上就是对OkHttp的更进一步封装。
1.Retrofit请求方式
Retrofit支持RESTful,HTTP请求方法包含get、post、delete、put、head、patch、trace、options总共8种。除get外,其他6种都是基于post方法衍生的,最常见的是get和post,而put、delete、post、get这四种最重要,分别对应数据库的增删改查。
(1)、@GET:对应HTTP的get请求方法
写法:
(2)、@POST:对应HTTP的post请求方法
写法:
(3)、@PUT:对应HTTP的put请求方法
写法:
(4)、@DELETE:对应HTTP的delete请求方法
写法:
HEAD、OPTIONS、PATCH这三个和上面差不多,还有种是HTTP,可替换以上七种,也可以扩展请求方法
(5)、@HTTP
写法:
我们先来看一下比较常见的接口地址:
对学生信息进行操作:1.查询学生数量 2.创建新学生 3.修改学生信息 4.删除学生。
而如果我们用了RESTful API就会变成这样:
大家是不是也看出两者的区别了? 修改跟删除直接用HTTP的请求方法(Method)来指定,虽然url 一样,但仍然可以知道你的动作。这就是所谓的RESTful 概念。
2.Retrofit各种请求注解的意思
3.请求头注解
该类型的注解用于为请求添加请求头。
看下@Headers的示例:
接下来来看@Header的示例:
可以看出@Header是以方法参数形势传入的,想必你现在能理解@Headers和@Header之间的区别了。
4.请求和响应格式注解
该类型的注解用于标注请求和响应的格式。
5.请求参数类注解
该类型的注解用来标注请求参数的格式,有些需要结合上面请求和响应格式的注解一起使用。
我们来看下示例:
1、@Body:根据转换方式将实例对象转换为相应的字符串作为请求参数传递。比如在很多情况下,你可能需要以post的方式上传json格式的数据。那么该怎么来做呢?
我们以一个登录接口为例,该接口接受以下格式的json数据:
首先建立请求实体,为了区别其他实体,通常来说约定以Post为后缀
然后定义该请求api
retrofit默认采用json转化器,因此在我们发送数据的时候会将Logint对象映射成json数据,这样发送出的数据就是json格式的。另外,如果你不确定这种转化行为,可以强制指定retrofit使用Gson转换器:
2、@Filed & @FiledMap
@Filed通常多用于Post请求中以表单的形势上传数据,这对任何开发者来说应该都是很常见的。
@FileMap和@Filed的用途相似,但是它用于不确定表单参数个数的情况下。
3、@Part :多用于Post请求实现文件上传功能(需要配合Multipart使用)。@Filed和@Part的主要区别就是,两者都可以用于Post提交,但是最大的不同在于@Part标志上文的内容可以是富媒体形势,比如上传一张图片,上传一段音乐,即它多用于字节流传输。而@Filed则相对简单些,通常是字符串键值对。
4、@PartMap:看上
5、@Path:用于替换请求地址,作用于方法参数
我们来看下Path路径的一个简单配置:
这里我们又传入了一个baseUrl("http://www.fendo.com/pulamsi/"),请求的完整 Url 就是通过 baseUrl 与注解的 value(下面称 “path“ ) 整合起来的,具体整合的规则如下:
假如基本地址为
6、@Query:用于条件字段参数,作用于方法参数
7、@QueryMap:用于条件字段参数,作用于方法参数
8、@Url:用于动态改变Url,作用于方法参数
注:如果使用Post请求方式,建议使用Field或FieldMap+FormUrlEncoded传递参数,虽然Query或QueryMap也可以实现,但是Query或QueryMap都是将参数拼接在url后面的,而@Field或@FieldMap传递的参数时放在请求体内。
6.标记类注解
Retrofit支持三种标记类注解,分别是:FormUrlEncoded、Multipart、Streaming。
1、FormUrlEncoded:指请求体是一个Form表单,Content-Type=application/x-www-form-urlencoded,需要和参数类注解@Field,@FieldMap搭配使用。
写法:
2、Multipart:指请求体是一个支持文件上传的Form表单,Content-Type=multipart/form-data,需要和参数类注解@Part,@PartMap搭配使用。
写法:
动态的写法
上传文件
上传多个文件示例
3、Streaming:指响应体的数据以流的形式返回,如果不使用默认会把数据全部加载到内存,所以下载文件时需要加上这个注解
写法:
7.Retrofit请求的形式
正常的网络请求有两种形式:
1)同步方式和异步方式。所谓同步方式,是指我们在在发出网络请求之后当前线程呗阻塞,直到请求的结果(成功或者失败)到来,才继续向下执行。
2)所谓异步,是指我们的网络请求发出之后,不必等待请求结果的到来,就可以去做其他的事情,当请求结果到来时,我们在做处理结果的动作。当时无论是同步还是异步,最终都是同步请求。
A)同步请求
1)首先定义要接口。注解Get表示使用的Get请求方式,{user}代表要被替换的数据
public interface GitHubService {
@GET("/users/{user}/repos")
List<Repo> listRepos(@Path("user") String user);
}
2)初始化RestAdapter,并利用动态代理来创建的接口对象。
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint("https://api.github.com")
.build();
GitHubService service = restAdapter.create(GitHubService.class);
3)使用网络访问并返回
List<Repo> repos = service.listRepos("octocat");
B)异步的方式
@POST("/users/new")
void createUser(@Body User user, Callback<User> cb);
注:body注解,在进行请求前对象会被转换器进行相应的数据转换。
前两部和之前的相同,不同在于最后一个参数变成了CallBack对象。
controller.getLogin(mail, password, new Callback<UserDataModel>() {
@Override
public void failure(RetrofitError error) {
// TODO Auto-generated method stub
}
@Override
public void success(UserDataModel ldm, Response response) {
// TODO Auto-generated method stub
});
可以看到当请求网络返回之后,会在failure中和success中进行回调,而且默认的数据转换器会把相应的json字符串转换为对象。(当然,我们也可以定义自己的数据转换器)
简单的来说,具有返回值的函数为同步执行的,异步执行函数没有返回值并且要求函数最后一个参数为Callback对象。
8.类型转换
服务器结果转换为Java对象
使用RestAdapter的转换器把HTTP请求结果(默认为JSON)转换为Java对象,Java对象通过函数返回值或者Callback接口定义
@GET("/users/list")
List<User> userList();
@GET("/users/list")
void userList(Callback<List<User>> cb);
如果要直接获取HTTP返回的对象,使用 Response 对象。
@GET("/users/list")
Response userList();
@GET("/users/list")
void userList(Callback<Response> cb);
二、实战Retrofit
这里使用的是聚合数据中的图灵机器人来演示
要使用retrofit请求网络数据,大概可以分为以下几步
1)添加依赖,这里以AndroidStudio为例:在build.grale添加如下依赖
2) 创建API接口
在retrofit中通过一个Java接口作为http请求的api接口。
3)创建实体类
我们访问:http://op.juhe.cn/robot/index
得到的数据如下
创建相应的实体类
ResultResponse类
RobotResponse类
4)发起网络请求
在这里baseUrl是在创建retrofit实力的时候定义的,我们也可以在API接口中定义完整的url。在这里建议在创建baseUrl中以”/”结尾,在API中不以”/”开头和结尾。
返回如下:
完整示例: http://download.csdn.net/detail/u011781521/9829987
Retrofit是一个Http请求库,和其它Http库最大区别在于通过大范围使用注解简化Http请求。目前Retrofit 2.0底层是依赖OkHttp实现的,也就是说Retrofit本质上就是对OkHttp的更进一步封装。
1.Retrofit请求方式
Retrofit支持RESTful,HTTP请求方法包含get、post、delete、put、head、patch、trace、options总共8种。除get外,其他6种都是基于post方法衍生的,最常见的是get和post,而put、delete、post、get这四种最重要,分别对应数据库的增删改查。
格式 | 含义 |
---|---|
@GET | 表示这是一个GET请求 |
@POST | 表示这个一个POST请求 |
@PUT | 表示这是一个PUT请求 |
@DELETE | 表示这是一个DELETE请求 |
@HEAD | 表示这是一个HEAD请求 |
@OPTIONS | 表示这是一个OPTION请求 |
@PATCH | 表示这是一个PAT请求 |
写法:
@GET("public") Call<BaseResult<List<User>>> getUser();
(2)、@POST:对应HTTP的post请求方法
写法:
@POST("User") Call<BaseResult<String>> addUser();
(3)、@PUT:对应HTTP的put请求方法
写法:
@PUT("User") Call<BaseResult<String>> updateUser();
(4)、@DELETE:对应HTTP的delete请求方法
写法:
@DELETE("User") Call<BaseResult<String>> deleteUser();
HEAD、OPTIONS、PATCH这三个和上面差不多,还有种是HTTP,可替换以上七种,也可以扩展请求方法
(5)、@HTTP
写法:
/** * method 表示请的方法,不区分大小写 * path表示路径 * hasBody表示是否有请求体 */ @HTTP(method = "get", path = "public", hasBody = false) Call<BaseResult<List<User>>> getUser();
我们先来看一下比较常见的接口地址:
对学生信息进行操作:1.查询学生数量 2.创建新学生 3.修改学生信息 4.删除学生。
请求方法 | 接口地址 | 接口说明 |
---|---|---|
get | /api/student/index | 查询接口 |
post | /api/student/ + 参数 | 创建接口 |
post | /api/student/update + 参数 | 修改接口 |
post | /api/student/delete + 参数 | 删除接口 |
请求方法 | 接口地址 | 接口说明 |
---|---|---|
get | /api/student/index | 查询接口 |
post | /api/student/ + 参数 | 创建接口 |
put | /api/student/ + 参数 | 修改接口 |
delete | /api/student/ + 参数 | 删除接口 |
2.Retrofit各种请求注解的意思
格式 | 含义 |
---|---|
@Headers | 添加请求头 |
@Path | 替换路径 |
@Query | 替代参数值,通常是结合get请求的 |
@FormUrlEncoded | 用表单数据提交 |
@Field | 替换参数值,是结合post请求的 |
该类型的注解用于为请求添加请求头。
注解 | 说明 |
---|---|
@Headers | 用于添加固定请求头,可以同时添加多个。通过该注解添加的请求头不会相互覆盖,而是共同存在 |
@Header | 作为方法的参数传入,用于添加不固定值的Header,该注解会更新已有的请求头 |
//使用@Headers添加单个请求头 @Headers("Cache-Control:public,max-age=120") @GET("mobile/active") Call<ResponseBody> getActive(@Query("id") int activeId); //使用@Headers添加多个请求头 @Headers({ "User-Agent:android" "Cache-Control:public,max-age=120", }) @GET("mobile/active") Call<ResponseBody> getActive(@Query("id") int activeId);
接下来来看@Header的示例:
@GET("mobile/active") Call<ResponseBody> getActive(@Header("token") String token,@Query("id") int activeId);
可以看出@Header是以方法参数形势传入的,想必你现在能理解@Headers和@Header之间的区别了。
4.请求和响应格式注解
该类型的注解用于标注请求和响应的格式。
名称 | 说明 |
---|---|
@FormUrlEncoded | 表示请求发送编码表单数据,每个键值对需要使用@Field注解 |
@Multipart | 表示请求发送multipart数据,需要配合使用@Part |
@Streaming | 表示响应用字节流的形式返回.如果没使用该注解,默认会把数据全部载入到内存中.该注解在在下载大文件的特别有用 |
该类型的注解用来标注请求参数的格式,有些需要结合上面请求和响应格式的注解一起使用。
名称 | 说明 |
---|---|
@Body | 多用于post请求发送非表单数据,比如想要以post方式传递json格式数据 |
@Filed | 多用于post请求中表单字段,Filed和FieldMap需要FormUrlEncoded结合使用 |
@FiledMap | 和@Filed作用一致,用于不确定表单参数 |
@Part | 用于表单字段,Part和PartMap与Multipart注解结合使用,适合文件上传的情况 |
@PartMap | 用于表单字段,默认接受的类型是Map<String,REquestBody>,可用于实现多文件上传 |
@Path | 用于url中的占位符 |
@Query | 用于Get中指定参数 |
@QueryMap | 和Query使用类似 |
@Url | 指定请求路径 |
1、@Body:根据转换方式将实例对象转换为相应的字符串作为请求参数传递。比如在很多情况下,你可能需要以post的方式上传json格式的数据。那么该怎么来做呢?
我们以一个登录接口为例,该接口接受以下格式的json数据:
{"password":"123456778","username":"fendo"}
首先建立请求实体,为了区别其他实体,通常来说约定以Post为后缀
public class Login { private String username; private String password; public LoginPost(String username, String password) { this.username = username; this.password = password; } }
然后定义该请求api
@POST("mobile/login") Call<ResponseBody> login(@Body Login post);
retrofit默认采用json转化器,因此在我们发送数据的时候会将Logint对象映射成json数据,这样发送出的数据就是json格式的。另外,如果你不确定这种转化行为,可以强制指定retrofit使用Gson转换器:
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://www.fendo.com/") .addConverterFactory(GsonConverterFactory.create()) .build();
2、@Filed & @FiledMap
@Filed通常多用于Post请求中以表单的形势上传数据,这对任何开发者来说应该都是很常见的。
@POST("mobile/register") Call<ResponseBody> registerDevice(@Field("id") String registerid);
@FileMap和@Filed的用途相似,但是它用于不确定表单参数个数的情况下。
3、@Part :多用于Post请求实现文件上传功能(需要配合Multipart使用)。@Filed和@Part的主要区别就是,两者都可以用于Post提交,但是最大的不同在于@Part标志上文的内容可以是富媒体形势,比如上传一张图片,上传一段音乐,即它多用于字节流传输。而@Filed则相对简单些,通常是字符串键值对。
@Multipart @POST("public") Call<BaseResult> uploadFile(@Part MultipartBody.Part file);
4、@PartMap:看上
@Multipart @POST("public") Call<BaseResult> uploadFile(@PartMap Map<String,RequestBody> RequestBodyMap);
5、@Path:用于替换请求地址,作用于方法参数
@GET("users/{user}/repos") Call<BaseResult<List<User>>> getUser(@Path("path") String path);
我们来看下Path路径的一个简单配置:
Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://www.fendo.com/pulamsi/") //增加返回值为String的支持 .addConverterFactory(ScalarsConverterFactory.create()) //增加返回值为Gson的支持(以实体类返回) .addConverterFactory(GsonConverterFactory.create()) //增加返回值为Oservable<T>的支持 .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .build();
这里我们又传入了一个baseUrl("http://www.fendo.com/pulamsi/"),请求的完整 Url 就是通过 baseUrl 与注解的 value(下面称 “path“ ) 整合起来的,具体整合的规则如下:
假如基本地址为
http://example.com/api/,关于baseUrl与注解中路径的拼接问题如下:
注解中的路径 | 最终Url (baseUrl为http://example.com/api/) |
---|---|
foo/bar/ | http://example.com/api/foo/bar/ |
/foo/bar/ | http://example.com/foo/bar/ |
https://github.com/square/retrofit/ | https://github.com/square/retrofit/ |
//github.com/square/retrofit/ | http://github.com/square/retrofit/ |
@GET("public") Call<BaseResult<List<User>>> getUser(@Query("userId") String userId);
7、@QueryMap:用于条件字段参数,作用于方法参数
@GET("public") Call<BaseResult<List<User>>> getUser(@QueryMap Map<String,String> map);
8、@Url:用于动态改变Url,作用于方法参数
@GET("public") Call<BaseResult<List<User>>> getUser(@Url String url);
注:如果使用Post请求方式,建议使用Field或FieldMap+FormUrlEncoded传递参数,虽然Query或QueryMap也可以实现,但是Query或QueryMap都是将参数拼接在url后面的,而@Field或@FieldMap传递的参数时放在请求体内。
6.标记类注解
Retrofit支持三种标记类注解,分别是:FormUrlEncoded、Multipart、Streaming。
1、FormUrlEncoded:指请求体是一个Form表单,Content-Type=application/x-www-form-urlencoded,需要和参数类注解@Field,@FieldMap搭配使用。
写法:
@FormUrlEncoded @POST("public") Call<BaseResult> addUser(@Field("userName") String userName);
2、Multipart:指请求体是一个支持文件上传的Form表单,Content-Type=multipart/form-data,需要和参数类注解@Part,@PartMap搭配使用。
写法:
@Multipart @POST("applist/apps/detail") Call<ResponsePojo> sendDetail(@Part("name1") String name1,@Part("name2") String name2);
动态的写法
@Multipart @POST("applist/apps/detail") Call<ResponsePojo> sendDetail(@PartMap Map<String, String> names);
上传文件
//上传文件 @POST("applist/apps/detail") Call<ResponseResult> sendFile(RequestBody fileRequest);
appListApi.sendFile(RequestBody.create(MediaType.parse("text/html"), new File("/sdcard/test.html")));
上传多个文件示例
Call<ResponseResult> sendFile( @Part("filename=test1") RequestBody fileRequest1 , @Part("filename=test2") RequestBody fileRequest2);
@Multipart
@POST("applist/apps/detail")
Call<ResponseResult> sendFile( @Part("filename=test1") RequestBody fileRequest1 , @Part("filename=test2") RequestBody fileRequest2);
3、Streaming:指响应体的数据以流的形式返回,如果不使用默认会把数据全部加载到内存,所以下载文件时需要加上这个注解
写法:
@Streaming @GET("download") Call<ResponseBody> downloadFile();
7.Retrofit请求的形式
正常的网络请求有两种形式:
1)同步方式和异步方式。所谓同步方式,是指我们在在发出网络请求之后当前线程呗阻塞,直到请求的结果(成功或者失败)到来,才继续向下执行。
2)所谓异步,是指我们的网络请求发出之后,不必等待请求结果的到来,就可以去做其他的事情,当请求结果到来时,我们在做处理结果的动作。当时无论是同步还是异步,最终都是同步请求。
A)同步请求
1)首先定义要接口。注解Get表示使用的Get请求方式,{user}代表要被替换的数据
public interface GitHubService {
@GET("/users/{user}/repos")
List<Repo> listRepos(@Path("user") String user);
}
2)初始化RestAdapter,并利用动态代理来创建的接口对象。
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint("https://api.github.com")
.build();
GitHubService service = restAdapter.create(GitHubService.class);
3)使用网络访问并返回
List<Repo> repos = service.listRepos("octocat");
B)异步的方式
@POST("/users/new")
void createUser(@Body User user, Callback<User> cb);
注:body注解,在进行请求前对象会被转换器进行相应的数据转换。
前两部和之前的相同,不同在于最后一个参数变成了CallBack对象。
controller.getLogin(mail, password, new Callback<UserDataModel>() {
@Override
public void failure(RetrofitError error) {
// TODO Auto-generated method stub
}
@Override
public void success(UserDataModel ldm, Response response) {
// TODO Auto-generated method stub
});
可以看到当请求网络返回之后,会在failure中和success中进行回调,而且默认的数据转换器会把相应的json字符串转换为对象。(当然,我们也可以定义自己的数据转换器)
简单的来说,具有返回值的函数为同步执行的,异步执行函数没有返回值并且要求函数最后一个参数为Callback对象。
8.类型转换
服务器结果转换为Java对象
使用RestAdapter的转换器把HTTP请求结果(默认为JSON)转换为Java对象,Java对象通过函数返回值或者Callback接口定义
@GET("/users/list")
List<User> userList();
@GET("/users/list")
void userList(Callback<List<User>> cb);
如果要直接获取HTTP返回的对象,使用 Response 对象。
@GET("/users/list")
Response userList();
@GET("/users/list")
void userList(Callback<Response> cb);
二、实战Retrofit
这里使用的是聚合数据中的图灵机器人来演示
要使用retrofit请求网络数据,大概可以分为以下几步
1)添加依赖,这里以AndroidStudio为例:在build.grale添加如下依赖
//Retrofit2所需要的包 compile 'com.squareup.retrofit2:retrofit:2.2.0' //ConverterFactory的Gson依赖包 compile 'com.squareup.retrofit2:converter-gson:2.2.0' compile 'com.squareup.retrofit2:converter-scalars:2.2.0'
2) 创建API接口
在retrofit中通过一个Java接口作为http请求的api接口。
public interface RobotServes { // 使用这个接口返回的值是String @GET("robot/index") Call<String> getString(@Query("info") String info, @Query("key") String key); // 使用这个接口返回的值已经封装成实体类了 @GET("robot/index") Call<RobotResponse> get(@Query("info") String info, @Query("key") String key); }
3)创建实体类
我们访问:http://op.juhe.cn/robot/index
得到的数据如下
{ "reason": "APPKEY错误或为空 ", "result": null, "error_code": 10001 }
创建相应的实体类
ResultResponse类
package com.example.retrofit.model; /** * Created by fendo on 2017/4/30. */ public class ResultResponse { private String code; private String text; public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getText() { return text; } public void setText(String text) { this.text = text; } }
RobotResponse类
package com.example.retrofit.model; /** * Created by fendo on 2017/4/30. */ public class RobotResponse { private String reason; private String error_code; private ResultResponse result; public String getReason() { return reason; } public void setReason(String reason) { this.reason = reason; } public String getError_code() { return error_code; } public void setError_code(String error_code) { this.error_code = error_code; } public ResultResponse getResult() { return result; } public void setResult(ResultResponse result) { this.result = result; } }
4)发起网络请求
Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://op.juhe.cn/") // 增加返回值为String的支持 .addConverterFactory(ScalarsConverterFactory.create()) // 增加返回值为Gson的支持 .addConverterFactory(GsonConverterFactory.create()) .build(); // 接口实体,返回的是String RobotServes phoneServes = retrofit.create(RobotServes.class); Call<String> call = phoneServes.getString("你好", "2833a660902644508778b5dfd452c080"); // 发送请求 call.enqueue(new Callback<String>() { // 响应回调 @Override public void onResponse(Call<String> call, Response<String> response) { Log.d(TAG, "onResponse = \n" + "response.message() = " + response.message() + "\n" + "response.body() = " + response.body()); Log.d(TAG, "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); } @Override public void onFailure(Call<String> call, Throwable t) { Log.d(TAG, "onFailure = \n" + t.toString()); } });
在这里baseUrl是在创建retrofit实力的时候定义的,我们也可以在API接口中定义完整的url。在这里建议在创建baseUrl中以”/”结尾,在API中不以”/”开头和结尾。
返回如下:
response.message() = OK response.body() = {"reason":"成功的返回","result":{"code":100000,"text":"嘿嘿,你好"},"error_code":0} <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 你好呀,希望你开心啊
完整示例: http://download.csdn.net/detail/u011781521/9829987
相关文章推荐
- 基于SpringCloud的微服务架构实战案例项目,以一个简单的购物流程为示例
- Android 框架设计Demo,一个简单的MVP示例搜索功能,网络请求用Retrofit+RxJava实现
- Delphi7下用dbExpress调用Oracle存储过程(返回数据集)的一个简单示例和调试过程
- 一个简单的破解示例
- Asp.net 2.0 一个简单的联动DropDownList示例(示例代码下载) [zhuan :D]
- 一个泛型类型List的简单示例
- 利用JBOSS+MyEclipse完成一个简单的EJB示例
- 一个序列化的简单示例
- 一个Forms验证简单示例
- SWT(一)一个最简单的SWT程序示例
- 实战Registry和RegistryKey类,一个简单的可疑文件扫描程序
- 最简单的一个Java窗体示例!
- SpringJdbc的一个简单示例
- 一个最简单的.NET Remoting构建的分布式应用程序示例
- Asp.net 2.0 一个简单的联动DropDownList示例(示例代码下载)
- Delphi7下用dbExpress调用Oracle存储过程(返回数据集)的一个简单示例和调试过程
- 一个简单的UrlRewrite示例[演示用,写给同事看的,VS2003环境]
- 小程序大问题,MSDN中一个小小示例所带来的疑问,一个关于DataList的一个简单应用
- 利用JBOSS+MyEclipse完成一个简单的EJB示例
- 用Jbuilder8做一个简单的struts示例