使用T4模板批量生成代码
前言
之前在 “使用T4模板生成代码 - 初探” 文章简单的使用了T4模板的生成功能,但对于一个模板生成多个实例文件,如何实现这个方式呢?无意发现一个解决方案 “MultipleOutputHelper.ttinclude” ,它让基于T4模板批量生成文件实例变得简单起来了。
什么是MultipleOutputHelper.ttinclude
Damien Guard是一个在加利福尼亚州圣何塞的开发人员,他构建出处理使用T4模板输出多文件的解决方案“MultipleOutputHelper.ttinclude”
使用
1. 初始化
获取MultipleOutputHelper.ttinclude文件模板
注意: 文件可以上github.com 托管上面获取( https://github.com/damieng/DamienGKit/tree/master/T4/MultipleOutputHelper)
在T4模板中使用include指令导入MultipleOutputHelper.ttinclude文件或者将MultipleOutputHelper.ttinclude的代码复制在T4模板内。
然后初始化Manager对象,代码如下:
注意: 这里的Manager.ttinclude 就是MultipleOutputHelper.ttinclude文件模板
2. 文件块
使用代码标识区分生成的代码块的范围
该代码声明了一个Employee.generated.cs文件,文件代码内容为:
1
|
public class Employee { ... } |
3. 页眉和页脚
很多模板需要共享一个共同的页眉和页脚时,可以使用import语句进行打开和关闭。简单的使用StartHeader和StartFooter的代码方法进行分割。
4. 编译执行
使用Process方法,进行文件分割。
场景应用
基于之前的“使用T4模板生成代码 - 初探” 文章的场景,进行基于NHibernate Mapper 来获取Domain对象,然后进行批量生成多个代码文件。
1. 自定义T4模板,文件名为“EntityRepositoryTemplate.tt”,代码如下:
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
|
<#@ template language= "C#" debug= "true" hostspecific= "True" #> // 导入MultipleOutputHelper.ttinclude文件 <#@include file= "$(SolutionDir)app\T4\MultipleOutputHelper.ttinclude" #> // 导入相关的DLL <#@ Assembly Name= "$(SolutionDir)lib\SharpArch.2.0.2\NHibernate.dll" #> <#@ Assembly Name= "$(SolutionDir)lib\SharpArch.2.0.2\SharpArch.NHibernate.dll" #> <#@ Assembly Name= "$(SolutionDir)lib\SharpArch.2.0.2\SharpArch.Domain.dll" #> <#@ Assembly Name= "$(SolutionDir)lib\SharpArch.2.0.2\FluentNHibernate.dll" #> <#@ Assembly Name= "$(SolutionDir)app\Cotide.Data\bin\$(ConfigurationName)\Cotide.Infrastructure.dll" #> <#@ Assembly Name= "$(SolutionDir)app\Cotide.Core\bin\$(ConfigurationName)\Cotide.Domain.dll" #> <#@ Assembly Name= "$(SolutionDir)app\Cotide.Framework\bin\$(ConfigurationName)\Cotide.Framework.dll" #> <#@ import namespace = "System.IO" #> <#@ import namespace = "System" #> <#@ import namespace = "System.Configuration" #> <# // 初始化 SharpArch.NHibernate.NHibernateSession.CloseAllSessions(); SharpArch.NHibernate.NHibernateSession.Reset(); string projectPath = @"C:\资料\Person\项目\Codeplex\电子商务\app\" ; string nhibernatePath = projectPath + @"Cotide.Web\NHibernate.config" ; string [] mappingAssemblies = new [] { @"C:\资料\Person\项目\Codeplex\电子商务\app\Cotide.Data\bin\Release\Cotide.Infrastructure.dll" }; // 加载配置 NHibernate.Cfg.Configuration configuration = SharpArch.NHibernate.NHibernateSession.Init( new SharpArch.NHibernate.SimpleSessionStorage(), mappingAssemblies, new Cotide.Infrastructure.NHibernateMaps.AutoPersistenceModelGenerator().Generate(), nhibernatePath); // 获取所有类映射 var allClassMetadata = SharpArch.NHibernate.NHibernateSession.GetDefaultSessionFactory().GetAllClassMetadata(); var manager = Manager.Create(Host, GenerationEnvironment); foreach (var entry in allClassMetadata) { var entityName = entry.Value.EntityName.Split( '.' ); var className = entityName[entityName.Length - 1]; // 定义输出文件 manager.StartNewFile( "I" +className+ "Repository.cs" ); #>//------------------------------------------------------------------- //版权所有:版权所有(C) 2012,Microsoft(China) Co.,LTD //系统名称: //文件名称:I<#=className#>Repository.cs //模块名称: //模块编号: //作 者:xcli //创建时间:2013/4/6 12:49:50 //功能说明: //----------------------------------------------------------------- //修改记录: //修改人: //修改时间: //修改内容: //----------------------------------------------------------------- using System; using System.Collections.Generic; using System.Linq; using System.Text; using Cotide.Domain.Contracts.Repositories.Extension; namespace Cotide.Domain.Contracts.Repositories { public interface I<#=className#>Repository : IDbProxyRepository<<#=className#>> { } } <# // 结束输出文件 manager.EndBlock(); } // 执行编译 manager.Process( true ); #> |
输出文件效果:
让程序自动执行基于T4文件的编译工作
在MSDN-”演练:创建自定义文本模板宿主“ 文章里面看到一段代码,进行了T4文件的编译工作,代码如下:
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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
|
using System; using System.IO; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Text; using Microsoft.VisualStudio.TextTemplating; namespace CustomHost { //The text template transformation engine is responsible for running //the transformation process. //The host is responsible for all input and output, locating files, //and anything else related to the external environment. //------------------------------------------------------------------------- class CustomCmdLineHost : ITextTemplatingEngineHost { //the path and file name of the text template that is being processed //--------------------------------------------------------------------- internal string TemplateFileValue; public string TemplateFile { get { return TemplateFileValue; } } //This will be the extension of the generated text output file. //The host can provide a default by setting the value of the field here. //The engine can change this value based on the optional output directive //if the user specifies it in the text template. //--------------------------------------------------------------------- private string fileExtensionValue = ".txt" ; public string FileExtension { get { return fileExtensionValue; } } //This will be the encoding of the generated text output file. //The host can provide a default by setting the value of the field here. //The engine can change this value based on the optional output directive //if the user specifies it in the text template. //--------------------------------------------------------------------- private Encoding fileEncodingValue = Encoding.UTF8; public Encoding FileEncoding { get { return fileEncodingValue; } } //These are the errors that occur when the engine processes a template. //The engine passes the errors to the host when it is done processing, //and the host can decide how to display them. For example, the host //can display the errors in the UI or write them to a file. //--------------------------------------------------------------------- private CompilerErrorCollection errorsValue; public CompilerErrorCollection Errors { get { return errorsValue; } } //The host can provide standard assembly references. //The engine will use these references when compiling and //executing the generated transformation class. //-------------------------------------------------------------- public IList< string > StandardAssemblyReferences { get { return new string [] { //If this host searches standard paths and the GAC, //we can specify the assembly name like this. //--------------------------------------------------------- //"System" //Because this host only resolves assemblies from the //fully qualified path and name of the assembly, //this is a quick way to get the code to give us the //fully qualified path and name of the System assembly. //--------------------------------------------------------- typeof (System.Uri).Assembly.Location }; } } //The host can provide standard imports or using statements. //The engine will add these statements to the generated //transformation class. //-------------------------------------------------------------- public IList< string > StandardImports { get { return new string [] { "System" }; } } //The engine calls this method based on the optional include directive //if the user has specified it in the text template. //This method can be called 0, 1, or more times. //--------------------------------------------------------------------- //The included text is returned in the context parameter. //If the host searches the registry for the location of include files, //or if the host searches multiple locations by default, the host can //return the final path of the include file in the location parameter. //--------------------------------------------------------------------- public bool LoadIncludeText( string requestFileName, out string content, out string location) { content = System.String.Empty; location = System.String.Empty; //If the argument is the fully qualified path of an existing file, //then we are done. //---------------------------------------------------------------- if (File.Exists(requestFileName)) { content = File.ReadAllText(requestFileName); return true ; } //This can be customized to search specific paths for the file. //This can be customized to accept paths to search as command line //arguments. //---------------------------------------------------------------- else { return false ; } } //Called by the Engine to enquire about //the processing options you require. //If you recognize that option, return an //appropriate value. //Otherwise, pass back NULL. //-------------------------------------------------------------------- public object GetHostOption( string optionName) { object returnObject; switch (optionName) { case "CacheAssemblies" : returnObject = true ; break ; default : returnObject = null ; break ; } return returnObject; } //The engine calls this method to resolve assembly references used in //the generated transformation class project and for the optional //assembly directive if the user has specified it in the text template. //This method can be called 0, 1, or more times. //--------------------------------------------------------------------- public string ResolveAssemblyReference( string assemblyReference) { //If the argument is the fully qualified path of an existing file, //then we are done. (This does not do any work.) //---------------------------------------------------------------- if (File.Exists(assemblyReference)) { return assemblyReference; } //Maybe the assembly is in the same folder as the text template that //called the directive. //---------------------------------------------------------------- string candidate = Path.Combine(Path.GetDirectoryName( this .TemplateFile), assemblyReference); if (File.Exists(candidate)) { return candidate; } //This can be customized to search specific paths for the file //or to search the GAC. //---------------------------------------------------------------- //This can be customized to accept paths to search as command line //arguments. //---------------------------------------------------------------- //If we cannot do better, return the original file name. return "" ; } //The engine calls this method based on the directives the user has //specified in the text template. //This method can be called 0, 1, or more times. //--------------------------------------------------------------------- public Type ResolveDirectiveProcessor( string processorName) { //This host will not resolve any specific processors. //Check the processor name, and if it is the name of a processor the //host wants to support, return the type of the processor. //--------------------------------------------------------------------- if ( string .Compare(processorName, "XYZ" , StringComparison.OrdinalIgnoreCase) == 0) { //return typeof(); } //This can be customized to search specific paths for the file //or to search the GAC //If the directive processor cannot be found, throw an error. throw new Exception( "Directive Processor not found" ); } //A directive processor can call this method if a file name does not //have a path. //The host can attempt to provide path information by searching //specific paths for the file and returning the file and path if found. //This method can be called 0, 1, or more times. //--------------------------------------------------------------------- public string ResolvePath( string fileName) { if (fileName == null ) { throw new ArgumentNullException( "the file name cannot be null" ); } //If the argument is the fully qualified path of an existing file, //then we are done //---------------------------------------------------------------- if (File.Exists(fileName)) { return fileName; } //Maybe the file is in the same folder as the text template that //called the directive. //---------------------------------------------------------------- string candidate = Path.Combine(Path.GetDirectoryName( this .TemplateFile), fileName); if (File.Exists(candidate)) { return candidate; } //Look more places. //---------------------------------------------------------------- //More code can go here... //If we cannot do better, return the original file name. return fileName; } //If a call to a directive in a text template does not provide a value //for a required parameter, the directive processor can try to get it //from the host by calling this method. //This method can be called 0, 1, or more times. //--------------------------------------------------------------------- public string ResolveParameterValue( string directiveId, string processorName, string parameterName) { if (directiveId == null ) { throw new ArgumentNullException( "the directiveId cannot be null" ); } if (processorName == null ) { throw new ArgumentNullException( "the processorName cannot be null" ); } if (parameterName == null ) { throw new ArgumentNullException( "the parameterName cannot be null" ); } //Code to provide "hard-coded" parameter values goes here. //This code depends on the directive processors this host will interact with. //If we cannot do better, return the empty string. return String.Empty; } //The engine calls this method to change the extension of the //generated text output file based on the optional output directive //if the user specifies it in the text template. //--------------------------------------------------------------------- public void SetFileExtension( string extension) { //The parameter extension has a '.' in front of it already. //-------------------------------------------------------- fileExtensionValue = extension; } //The engine calls this method to change the encoding of the //generated text output file based on the optional output directive //if the user specifies it in the text template. //---------------------------------------------------------------------- public void SetOutputEncoding(System.Text.Encoding encoding, bool fromOutputDirective) { fileEncodingValue = encoding; } //The engine calls this method when it is done processing a text //template to pass any errors that occurred to the host. //The host can decide how to display them. //--------------------------------------------------------------------- public void LogErrors(CompilerErrorCollection errors) { errorsValue = errors; } //This is the application domain that is used to compile and run //the generated transformation class to create the generated text output. //---------------------------------------------------------------------- public AppDomain ProvideTemplatingAppDomain( string content) { //This host will provide a new application domain each time the //engine processes a text template. //------------------------------------------------------------- return AppDomain.CreateDomain( "Generation App Domain" ); //This could be changed to return the current appdomain, but new //assemblies are loaded into this AppDomain on a regular basis. //If the AppDomain lasts too long, it will grow indefintely, //which might be regarded as a leak. //This could be customized to cache the application domain for //a certain number of text template generations (for example, 10). //This could be customized based on the contents of the text //template, which are provided as a parameter for that purpose. } } //This will accept the path of a text template as an argument. //It will create an instance of the custom host and an instance of the //text templating transformation engine, and will transform the //template to create the generated text output file. //------------------------------------------------------------------------- class Program { static void Main( string [] args) { try { ProcessTemplate(args); } catch (Exception ex) { Console.WriteLine(ex.Message); } } static void ProcessTemplate( string [] args) { string templateFileName = null ; if (args.Length == 0) { throw new System.Exception( "you must provide a text template file path" ); } templateFileName = args[0]; if (templateFileName == null ) { throw new ArgumentNullException( "the file name cannot be null" ); } if (!File.Exists(templateFileName)) { throw new FileNotFoundException( "the file cannot be found" ); } CustomCmdLineHost host = new CustomCmdLineHost(); Engine engine = new Engine(); host.TemplateFileValue = templateFileName; //Read the text template. string input = File.ReadAllText(templateFileName); //Transform the text template. string output = engine.ProcessTemplate(input, host); string outputFileName = Path.GetFileNameWithoutExtension(templateFileName); outputFileName = Path.Combine(Path.GetDirectoryName(templateFileName), outputFileName); outputFileName = outputFileName + "1" + host.FileExtension; File.WriteAllText(outputFileName, output, host.FileEncoding); foreach (CompilerError error in host.Errors) { Console.WriteLine(error.ToString()); } } } } |
联想:基于这个代码实例,可以做一个基于T4模板来批量输出代码的小工具,待续.....
参考资料
- MultipleOutputHelper.ttinclude 文件作者(Damien Guard)- 博客( http://damieng.com )
- Damien Guard (Github)( https://github.com/damieng )
- 创建自定义文本模板宿主( MSDN) ( http://msdn.microsoft.com/zh-cn/library/vstudio/bb126579.aspx )
- 使用T4模板生成代码 - 初探 ( http://www.cotide.com/xcli/Blog/Article/44 )
ASP.NET MVC+EF框架+EasyUI实现权限管系列(2)-数据库访问层的设计Demo
前言:这篇博客我们继续来实现我的权限系列,这个博客一段时间也没有写了,重点是我在想还写不写,最终我决定还是写下去,因为我们是为了学习,当别人提出意见的时候,我们可以参考和采纳,但是我们不一定非要采纳,上几篇博客大家都说用CodeFirst来实现,是啊,现在基本很少有人用我的这种方法来实现了,都是用CodeFirst来实现,但是我写这篇博客的目的不是为了学多少东西,而是为了学一种编程的思想,所以我今天继续这个话题我们聊下去。
1.模型设计
(1)今天我们先来初步的设计一下模型的搭建,也就是我们在edmx文件下面搭建出我们自己设计的实体对象,添加实体的步骤我就在这里不罗嗦了,大家看图就能够设计出来,如图所示:
(2) 解释:UserInfo(用户表)实体表里面的UName的属性最大长度为32,可以为Null为true,其他默认,Pwd的属性最大长度为16,可以为Null为true,其他默认,
(3) Role(角色)表里面的RoleName的属性最大长度为32,可以为Null为true,其他默认。
(4) 注意:当我们添加实体的时候,选择添加实体集的时候后面会有一个“集”字,我们记得要删除它。
(5)当我们设计完这个简单的Demo之后,我们将使用模型的关系生成数据库,这个步骤我也不往出来写了,相信有一点基础的同学都知道该如何实现。
2.搭建架构-设计数据访问层
(1)我们在数据库访问层(LYZJ.UserLimitMVC.DAL) 下面添加UserInfoRepository(用户仓储),RoleRepository(角色仓储),这些功能是为了实现对数据库的操作,也就是增删改查,注意了,这里会有一个问题,我们的用户和角色都会用到数据库的操作(增删改查),那么我们怎么办呢,想必这里大家已经清楚该怎么办了,当我们遇到公共的东西的时候我们最好能够抽象出来实现一个固定的功能的基类,然后我们只对其进行继承操作。
(2) 我们的Demo设计的数据库访问层的架构如图所示:
3.对基类(BaseRepository(仓储))操作数据库(增删改查)的实现。
(1)添加引用
当我们对基类进行操作的时候,我们需要用到我们刚才建立的实体的DLL和使用Entity FrameWork操作数据库的DLL,下面我们在数据库访问层添加这两个引用,分别是:LYZJ.UserLimitMVC.Model,System.Data.Entity。
(2)对基类(BaseRepository)操作数据库的实现方法,含有增删改查和分页查询,代码如下:
1 using System.Data; 2 3 using LYZJ.UserLimitMVC.Model; 4 5 using System; 6 7 using System.Collections.Generic; 8 9 using System.Linq; 10 11 using System.Text; 12 13 using System.Threading.Tasks; 14 15 16 17 namespace LYZJ.UserLimitMVC.DAL 18 19 { 20 21 /// <summary> 22 23 /// 实现对数据库的操作(增删改查)的基类 24 25 /// </summary> 26 27 /// <typeparam name="T">定义泛型,约束其是一个类</typeparam> 28 29 public class BaseRepository<T> where T : class 30 31 { 32 33 //创建EF框架的上下文 34 35 private DataModelContainer db = new DataModelContainer(); 36 37 38 39 // 实现对数据库的添加功能,添加实现EF框架的引用 40 41 public T AddEntity(T entity) 42 43 { 44 45 //EF4.0的写法 添加实体 46 47 //db.CreateObjectSet<T>().AddObject(entity); 48 49 //EF5.0的写法 50 51 db.Entry<T>(entity).State = EntityState.Added; 52 53 54 55 //下面的写法统一 56 57 db.SaveChanges(); 58 59 return entity; 60 61 } 62 63 64 65 //实现对数据库的修改功能 66 67 public bool UpdateEntity(T entity) 68 69 { 70 71 //EF4.0的写法 72 73 //db.CreateObjectSet<T>().Addach(entity); 74 75 //db.ObjectStateManager.ChangeObjectState(entity, EntityState.Modified); 76 77 //EF5.0的写法 78 79 db.Set<T>().Attach(entity); 80 81 db.Entry<T>(entity).State = EntityState.Modified; 82 83 84 85 return db.SaveChanges() > 0; 86 87 } 88 89 90 91 //实现对数据库的删除功能 92 93 public bool DeleteEntity(T entity) 94 95 { 96 97 //EF4.0的写法 98 99 //db.CreateObjectSet<T>().Addach(entity); 100 101 //db.ObjectStateManager.ChangeObjectState(entity, EntityState.Deleted); 102 103 //EF5.0的写法 104 105 db.Set<T>().Attach(entity); 106 107 db.Entry<T>(entity).State = EntityState.Deleted; 108 109 110 111 return db.SaveChanges() > 0; 112 113 } 114 115 116 117 //实现对数据库的查询 --简单查询 118 119 public IQueryable<T> LoadEntities(Func<T, bool> whereLambda) 120 121 { 122 123 //EF4.0的写法 124 125 //return db.CreateObjectSet<T>().Where<T>(whereLambda).AsQueryable(); 126 127 //EF5.0的写法 128 129 return db.Set<T>().Where<T>(whereLambda).AsQueryable(); 130 131 } 132 133 134 135 /// <summary> 136 137 /// 实现对数据的分页查询 138 139 /// </summary> 140 141 /// <typeparam name="S">按照某个类进行排序</typeparam> 142 143 /// <param name="pageIndex">当前第几页</param> 144 145 /// <param name="pageSize">一页显示多少条数据</param> 146 147 /// <param name="total">总条数</param> 148 149 /// <param name="whereLambda">取得排序的条件</param> 150 151 /// <param name="isAsc">如何排序,根据倒叙还是升序</param> 152 153 /// <param name="orderByLambda">根据那个字段进行排序</param> 154 155 /// <returns></returns> 156 157 public IQueryable<T> LoadPageEntities<S>(int pageIndex, int pageSize, out int total, Func<T, bool> whereLambda, 158 159 bool isAsc, Func<T, S> orderByLambda) 160 161 { 162 163 //EF4.0和上面的查询一样 164 165 //EF5.0 166 167 var temp = db.Set<T>().Where<T>(whereLambda); 168 169 total = temp.Count(); //得到总的条数 170 171 //排序,获取当前页的数据 172 173 if (isAsc) 174 175 { 176 177 temp = temp.OrderBy<T, S>(orderByLambda) 178 179 .Skip<T>(pageSize * (pageIndex - 1)) //越过多少条 180 181 .Take<T>(pageSize).AsQueryable(); //取出多少条 182 183 } 184 185 else 186 187 { 188 189 temp = temp.OrderByDescending<T, S>(orderByLambda) 190 191 .Skip<T>(pageSize*(pageIndex - 1)) //越过多少条 192 193 .Take<T>(pageSize).AsQueryable(); //取出多少条 194 195 } 196 197 return temp.AsQueryable(); 198 199 } 200 201 } 202 203 }
4.继承实现用户和角色的操作数据库的方法
(1)当我们写完操作数据库的基类的时候,这时候我们就要对用户和角色实现对数据库的操作,我在上面就说了,这时候我们可以使用继承基类来直接实现对数据库的操作。
(2)用户仓储(UserInfoRepository继承基类的代码
1 namespace LYZJ.UserLimitMVC.DAL 2 3 { 4 public class UserInfoRepository:BaseRepository<UserInfo> 5 6 { 7 8 } 9 10 }
(3)角色仓储(RoleRepository)继承基类的代码
1 namespace LYZJ.UserLimitMVC.DAL 2 3 { 4 public class RoleRepository : BaseRepository<Role> 5 6 { 7 8 } 9 }
5.图形总结流程-对此代码实现的图形总结
(1)到这里,我们的这篇博客算是写完了,当然很多东西我们没有实现,而且也看不到效果,我现在会加快速度让大家看到效果,在最后我将画一张图来说明一下这个实现的效果,如图所示:
(2)下篇博客我们开始讨论面向接口的编程。期待中,晚上回来继续完成。
作者:韩迎龙
出处:http://www.cnblogs.com/hanyinglong
MVC/.NET群:159227188
本页版权归作者和博客园所有,欢迎转载,但未经作者同意必须保留此段声明, 且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利
友情链接:夜鹰网络
友情链接:CK.wave's Blog