您的位置:首页 > 编程语言 > Java开发

Java常用的设计模式:适配器模式

2018-03-31 14:54 531 查看

一、场景问题

    先考虑一个生活场景的问题,小明有一个台式机,硬盘比较旧了,其它的都还好,所以小明打算换一个新的硬盘,一切准备就绪之后发现新的硬盘跟原来的电影不是同一个规格的接口,不能用到一起去,于是小明又买了个转接线,这个转接线的作用就是将新的硬盘接口与电源线口连接起来,这样就可以使用了。在上边这个例子中,转接线的作用就相当于一个适配器,用来协调连接两个接口不同的东西。再说一个生活的场景,比如现在有的手机充电线,一端是通用的USB接口,而另一端为了适配手机就分开了多种不同的接口,这都是适配器在生活中的常见场景。    接下来说项目上的场景问题,首先在我们的项目上,我所做的项目是某银行的互联网金融项目,作为一个新开发的后端接口平台,接口的统一性就显得尤为重要了。行内有多个外接渠道,每个渠道对外的调用形式各不相同,为了保证系统能够适应各个渠道的接口,我们在互金接口外层新开发了Adapter项目,该项目作为一层适配器,用来根据各个渠道进行接口的转入转出配置。这样做也将这个接口转换的功能抽离了出来,避免了嵌入在后端应用上带来的复杂性问题。《研磨设计模式》中给出了一个日志管理系统的例子,这里就像书中所说,单纯的抽取这个场景下与适配器模式有关的地方,而不是完整的实现这个功能。下面是日志系统管理的第一版代码:新建一个日志数据模型类,也就是要写入文件的数据对象,因为要写入文件,所以需要序列化,使用implements实现Serializable接口(书中此处没有实现,代码运行报错了)package com.chenxyt.java.test;
