服务器之家

服务器之家 > 正文

利用C#实现AOP常见的几种方法详解

时间:2022-01-21 14:01     来源/作者:梦在旅途

前言

aop为aspect oriented programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的中统一处理业务逻辑的一种技术,比较常见的场景是:日志记录,错误捕获、性能监控等

aop的本质是通过代理对象来间接执行真实对象,在代理类中往往会添加装饰一些额外的业务代码,比如如下代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class reala
 {
 public virtual string pro { get; set; }
 
 public virtual void showhello(string name)
 {
 console.writeline($"hello!{name},welcome!");
 }
 }
 
 
//调用:
 
 var a = new reala();
 a.pro = "测试";
 a.showhello("梦在旅途");

这段代码很简单,只是new一个对象,然后设置属性及调用方法,但如果我想在设置属性前后及调用方法前后或报错都能收集日志信息,该如何做呢?可能大家会想到,在设置属性及调用方法前后都加上记录日志的代码不就可以了,虽然这样是可以,但如果很多地方都要用到这个类的时候,那重复的代码是否太多了一些吧,所以我们应该使用代理模式或装饰模式,将原有的真实类reala委托给代理类proxyreala来执行,代理类中在设置属性及调用方法时,再添加记录日志的代码就可以了,这样可以保证代码的干净整洁,也便于代码的后期维护。(注意,在c#中若需被子类重写,父类必需是虚方法或虚属性virtual)

如下代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class proxyreala : reala
 {
 
 public override string pro
 {
 get
 {
 return base.pro;
 }
 set
 {
 showlog("设置pro属性前日志信息");
 base.pro = value;
 showlog($"设置pro属性后日志信息:{value}");
 }
 }
 
 public override void showhello(string name)
 {
 try
 {
 showlog("showhello执行前日志信息");
 base.showhello(name);
 showlog("showhello执行后日志信息");
 }
 catch(exception ex)
 {
 showlog($"showhello执行出错日志信息:{ex.message}");
 }
 }
 
 private void showlog(string log)
 {
 console.writeline($"{datetime.now.tostring()}-{log}");
 }
 }
 
 
//调用:
 var aa = new proxyreala();
 aa.pro = "测试2";
 aa.showhello("zuowenjun.cn");

这段代码同样很简单,就是proxyreala继承自reala类,即可看成是proxyreala代理reala,由proxyreala提供各种属性及方法调用。这样在proxyreala类内部属性及方法执行前后都有统一记录日志的代码,不论在哪里用这个reala类,都可以直接用proxyreala类代替,因为里氏替换原则,父类可以被子类替换,而且后续若想更改日志记录代码方式,只需要在proxyreala中更改就行了,这样所有用到的proxyreala类的日志都会改变,是不是很爽。

上述执行结果如下图示:

利用C#实现AOP常见的几种方法详解

以上通过定义代理类的方式能够实现在方法中统一进行各种执行点的拦截代码逻辑处理,拦截点(或者称为:横切面,切面点)一般主要为:执行前,执行后,发生错误,虽然解决了之前直接调用真实类reala时,需要重复增加各种逻辑代码的问题,但随之而来的新问题又来了,那就是当一个系统中的类非常多的时候,如果我们针对每个类都定义一个代理类,那么系统的类的个数会成倍增加,而且不同的代理类中可能某些拦截业务逻辑代码都是相同的,这种情况同样是不能允许的,那有没有什么好的办法呢?答案是肯定的,以下是我结合网上资源及个人总结的如下几种常见的实现aop的方式,各位可以参考学习。

第一种:静态织入,即:在编译时,就将各种涉及aop拦截的代码注入到符合一定规则的类中,编译后的代码与我们直接在reala调用属性或方法前后增加代码是相同的,只是这个工作交由编译器来完成。

postsharp:postsharp的aspect是使用attribute实现的,我们只需事先通过继承自onmethodboundaryaspect,然后重写几个常见的方法即可,如:onentry,onexit等,最后只需要在需要进行aop拦截的属性或方法上加上aop拦截特性类即可。由于postsharp是静态织入的,所以相比其它的通过反射或emit反射来说效率是最高的,但postsharp是收费版本的,而且网上的教程比较多,我就不在此重复说明了。

第二种:emit反射,即:通过emit反射动态生成代理类,如下castle.dynamicproxy的aop实现方式,代码也还是比较简单的,效率相对第一种要慢一点,但对于普通的反射来说又高一些,代码实现如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
using castle.core.interceptor;
using castle.dynamicproxy;
using nlog;
using nlog.config;
using nlog.win32.targets;
using system;
using system.collections.generic;
using system.linq;
using system.text;
using system.threading.tasks;
 
namespace consoleapp
{
 class program
 {
 static void main(string[] args)
 {
 proxygenerator generator = new proxygenerator();
 var test = generator.createclassproxy<testa>(new testinterceptor());
 console.writeline($"getresult:{test.getresult(console.readline())}");
 test.getresult2("test");
 console.readkey();
 }
 }
 
 public class testinterceptor : standardinterceptor
 {
 private static nlog.logger logger;
 
 protected override void preproceed(iinvocation invocation)
 {
 console.writeline(invocation.method.name + "执行前,入参:" + string.join(",", invocation.arguments));
 }
 
 protected override void performproceed(iinvocation invocation)
 {
 console.writeline(invocation.method.name + "执行中");
 try
 {
 base.performproceed(invocation);
 }
 catch (exception ex)
 {
 handleexception(ex);
 }
 }
 
 protected override void postproceed(iinvocation invocation)
 {
 console.writeline(invocation.method.name + "执行后,返回值:" + invocation.returnvalue);
 }
 
 private void handleexception(exception ex)
 {
 if (logger == null)
 {
 loggingconfiguration config = new loggingconfiguration();
 
 coloredconsoletarget consoletarget = new coloredconsoletarget();
 consoletarget.layout = "${date:format=hh\\:mm\\:ss} ${logger} ${message}";
 config.addtarget("console", consoletarget);
 
 loggingrule rule1 = new loggingrule("*", loglevel.debug, consoletarget);
 config.loggingrules.add(rule1);
 logmanager.configuration = config;
 
 logger = logmanager.getcurrentclasslogger(); //new nlog.logfactory().getcurrentclasslogger();
 }
 logger.errorexception("error",ex);
 }
 }
 
 public class testa
 {
 public virtual string getresult(string msg)
 {
 string str = $"{datetime.now.tostring("yyyy-mm-dd hh:mm:ss")}---{msg}";
 return str;
 }
 
 public virtual string getresult2(string msg)
 {
 throw new exception("throw exception!");
 }
 }
}

简要说明一下代码原理,先创建proxygenerator类实例,从名字就看得出来,是代理类生成器,然后实例化一个基于继承自standardinterceptor的testinterceptor,这个testinterceptor是一个自定义的拦截器,最后通过generator.createclassproxy<testa>(new testinterceptor())动态创建了一个继承自testa的动态代理类,这个代理类只有在运行时才会生成的,后面就可以如代码所示,直接用动态代理类对象实例test操作testa的所有属性与方法,当然这里需要注意,若需要被动态代理类所代理并拦截,则父类的属性或方法必需是virtual,这点与我上面说的直接写一个代理类相同。

上述代码运行效果如下:

利用C#实现AOP常见的几种方法详解

第三种:普通反射+利用remoting的远程访问对象时的直实代理类来实现,代码如下,这个可能相比以上两种稍微复杂一点:

以上代码实现步骤说明:

1.这里定义的一个真实类aopclass必需继承自contextboundobject类,而contextboundobject类又直接继承自marshalbyrefobject类,表明该类是上下文绑定对象,允许在支持远程处理的应用程序中跨应用程序域边界访问对象,说白了就是可以获取这个真实类的所有信息,以便可以被生成动态代理。

2.定义继承自proxyattribute的代理特性标识类aopattribute,以表明哪些类可以被代理,同时注意重写createinstance方法,在createinstance方法里实现通过委托与生成透明代理类的过程,realproxy.gettransparentproxy() 非常重要,目的就是根据定义的aopproxy代理类获取生成透明代理类对象实例。

3.实现通用的aopproxy代理类,代理类必需继承自realproxy类,在这个代理类里面重写invoke方法,该方法是统一执行被代理的真实类的所有方法、属性、字段的出入口,我们只需要在该方法中根据传入的imessage进行判断并实现相应的拦截代码即可。

4.最后在需要进行aop拦截的类上标注aopattribute即可(注意:被标识的类必需是如第1条说明的继承自contextboundobject类),在实际调用的过程中是感知不到任何的变化。且aopattribute可以被子类继承,也就意味着所有子类都可以被代理并拦截。

如上代码运行效果如下:

利用C#实现AOP常见的几种方法详解

这里顺便分享微软官方如果利用realproxy类实现aop的,详见地址:https://msdn.microsoft.com/zh-cn/library/dn574804.aspx

第四种:反射+ 通过定义统一的出入口,并运用一些特性实现aop的效果,比如:常见的mvc、web api中的过滤器特性 ,我这里根据mvc的思路,实现了类似的mvc过滤器的aop效果,只是中间用到了反射,可能性能不佳,但效果还是成功实现了各种拦截,正如mvc一样,既支持过滤器特性,也支持controller中的action执行前,执行后,错误等方法实现拦截

实现思路如下:

a.过滤器及controller特定方法拦截实现原理:

1.获取程序集中所有继承自controller的类型;

2.根据controller的名称找到第1步中的对应的controller的类型:findcontrollertype

3.根据找到的controller类型及action的名称找到对应的方法:findaction

4.创建controller类型的实例;

5.根据action方法找到定义在方法上的所有过滤器特性(包含:执行前、执行后、错误)

6.执行controller中的onactionexecuting方法,随后执行执行前的过滤器特性列表,如:actionexecutingfilter

7.执行action方法,获得结果;

8.执行controller中的onactionexecuted方法,随后执行执行后的过滤器特性列表,如:actionexecutedfilter

9.通过try catch在catch中执行controller中的onactionerror方法,随后执行错误过滤器特性列表,如:actionerrorfilter

10.最后返回结果;

b.实现执行路由配置效果原理:

1.增加可设置路由模板列表方法:addexecroutetemplate,在方法中验证controller、action,并获取模板中的占位符数组,最后保存到类全局对象中routetemplates;

2.增加根据执行路由执行对应的controller中的action方法的效果:run,在该方法中主要遍历所有路由模板,然后与实行执行的请求路由信息通过正则匹配,若匹配ok,并能正确找到controller及action,则说明正确,并最终统一调用:process方法,执行a中的所有步骤最终返回结果。

需要说明该模拟mvc方案并没有实现action方法参数的的绑定功能,因为modelbinding本身就是比较复杂的机制,所以这里只是为了搞清楚aop的实现原理,故不作这方面的研究,大家如果有空可以实现,最终实现mvc不仅是asp.net mvc,还可以是console mvc,甚至是winform mvc等。

以下是实现的全部代码,代码中我已进行了一些基本的优化,可以直接使用:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
public abstract class controller
{
 public virtual void onactionexecuting(methodinfo action)
 {
 
 }
 
 public virtual void onactionexecuted(methodinfo action)
 {
 
 }
 
 public virtual void onactionerror(methodinfo action, exception ex)
 {
 
 }
 
}
 
public abstract class filterattribute : attribute
{
 public abstract string filtertype { get; }
 public abstract void execute(controller ctrller, object extdata);
}
 
public class actionexecutingfilter : filterattribute
{
 public override string filtertype => "before";
 
 public override void execute(controller ctrller, object extdata)
 {
 console.writeline($"我是在{ctrller.gettype().name}.actionexecutingfilter中拦截发出的消息!-{datetime.now.tostring()}");
 }
}
 
public class actionexecutedfilter : filterattribute
{
 public override string filtertype => "after";
 
 public override void execute(controller ctrller, object extdata)
 {
 console.writeline($"我是在{ctrller.gettype().name}.actionexecutedfilter中拦截发出的消息!-{datetime.now.tostring()}");
 }
}
 
public class actionerrorfilter : filterattribute
{
 public override string filtertype => "exception";
 
 public override void execute(controller ctrller, object extdata)
 {
 console.writeline($"我是在{ctrller.gettype().name}.actionerrorfilter中拦截发出的消息!-{datetime.now.tostring()}-error msg:{(extdata as exception).message}");
 }
}
 
public class appcontext
{
 private static readonly type controllertype = typeof(controller);
 private static readonly dictionary<string, type> matchedcontrollertypes = new dictionary<string, type>();
 private static readonly dictionary<string, methodinfo> matchedcontrolleractions = new dictionary<string, methodinfo>();
 private dictionary<string,string[]> routetemplates = new dictionary<string, string[]>();
 
 
 public void addexecroutetemplate(string execroutetemplate)
 {
 if (!regex.ismatch(execroutetemplate, "{controller}", regexoptions.ignorecase))
 {
  throw new argumentexception("执行路由模板不正确,缺少{controller}");
 }
 
 if (!regex.ismatch(execroutetemplate, "{action}", regexoptions.ignorecase))
 {
  throw new argumentexception("执行路由模板不正确,缺少{action}");
 }
 
 string[] keys = regex.matches(execroutetemplate, @"(?<={)\w+(?=})", regexoptions.ignorecase).cast<match>().select(c => c.value.tolower()).toarray();
 
 routetemplates.add(execroutetemplate,keys);
 }
 
 public object run(string execroute)
 {
 //{controller}/{action}/{id}
 string ctrller = null;
 string actionname = null;
 arraylist args = null;
 type controllertype = null;
 bool findresult = false;
 
 foreach (var r in routetemplates)
 {
  string[] keys = r.value;
  string execroutepattern = regex.replace(r.key, @"{(?<key>\w+)}", (m) => string.format(@"(?<{0}>.[^/\\]+)", m.groups["key"].value.tolower()), regexoptions.ignorecase);
 
  args = new arraylist();
  if (regex.ismatch(execroute, execroutepattern))
  {
  var match = regex.match(execroute, execroutepattern);
  for (int i = 0; i < keys.length; i++)
  {
   if ("controller".equals(keys[i], stringcomparison.ordinalignorecase))
   {
   ctrller = match.groups["controller"].value;
   }
   else if ("action".equals(keys[i], stringcomparison.ordinalignorecase))
   {
   actionname = match.groups["action"].value;
   }
   else
   {
   args.add(match.groups[keys[i]].value);
   }
  }
 
  if ((controllertype = findcontrollertype(ctrller)) != null && findaction(controllertype, actionname, args.toarray()) != null)
  {
   findresult = true;
   break;
  }
  }
 }
 
 if (findresult)
 {
  return process(ctrller, actionname, args.toarray());
 }
 else
 {
  throw new exception($"在已配置的路由模板列表中未找到与该执行路由相匹配的路由信息:{execroute}");
 }
 }
 
 public object process(string ctrller, string actionname, params object[] args)
 {
 type matchedcontrollertype = findcontrollertype(ctrller);
 
 if (matchedcontrollertype == null)
 {
  throw new argumentexception($"未找到类型为{ctrller}的controller类型");
 }
 
 object execresult = null;
 if (matchedcontrollertype != null)
 {
  var matchedcontroller = (controller)activator.createinstance(matchedcontrollertype);
  methodinfo action = findaction(matchedcontrollertype, actionname, args);
  if (action == null)
  {
  throw new argumentexception($"在{matchedcontrollertype.fullname}中未找到与方法名:{actionname}及参数个数:{args.count()}相匹配的方法");
  }
 
 
  var filters = action.getcustomattributes<filterattribute>(true);
  list<filterattribute> execbeforefilters = new list<filterattribute>();
  list<filterattribute> execafterfilters = new list<filterattribute>();
  list<filterattribute> exceptionfilters = new list<filterattribute>();
 
  if (filters != null && filters.count() > 0)
  {
  execbeforefilters = filters.where(f => f.filtertype == "before").tolist();
  execafterfilters = filters.where(f => f.filtertype == "after").tolist();
  exceptionfilters = filters.where(f => f.filtertype == "exception").tolist();
  }
 
  try
  {
  matchedcontroller.onactionexecuting(action);
 
  if (execbeforefilters != null && execbeforefilters.count > 0)
  {
   execbeforefilters.foreach(f => f.execute(matchedcontroller, null));
  }
 
  var mparams = action.getparameters();
  object[] newargs = new object[args.length];
  for (int i = 0; i < mparams.length; i++)
  {
   newargs[i] = convert.changetype(args[i], mparams[i].parametertype);
  }
 
  execresult = action.invoke(matchedcontroller, newargs);
 
  matchedcontroller.onactionexecuted(action);
 
  if (execbeforefilters != null && execbeforefilters.count > 0)
  {
   execafterfilters.foreach(f => f.execute(matchedcontroller, null));
  }
 
  }
  catch (exception ex)
  {
  matchedcontroller.onactionerror(action, ex);
 
  if (exceptionfilters != null && exceptionfilters.count > 0)
  {
   exceptionfilters.foreach(f => f.execute(matchedcontroller, ex));
  }
  }
 
 
 }
 
 return execresult;
 
 }
 
 private type findcontrollertype(string ctrller)
 {
 type matchedcontrollertype = null;
 if (!matchedcontrollertypes.containskey(ctrller))
 {
  var assy = assembly.getassembly(typeof(controller));
 
  foreach (var m in assy.getmodules(false))
  {
  foreach (var t in m.gettypes())
  {
   if (controllertype.isassignablefrom(t) && !t.isabstract)
   {
   if (t.name.equals(ctrller, stringcomparison.ordinalignorecase) || t.name.equals($"{ctrller}controller", stringcomparison.ordinalignorecase))
   {
    matchedcontrollertype = t;
    matchedcontrollertypes[ctrller] = matchedcontrollertype;
    break;
   }
   }
  }
  }
 }
 else
 {
  matchedcontrollertype = matchedcontrollertypes[ctrller];
 }
 
 return matchedcontrollertype;
 }
 
 private methodinfo findaction(type matchedcontrollertype, string actionname, object[] args)
 {
 string ctrlerwithactionkey = $"{matchedcontrollertype.fullname}.{actionname}";
 methodinfo action = null;
 if (!matchedcontrolleractions.containskey(ctrlerwithactionkey))
 {
  if (args == null) args = new object[0];
  foreach (var m in matchedcontrollertype.getmethods(bindingflags.instance | bindingflags.public))
  {
  if (m.name.equals(actionname, stringcomparison.ordinalignorecase) && m.getparameters().length == args.length)
  {
   action = m;
   matchedcontrolleractions[ctrlerwithactionkey] = action;
   break;
  }
  }
 }
 else
 {
  action = matchedcontrolleractions[ctrlerwithactionkey];
 }
 
 return action;
 }
}

使用前,先定义一个继承自controller的类,如:testcontroller,并重写相应的方法,或在指定的方法上加上所需的过滤器特性,如下代码所示:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class testcontroller : controller
{
 public override void onactionexecuting(methodinfo action)
 {
 console.writeline($"{action.name}执行前,onactionexecuting---{datetime.now.tostring()}");
 }
 
 public override void onactionexecuted(methodinfo action)
 {
 console.writeline($"{action.name}执行后,onactionexecuted--{datetime.now.tostring()}");
 }
 
 public override void onactionerror(methodinfo action, exception ex)
 {
 console.writeline($"{action.name}执行,onactionerror--{datetime.now.tostring()}:{ex.message}");
 }
 
 [actionexecutingfilter]
 [actionexecutedfilter]
 public string helloworld(string name)
 {
 return ($"hello world!->{name}");
 }
 
 [actionexecutingfilter]
 [actionexecutedfilter]
 [actionerrorfilter]
 public string testerror(string name)
 {
 throw new exception("这是测试抛出的错误信息!");
 }
 
 [actionexecutingfilter]
 [actionexecutedfilter]
 public int add(int a, int b)
 {
 return a + b;
 }
}

最后前端实际调用就非常简单了,代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class mvcprogram
{
 static void main(string[] args)
 {
 try
 {
  var appcontext = new appcontext();
  object rs = appcontext.process("test", "helloworld", "梦在旅途");
  console.writeline($"process执行的结果1:{rs}");
 
  console.writeline("=".padright(50, '='));
 
  appcontext.addexecroutetemplate("{controller}/{action}/{name}");
  appcontext.addexecroutetemplate("{action}/{controller}/{name}");
 
  object result1 = appcontext.run("helloworld/test/梦在旅途-zuowenjun.cn");
  console.writeline($"执行的结果1:{result1}");
 
  console.writeline("=".padright(50, '='));
 
  object result2 = appcontext.run("test/helloworld/梦在旅途-zuowenjun.cn");
  console.writeline($"执行的结果2:{result2}");
 
  console.writeline("=".padright(50, '='));
 
  appcontext.addexecroutetemplate("{action}/{controller}/{a}/{b}");
  object result3 = appcontext.run("add/test/500/20");
  console.writeline($"执行的结果3:{result3}");
 
  object result4 = appcontext.run("test/testerror/梦在旅途-zuowenjun.cn");
  console.writeline($"执行的结果4:{result4}");
 }
 catch (exception ex)
 {
  console.foregroundcolor = consolecolor.red;
  console.writeline($"发生错误:{ex.message}");
  console.resetcolor();
 }
 
 console.readkey();
 }
}

可以看到,与asp.net mvc有点类似,只是asp.net mvc是通过url访问,而这里是通过appcontext.run 执行路由url 或process方法,直接指定controller、action、参数来执行。

通过以上调用代码可以看出路由配置还是比较灵活的,当然参数配置除外。如果大家有更好的想法也可以在下方评论交流,谢谢!

mvc代码执行效果如下:

利用C#实现AOP常见的几种方法详解

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。

原文链接:http://www.cnblogs.com/zuowj/p/7501896.html

标签:
C# AOP 

相关文章

热门资讯

蜘蛛侠3英雄无归3正片免费播放 蜘蛛侠3在线观看免费高清完整
蜘蛛侠3英雄无归3正片免费播放 蜘蛛侠3在线观看免费高清完整 2021-08-24
yue是什么意思 网络流行语yue了是什么梗
yue是什么意思 网络流行语yue了是什么梗 2020-10-11
背刺什么意思 网络词语背刺是什么梗
背刺什么意思 网络词语背刺是什么梗 2020-05-22
2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全
2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全 2019-12-26
2021年耽改剧名单 2021要播出的59部耽改剧列表
2021年耽改剧名单 2021要播出的59部耽改剧列表 2021-03-05
返回顶部