import java.io.Serializable;
//日志内容数据模型 序列化
public class LogModel implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1L;
//日志Id
private String logId;
//操作用户
private String operateUser;
//操作时间
private String operateTime;
//日志内容
private String logContent;
public String getLogId() {
return logId;
}
public void setLogId(String logId) {
this.logId = logId;
}
public String getOperateUser() {
return operateUser;
}
public void setOperateUser(String operateUser) {
this.operateUser = operateUser;
}
public String getOperateTime() {
return operateTime;
}
public void setOperateTime(String operateTime) {
this.operateTime = operateTime;
}
public String getLogContent() {
return logContent;
}
public void setLogContent(String logContent) {
this.logContent = logContent;
}
//打印时将对象转成Sring
public String toString(){
return "logId="+logId+",operateUser="+operateUser+",operateTime="+operateTime+",logContent="+logContent;
}
}然后创建一个日志操作接口,声明读、写文件的方法
package com.chenxyt.java.test;
import java.util.List;
//日志操作接口 多个内容 采用List存储
public interface LogFileOperateApi {
//读取文件内容
public List<LogModel> readFile();
//写入文件内容
public void writeLogFile(List<LogModel> list);
}实现上边的方法,实现过程就是文件的读写操作
package com.chenxyt.java.test;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;
//日志操作类
public class LogFileOperate implements LogFileOperateApi{
//日志文件的路径
private String LogFileName = "Adapter.log";
public LogFileOperate(String LogFileName) {
// TODO Auto-generated constructor stub
//如果传的不是空 则更新文件名
if(LogFileName!=null&&LogFileName.trim().length()>0){
this.LogFileName = LogFileName;
}
}
@Override
public List<LogModel> readFile() {
// TODO Auto-generated method stub
List<LogModel> list = null;
ObjectInputStream oin = null;
try {
//打开文件
File f = new File(LogFileName);
if(f.exists()){
//文件内容读取
oin = new ObjectInputStream(new BufferedInputStream(new FileInputStream(f)));
list = (List<LogModel>)oin.readObject();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally{
try {
if(oin!=null){
oin.close();
}
}catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return list;
}
@Override
public void writeLogFile(List<LogModel> list) {
// TODO Auto-generated method stub
File f = new File(LogFileName);
ObjectOutputStream out = null;
try {
//写入文件
out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(f)));
out.writeObject(list);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
try {
out.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}新建一个测试的客户端类,测试日志读写
package com.chenxyt.java.test;
import java.util.ArrayList;
import java.util.List;
public class Client {
public static void main(String[] args) {
LogModel lgm = new LogModel();
lgm.setLogId("1");
lgm.setOperateUser("chenxyt");
lgm.setOperateTime("2018-3-30 15:08:29");
lgm.setLogContent("This is a Test");
List<LogModel> list = new ArrayList<LogModel>();
list.add(lgm);
LogFileOperateApi opt = new LogFileOperate("");
//写入文件
opt.writeLogFile(list);
//读取文件
List<LogModel> readLog = opt.readFile();
System.out.println("readLog==" + readLog);
}
}运行结果:
    

    第一版简单的日志读写功能实现了,现在由于业务需要而升级系统,客户要求使用数据库进行日志的管理。于是有了下边的代码:
package com.chenxyt.java.test;
import java.util.List;
//数据库操作日志接口
public interface LogDbOperateApi {
//新增日志
public void createLog(LogModel lm);
//修改日志
public void updateLog(LogModel lm);
//删除日志
public void deleteLog(LogModel lm);
//获取所有日志
public List<LogModel> getAllLog();
}
这里简单的写了一下数据库操作日志提供的接口,具体操作暂且不管,只是知道我们现在有两版实现日志功能的操作。
    接下来客户的需求又变了,能不能让现有的业务既支持第一版的文件形式记录,也支持第二版的数据库形式记录呢?问题不是简简单单额两个版本合并一下即可,现在的业务是支持第二版的接口,而以前的业务是支持第一版的接口,两者之间没办法公用,也就导致了现在使用第二版的客户端无法使用第一个版本。当然我们也可以按照第二个版本的接口重新开发一个文件形式的日志操作,但是我们没必要重写以前写过的内容,而是想办法复用。还有一种方式就是修改第一版的代码,这也是不可取的,因为你不知道这个日志系统影响了多少个外部调用。

二、解决方案

    上述问题的解决方案就是使用适配器模式。适配器模式:将一个类的接口,转换成客户希望的另外一个接口。适配器使得原本由于接口不兼容而不能一起的工作类可以一起工作。    适配器解决问题的思路就是定义一个实现第二版接口的类,然后在这个接口内部实现的时候,转调第一版已经实现的功能,这样就在使用组合类的情况下,完成了业务需要。既复用了第一版的功能,也实现了第二版的接口。适配器模式的结构图如下,这里为了便于理解没有采用UML类图:
    

Client:客户端

Target:现在需求对应的接口Adaptee:以前版本的接口Adapter:适配器,实现了现在版本接口的类,内部调用以前版本的接口(实现)    这个结构图的意思是,客户端现在只能调用新版本的接口,因为要保证现在的功能能够实现,然后现在要整合也可以使用以前的功能,所以加了一个实现了现在接口的适配器,内部调用以前的方法,这就达成了转换适配的目的。逻辑代码:现有的接口:package com.chenxyt.java.practice;
//客户端使用的接口
public interface Target {
public void request();
}已经存在的类:
package com.chenxyt.java.practice;
//原来已经存在的方法 需要被适配的方法
public class Adaptee {
public void specificRequest(){
System.out.println("适配成功!");
}
}
适配器:
package com.chenxyt.java.practice;
//适配器的实现
public class Adapter implements Target{
//定义要适配的对象
private Adaptee adaptee;
//构造方法 传入需要适配的对象
public Adapter(Adaptee adaptee) {
// TODO Auto-generated constructor stub
this.adaptee = adaptee;
}
@Override
public void request() {
// TODO Auto-generated method stub
//转调已经实现了的方法,进行适配
adaptee.specificRequest();
}
}客户端调用:
package com.chenxyt.java.practice;
//使用适配器的客户端
public class Client {
public static void main(String[] args) {
//创建要适配的对象
Adaptee adaptee = new Adaptee();
//定义客户端需要调用的接口对象
Target target = new Adapter(adaptee);
//调用方法
target.request();
}
}    这里我说一下我的理解,实际上所谓的适配器,就是现有接口的一个实现,接口的实现会根据构造器不同而调用不同的方法,也就是向上转型。比如上述客户端中的Target target = new Adapter(adaptee);这里由于使用了Adapter构造器,所以走到了Adapter的实现中,然后这个实现传入了要适配的对象,在接口的方法实现中调用了这个对象的方法。实际上这个接口还有另外一个实现,结合前边日志系统的例子说的话,另外一个实现就是数据库形式的日志管理系统,适配器里实现的就是文件形式的管理系统。
按照上边适配器的逻辑,修改前边日志文件的例子:
这一部分代码是我自己实现的,忽略了日志操作的细节,以及舍弃了书中关于修改与删除的部分,思想都是一样的,具体业务场景具体分析。自己写的原因是想看一下自己是否知道怎么使用。
在前边日志操作模块代码的基础上新增Adapter类,该类主要做适配,定义了要适配的对象,在创建日志的方法中使用这个对象调用了以前的方法。
package com.chenxyt.java.test;

import java.util.List;
//适配器类
public class Adapter implements LogDbOperateApi{
//要适配的对象
private LogFileOperate logFileOperate;
private List<LogModel> list;
//构造方法,传入要适配的对象
public  Adapter(LogFileOperate logFileOperate,List<LogModel> list){
this.logFileOperate = logFileOperate;
this.list = list;
}
@Override
public void createLog(LogModel lm) {
/
dc7f
/ TODO Auto-generated method stub
logFileOperate.writeLogFile(list);
}
@Override
public void updateLog(LogModel lm) {
// TODO Auto-generated method stub
}
@Override
public void deleteLog(LogModel lm) {
// TODO Auto-generated method stub
}
@Override
public List<LogModel> getAllLog() {
// TODO Auto-generated method stub
return logFileOperate.readFile();
}
}
因为调用形式变了,所以客户端代码需要修改一下,使用现有的接口对象,调用现有的方法。
package com.chenxyt.java.test;
import java.util.ArrayList;
import java.util.List;
public class Client {
public static void main(String[] args) {
LogModel lgm = new LogModel();
//要适配的方法
LogFileOperate logFileOperate = new LogFileOperate("");
lgm.setLogId("1");
lgm.setOperateUser("chenxyt");
lgm.setOperateTime("2018-3-30 17:48:19");
lgm.setLogContent("This is a Test");
List<LogModel> list = new ArrayList<LogModel>();
list.add(lgm);
//定义接口对象 使用适配类 传入要适配的对象跟操作的参数
LogDbOperateApi opt = new Adapter(logFileOperate,list);
//写入文件
opt.createLog(null);
//读取文件
List<LogModel> readLog = opt.getAllLog();
System.out.println("readLog==" + readLog);
}
}这里我把原来的文件删了,重新运行了一遍,运行结果如下:
    

    如上,经过改造成适配器模式,可以使用新的接口匹配原来的日志记录方式。
    这里我觉得有必要把《Java编程思想》第九章第六节中关于适配接口的例子拿出来再分析一下,当时学习的时候可能有一点不理解,现在再看一下,同时也将原来代码中同名的函数区分了一下,以免歧义影响。链接地址Java编程思想学习笔记九: 接口 首先有个Processor接口,该接口在本例中相当于新版本日志管理的接口:
package com.chenxyt.java.practice;
public interface Processor{
String name();
Object processFunc(Object input);
}
然后是个Apply类,该类有个静态方法,而该方法只能调用Processor接口类型的参数,在本例中相当于客户端要调用的方法:package com.chenxyt.java.practice;
public class Apply {
public static void process(Processor p,Object s){
System.out.println("Using Processor" + p.name());
System.out.println(p.processFunc(s));
}
} 然后是一个Filter类,这个类是以前就存在的,相当于本例中的旧版本的日志记录功能,我们不能对其修改,但是要在新的接口中调用这个功能,因此需要适配:
package com.chenxyt.java.practice;
public class Filter{
public String name(){
return getClass().getSimpleName();
}
public Waveform process(Waveform input){
return input;
}
} 这里看似稍微复杂了一些因为这个接口的process方法返回了Waveform类型,实际上就是多了一个返回类型的转换:
package com.chenxyt.java.practice;

public class Waveform {
private static long counter;
private final long id = counter++;
public String toString(){
return "Waveform" + id;
}
}与本例相比,旧的方法实现(Filter类)还多了几个子类,因为类的继承关系,所以适配基类的接口,同样可以用来适配子类:
package com.chenxyt.java.practice;
public class HighPass extends Filter{
double cutoff;
public HighPass(double cutoff) {
this.cutoff = cutoff;
}
public Waveform process(Waveform input){
return input;
}
}
package com.chenxyt.java.practice;
public class LowPass extends Filter{
double cutoff;
public LowPass(double cutoff) {
this.cutoff = cutoff;
}
public Waveform process(Waveform input){
return input;
}
}
package com.chenxyt.java.practice;
public class BandPass extends Filter {
double lowCutoff,highCutoff;
public BandPass(double lowCutoff,double highCutoff){
this.lowCutoff = lowCutoff;
this.highCutoff = highCutoff;
}
public Waveform process(Waveform input){
return input;
}
}
以上代码涵盖了本例中新接口、旧版本的实现、现有客户端要调用的方法,接下来是客户端和适配器,这里写到了一起:
package com.chenxyt.java.practice;

class FilterAdapter implements Processor{
Filter filter = new Filter();
public FilterAdapter(Filter filter) {
this.filter = filter;
}
@Override
public String name() {
// TODO Auto-generated method stub
return filter.name();
}
public Waveform processFunc(Object input) {
// TODO Auto-generated method stub
return filter.process((Waveform)input);
}
}
public class FilterProcessor{
public static void main(String[] args) {
Waveform w = new Waveform();
Apply.doProcess(new FilterAdapter(new LowPass(1.0)),w);
Apply.doProcess(new FilterAdapter(new HighPass(2.0)),w);
Apply.doProcess(new FilterAdapter(new BandPass(3.0,4.0)),w);
}
}按照适配器模式的思想梳理一下,建立一个FilterAdapter适配器类,该类实现了Processor接口,然后在实现方法processFunc内部调用了filter.process方法。processFunc是个可以接受协变返回值的方法。main函数里,为了看着清楚一些,我们修改一下:
public static void main(String[] args) {
Waveform w = new Waveform();
Processor p1 = new FilterAdapter(new LowPass(1.0));
Processor p2 = new FilterAdapter(new HighPass(2.0));
Processor p3 = new FilterAdapter(new BandPass(3.0,4.0));
Apply.doProcess(p1,w);
Apply.doProcess(p2,w);
Apply.doProcess(p3,w);
}Processor接口对象的类型为FilterAdapter,然后调用Apply中方法的时候传递这个对象,经过向上转型、动态加载等一系列操作,编译器就知道应该执行哪个方法了。这样下来,这个适配器模式的示例就理清楚了。

三、模式讲解

    1.模式的功能:适配器的功能是完成接口的转换,目的是已有接口的复用,而不是实现业务功能。当然适配器也可以实现一些功能,比如为了参数类型的匹配而做的一些特有的功能,这种适配器叫做智能适配器。    2.Adaptee和Target的关系:被适配的方法,和适配的接口,没有必然的关联,在一些业务场景中,二者的方法可能完全相同,(名称、参数列表),也可能完全不同,这里没有绝对的关联,具体功能具体分析。    3.对象组合:在编程思想中,讲述了实现新类的方式有组合跟继承两种关系,我们说使用组合的场景就是想使用原来类的对象实现特定的功能,这一点符合适配器的思想,我们也看到了适配器确实是使用组合的方式来完成指定的任务。    4.适配器的常见实现:在实现适配器的时候,通常是实现了一个类,这个类是实现了Target接口的类,然后在实现里调用Adaptee,也就是说适配器类与Target有关,而与Adaptee无关。    5.智能适配器:适配器中加入新的功能,或者是Adaptee中没有但是Target接口中有的功能,这种用来完成更复杂功能的适配器称作只能适配器。    6.适配多个Adaptee:适配器在适配的时候可以适配多个Adaptee,也就是说实现某个新的Target功能的时候,需要调用多个模块的功能。    7.适配器Adapter实现的复杂程度:实现的复杂程度主要取决于Target和Adaptee的相似程度,相似程度高,比如只有方法名不同,那么简单转换一下就可以了,相似程度低,可能就要用到更为复杂的组合调用了。    8.缺省适配:缺省适配的意思是,为一个接口提供缺省实现。有了它就不用直接去实现接口,而是采用去继承这个缺省适配对象,从而让子类可以有选择的去覆盖实现需要的方法,对于不需要的方法,使用缺省适配的方法就可以了。

四、双向适配

    适配器也支持双向适配的形式,前文中我们使用Adaptee适配Target,实际上也可以同时使用Target适配Adaptee,这种适配形式叫做双向适配器。    回顾之前的日志系统的例子,假如现在客户有了新的需求,第二版日志还处于使用阶段,要求能够使用第一版的接口同时也实现第二版的功能,也就是说使用第一版的接口实现第二版的功能,然后使用第二版的接口可以实现第一版的功能,这种交叉匹配的关系我们可以使用双向适配器来完成。首选我们实现一下数据库存储日志的代码,前文只提供了接口,这里提供一个实现,但是实现没有真正的操作数据库,只是便于后边的适配器使用:
package com.chenxyt.java.test;import java.util.List;//DB存储日志的实现
public class LogDbOperate implements LogDbOperateApi { @Override
 public void createLog(LogModel lm) {
  // TODO Auto-generated method stub
  System.out.println("现在进行数据库存储日志" + lm);
 }
 @Override
 public void updateLog(LogModel lm) {
  // TODO Auto-generated method stub
  System.out.println("现在更新数据库日志" + lm);
 }
 @Override
 public void deleteLog(LogModel lm) {
  // TODO Auto-generated method stub
  System.out.println("现在删除数据库日志" + lm);
 }
 @Override
 public List<LogModel> getAllLog() {
  // TODO Auto-generated method stub
  System.out.println("现在获取数据库日志");
  return null;
 }}

然后是现实双向适配器,因为是双向适配,所以它实现了两个接口,这里类的构造函数我分开了写,便于后边调用理解,原文中写了一个构造器传入了两个对象参数:
package com.chenxyt.java.test;

import java.util.List;

public class TwoDirectAdapter implements LogDbOperateApi,LogFileOperateApi{
private LogDbOperate logDbOperate;
private LogFileOperate logFileOperate;

public TwoDirectAdapter(LogDbOperate logDbOperate) {
// TODO Auto-generated constructor stub
this.logDbOperate = logDbOperate;
}
public TwoDirectAdapter(LogFileOperate logFileOperate) {
// TODO Auto-generated constructor stub
this.logFileOperate = logFileOperate;
}
@Override
public List<LogModel> readFile() {
// TODO Auto-generated method stub
return logDbOperate.getAllLog();
}
@Override
public void writeLogFile(List<LogModel> list) {
// TODO Auto-generated method stub
for(LogModel lm:list){
logDbOperate.createLog(lm);
}
}
@Override
public void createLog(LogModel lm) {
// TODO Auto-generated method stub
List<LogModel> list = logFileOperate.readFile();
list.add(lm);
logFileOperate.writeLogFile(list);
}
@Override
public void updateLog(LogModel lm) {
// TODO Auto-generated method stub
}
@Override
public void deleteLog(LogModel lm) {
// TODO Auto-generated method stub
}
@Override
public List<LogModel> getAllLog() {
// TODO Auto-generated method stub
return logFileOperate.readFile();
}

}

最后我们写一个客户端来使用这个适配器:
package com.chenxyt.java.test;
import java.util.ArrayList;
import java.util.List;
public class Client {
public static void main(String[] args) {
LogModel lgm = new LogModel();
//要适配的方法
LogFileOperate logFileOperate = new LogFileOperate("");
LogDbOperate logDbOperate = new LogDbOperate();
lgm.setLogId("1");
lgm.setOperateUser("chenxyt");
lgm.setOperateTime("2018-3-30 17:48:19");
lgm.setLogContent("This is a Test");
List<LogModel> list = new ArrayList<LogModel>();
list.add(lgm);
//定义数据库操作接口 实际使用文件形式
LogDbOperateApi opt = new TwoDirectAdapter(logFileOperate);
//定义文件操作接口 实际使用数据库操作形式
LogFileOperateApi lgf = new TwoDirectAdapter(logDbOperate);
//写入文件
opt.createLog(lgm);
//读取文件
List<LogModel> readLog = lgf.readFile();
System.out.println("readLog==" + readLog);
}
}

    这里可以看出实际上双向适配器的实现就是实现了两个接口,然后传入了两个对象,接口内部交叉使用对象调用方法。

五、对象适配器和类适配器

    对象适配器就是使用对象组合的形式来完成适配的工作本文这种适配器模式,而类适配器就是提供一个子类同时继承Target和Adaptee这两个类,也就是多重继承,而由于Java中不支持多重继承,因此目前还没有涉及到类适配器。简单的类似类适配器的例子我们可以写一个适配器去继承Target类,然后在类的内部实现Adaptee的功能。 改变的地方在于:    1.需要继承LogFileOperate的实现,然后再实现LogDbOperateApi的接口    2.需要按照继承LogFileOperate的要求,传入对应的参数    3.不需要再持有LogFileOperate对象进行组合了,因为适配器本身就是继承了LogFileOperate的子类    4.以前调用被适配对象的方法的地方,全部修改成调用自己的方法(适配器类对象的方法 使用this进行调用),当然这里是调用继承的类的方法,而不是实现接口的方法。我们是让继承的类去适配实现的接口。提供一个版本的示例代码:
package com.chenxyt.java.test;

import java.util.List;

public class ClassAdapter extends LogFileOperate implements LogDbOperateApi{
public ClassAdapter(String LogFileName) {
super(LogFileName);
// TODO Auto-generated constructor stub
}
@Override
public void createLog(LogModel lm) {
// TODO Auto-generated method stub
List <LogModel> list = this.readFile();
list.add(lm);
this.writeLogFile(list);
}

@Override
public void updateLog(LogModel lm) {
// TODO Auto-generated method stub
}

@Override
public void deleteLog(LogModel lm) {
// TODO Auto-generated method stub
}

@Override
public List<LogModel> getAllLog() {
// TODO Auto-generated method stub
return this.readFile();
}
}

类适配器和对象适配器的权衡:从实现上来说,类适配器使用了继承的方式,这是一种静态的方式,而对象适配器使用了组合对象的形式,这是一种动态的形式。类适配器由于是继承的关系,所以适配器不能再和Adaptee的子类一起工作,而对象适配器采用对象组合的形式,却不关心Adaptee还是其子类。对于类适配器,适配器可以重新定义Adaptee,也就是覆盖其方法,而对象适配器如果想这样做,必须适配Adaptee的子类,让子类实现功能的覆盖。对于类适配器,仅仅引入了一个对象,不需要 额外的引用间接的得到Adaptee,而对象适配器需要额外的引用。不管哪种方式,适合的就是最好的。

六、适配器模式的优缺点

优点:    1.复用性强,如果一个功能已经存在,使用适配器模式可以更好的在现有接口的基础上去适配已经存在的功能。    2.可扩展性强,智能适配器内部可以实现其它的业务功能,从而很自然的扩展了系统的功能。缺点:    1.过多的使用适配器模式会使系统更加凌乱,不易于整体的把控,比如明明调用的A接口,内部实现确实B接口,所以如果不是很必要的情况下,可以直接重构而不是使用适配器。

七、思考&总结

    1.适配器的本质:转换匹配,复用功能,适配器通过转换的形式(组合对象)来完成接口的相互匹配,也就是支持双向匹配双向适配,同时适配器内部可以实现其它的业务功能,这种称为只能适配器。     2.何时选用:如果你想使用一个已经存在的类,但是这个接口不符合你的规范,这种情况可以使用适配器模式来完成转换。如果你想创建一个可复用的类,这个类可以和一些不兼容的类一起工作,那么可以使用适配器模式,到时候需要什么就适配什么。如果你想使用已经存在的子类,但是不能对这些子类都进行匹配,那么可以使用适配器模式,只对其父类进行适配即可。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: