用ExpressionTree实现JSON解析器
今年的春节与往年不同,对每个人来说都是刻骨铭心的。突入其来的新型冠状病毒使大家过上了“梦想”中的生活:吃了睡,睡了吃,还不用去公司上班,如今这样的生活就在我们面前,可一点都不踏实,只有不停的学习才能让人安心。于是我把年前弄了一点的JSON解析器实现了一下,序列化/反序列化对象转换这部分主要用到了ExpressionTree来实现,然后写了这篇文章来介绍这个项目。
先展示一下使用方法:
public class Student { public int Id { get; set; } public string Name { get; set; } public Sex Sex { get; set; } public DateTime? Birthday { get; set; } public string Address { get; set; } } public enum Sex { Unkown,Male,Female, }
Student
json反序列化成Student
var json = "{\"id\":100,\"Name\":\"张三\",\"Sex\":1,\"Birthday\":\"2000-10-10\"}"; var student = JsonParse.To<Student>(json);
Student序列化为json:
var student = new Student { Id = , Name = "testName", Sex = Sex.Unkown, Address = "北京市海淀区", Birthday = DateTime.Now }; var json = JsonParse.ToJson(student); //{"Id":111,"Name":"testName","Sex":"Unkown","Birthday":"2020-02-15 17:43:31","Address":"北京市海淀区"} var option = new JsonOption { WriteEnumValue = true, //序列化时使用枚举值 DateTimeFormat = "yyyy-MM-dd" //指定datetime格式 }; var json2 = JsonParse.ToJson(student, option); //{"Id":111,"Name":"testName","Sex":0,"Birthday":"2020-02-15","Address":"北京市海淀区"}
json to List,Ienumerable,Array:
var json = "[{\"id\":100,\"Name\":\"张三\",\"Sex\":1,\"Birthday\":\"2000-10-10\"},{\"id\":101,\"Name\":\"李四\",\"Sex\":\"female\",\"Birthday\":null,\"Address\":\"\"}]"; var list = JsonParse.To<List<Student>>(json); var list2 = JsonParse.To<IEnumerable<Student>>(json); var arr = JsonParse.To<Student[]>(json);
List<Stuednt> 转换为json
var list = new List<Student> { ,Name=,,) }, ,Name="username2",Sex=Sex.Female}, }; var json1 = JsonParse.ToJson(list, true); //使用缩进格式,默认是压缩的json /* [ { "Id":123, "Name":"username1", "Sex":"Male", "Birthday":"1980-01-01 00:00:00", "Address":null }, { "Id":125, "Name":"username2", "Sex":"Female", "Birthday":null, "Address":null } ] */ var option = new JsonOption { Indented = true, //缩进格式 DateTimeFormat = "yyyy-MM-dd", IgnoreNullValue = true //忽略null输出 }; var json2 = JsonParse.ToJson(list, option); /* [ { "Id":123, "Name":"username1", "Sex":"Male", "Birthday":"1980-01-01" }, { "Id":125, "Name":"username2", "Sex":"Female" } ] */
json转为Dictironary:
//Json to Dictionary var json = "{\"确诊病例\":66580,\"疑似病例\":8969,\"治愈病例\":8286,\"死亡病例\":1524}"; var dic = JsonParse.To<Dictionary<string, int>>(json); var dic2 = JsonParse.To<IDictionary<string, int>>(json);
JsonParse提供了一些可以重载的对象序列化/反序列化的静态方法,内部实际是调用JsonSerializer去完成的,更复杂的功能也是需要利用JsonSerializer来实现的,这个不是重点就不去介绍了。
对于JSON的解析主要包含两个功能:序列化和反序列化,序列化是将对象转换为JSON字符串,反序列化是将JSON字符串转换为指定的对象。本项目涉及到的几个核心对象有JsonReader、JsonWriter、 ITypeConverter、IConverterCreator等,下面一一介绍。
1、JsonReader json读取器
JsonReader可以简单的理解为一个json字符串的扫描仪,按照json语法规则进行扫描,每次扫描取出一个JsonTokenType及其对应的值,JsonTokenType枚举定义:
public enum JsonTokenType : byte { None, StartObject, //{ EndObject, //} StartArray, //[ EndArray, //] PropertyName, //{标识后双引号包围的字符串或{内逗号后双引号包围的字符串 解析为PropertyName String, //除PropertyName外双引号包围的字符串 Number, //没有引号包围的数字 True, //true False, //false Null, //null Comment //注释 }
字符串扫描方法 Read() :
public bool Read() { switch (_state) { ; return ReadToken(); case ReadState.StartObject: return ReadProperty(); case ReadState.Property: case ReadState.StartArray: return ReadToken(); case ReadState.EndObject: case ReadState.EndArray: case ReadState.Comma: case ReadState.Value: return ReadNextToken(); case ReadState.End: return ValidateEndToken(); default: throw new JsonException($"非法字符{_currentChar}", _line, _position); } }
从Read方法可以看出JsonReader内部维持了一个ReadState状态机,每次调用根据上一个ReadState来进行下一个token的解析,这样既驱动了内部方法分支跳转,同时又比较容易的对json格式进行校验,例如:遇到 {(StartObject) 下一个有效字符(空白字符除外)只能是 “(PropertyName)或 }(EndObject)之一,所以当ReadState=StartObject时应该去执行ReadProperty()方法,而在ReadProperty()方法里只需要对 ” 和 } 两个字符做正确的响应,出现其他字符都说明这个json文档格式不正确,抛异常就行了,所以ReadProperty()方法的核心代码如下所示:
private bool ReadProperty() { var value = MoveNext(true); switch (value) { case '"': //读取propertyName值 return true; case '}': //readState状态值切换 return true; default: throw new JsonException($"非法字符{value }", _line, _position); } }
....等等其他方法的跳转和格式的校验都是采用类似方法处理的。
token的校验有一个比较麻烦的地方就是容器(JsonObject和JsonArray)嵌套后符号的闭合是否正确,即{与},[与]必须成对出现,比如: [ { } } ]这个错误的json字符串,如果仅仅利用上一个token来验证下一个token是否合法,是无法判断出这个json是不合法的, 这时Stack后进先出的特性就非常适合这个场景了,借助Stack我们可以这样验证这个json:遇到第一个[,进行压栈操作;第二个{,继续压栈;第三个},出栈操作,对出栈的值进行判断与当前值是否能闭合,出栈值是{,刚好与}是成对的,那么第三个字符是合法的,此时栈顶值是[;第四个字符},出栈操作,出栈的值是[,与}无法成对,值非法,验证结束。
JsonReader的核心功能是对json文本的拆解与校验,核心方法就是Read(),调用Read()方法会有3中情况存在:1.返回true,正确读取到一个JsonTokenType且文档未读完 2.返回false,正确读取到一个JsonTokenType且文档已全部读取完毕 3.出现异常,json格式不正确或不满足配置要求。上层的反序列化功能都是依赖JsonReader来完成的,使用JsonReader读完一个json后得到的是一组的JsonTokenType以及对应的值,至于这些tokentype之间所包含的层级关系会由后面的ITypeConverter或JsonToken等对象进行处理。
2、JosnWriter json写入器
JosnWriter和JsonReader的功能则相反,是将数据按照json规范输出为json字符串,序列化功能类最终都是交给JosnWriter来完成的。调用JsonWriter的写入方法每次会写入一个JsonTokenType值,当然写的时候也需要校验值是否合法,校验逻辑与JsonReader的校验差不多,功能相对简单就不去介绍了,有兴趣的同学可以直接看代码,代码地址在文档末尾。
3、(反)序列化接口ITypeConverter
主要类之间的引用关系图:
ITypeConverter接口是整个对象序列化/反序列化过程的核心,ITypeConverter的职责是依托于JsonReader,JsonWriter来实现特定对象类型的(反)序列化,但是光有ITypeConverter还不够,因为是特定对象的(反)序列化器,一个ITypeConverter实现类只能解析一个或一类对象,解析一个对象会用到很多个ITypeConverter,对于外部调用者来说根本不知道什么的时候使用哪个ITypeConverter,这个工作就交给了IConverterCreator工厂来完成,看下IConverterCreator的定义:
public interface IConverterCreator { bool CanConvert(Type type); ITypeConverter Create(Type type); }
使用这个工厂创建ITypeConverter前需要调用CanConvert方法来判断给定的Type是否支持,当返回true时就可以去创建对应的TypeConverter,不然创建出来了也不能正常工作,这样就需要有一堆IConverterCreator的候选项来供调用者查找,然后去遍历这些候选项调用CanConvert方法,当遍历到某个候选项返回true时,就可以创建ITypeConverter开始干活了,基于此抽象了一个TypeConverterProvider类:
public abstract class TypeConverterProvider { public abstract IReadOnlyCollection<IConverterCreator> AllConverterFactories(); public abstract void AddConverterFactory(IConverterCreator converter); public virtual ITypeConverter Build(Type type) { ITypeConverter convert = null; foreach (var creator in AllConverterFactories()) { if (creator.CanConvert(type)) { convert = creator.Create(type); break; } } if (convert == null) throw new JsonException($"创建{type}的{nameof(ITypeConverter)}失败,不支持的类型"); return convert; } }
为了能够扩展使用自定义实现的IConverterCreator,提供了一个AddConverterFactory方法,可以从外部添加自定义的IConverterCreator。Build方法的默认实现就是遍历AllConverterFactories,然后判断是否能创建ITypeConverter,只要符合条件就调用IConverterCreator的Create方法来创建ITypeConverter返回,整个工厂生成器实现闭合,理论上只要AllConverterFactories里面的IConverterCreator足够多或者足够强大,能够转换所有类型的Type,那么这个工厂生成器就可以利用IConverterCreator创建ITypeConverter来实现任意类型的(反)序列化工作了。
4、用ExpressionTree对ITypeConverter的几个实现
4.1 TypeConverterBase
利用表达式树生成委托的功能,然后将委托缓存下来,执行性能可以和静态编写的代码相当。TypeConverterBase提取了一个公共属性Func<object> CreateInstance,目的是为反序列化创建Type的对象是调用,委托的是使用表达式树编译生成:
protected virtual Func<object> BuildCreateInstanceMethod(Type type) { NewExpression newExp; //优先获取无参构造函数 var constructor = type.GetConstructor(Array.Empty<Type>()); if (constructor != null) newExp = Expression.New(type); else { //查找参数最少的一个构造函数 constructor = type.GetConstructors().OrderBy(t => t.GetParameters().Length).FirstOrDefault(); var parameters = constructor.GetParameters(); List<Expression> parametExps = new List<Expression>(); foreach (var para in parameters) { //有参构造函数使用默认值填充 var defaultValue = GetDefaultValue(para.ParameterType); ConstantExpression constant = Expression.Constant(defaultValue); var paraValueExp = Expression.Convert(constant, para.ParameterType); parametExps.Add(paraValueExp); } newExp = Expression.New(constructor, parametExps); } Expression<Func<object>> expression = Expression.Lambda<Func<object>>(newExp); return expression.Compile(); }
这个方法首先判断该类型是否有无参的构造函数,如果有就直接通过Expression.New(type)去构造,没有的话去查找参数最少的一个构造函数来构造,构造带参数构造函数的时候是需要传递这些参数的,默认实现是直接传递当前参数类型的默认值,当然也是可以通过配置等方式来指定参数数据值的。获取一个type默认值的表达式Expression.Default(type),如果类型是int,就相当于default(int),如果类型是string,就相当于default(string)等等。然后使用常量表达式Expression.Constant(defaultValue)转换成Expression,将转换的结果添加到List<Expression>中,再使用构造函数表达式的重载方法newExp= Expression.New(constructor, parametExps),转换成lambad表达式Expression.Lambda<Func<object>>(newExp),就可以调用Compile方法生成委托了。
有了Func<object> CreateInstance这个委托方法,实例化对象就只需要执行委托就行了,也不用反射创建去对象了。
TypeConverterBase的具体实现类大体归为3类,处理JsonObject类型的解析器:ObjectConverter、DictionaryConverter,处理JsonArray类型的解析器:EnumberableConverter(具体实现有ListConverter,ArrayConverter...); 处理Json值类型(JsonString,JsonNumber,JsonBoolean,JsonNull)的解析器:ValueConverter。每个解析器都是针对各自类型特点来完成json(反)序列化的。
4.2 对象解析器 ObjectConverter
为了能使对象中的属性/字段能与JsonObject中的Property进行相互转化,我们定义了2个委托属性:Func<object, object> GetValue,设置属性/字段值Action<object, object> SetValue。参数的定义都是使用object类型的,目的是为了保证方法的通用性。GetValue是获取属性/字段值的委托方法,第一个入参object是当前类的实例对象,返回的object是对应属性/字段的值。看下GetValue委托生成的代码:
protected virtual Func<object, object> BuildGetValueMethod() { var instanceExp = Expression.Parameter(typeof(object), "instance"); var instanceTypeExp = Expression.Convert(instanceExp, MemberInfo.DeclaringType); var memberExp = Expression.PropertyOrField(instanceTypeExp, MemberInfo.Name); var body = Expression.TypeAs(memberExp, typeof(object)); Expression<Func<object, object>> exp = Expression.Lambda<Func<object, object>>(body, instanceExp); return exp.Compile(); }
首先定义好方法的参数var instanceExp = Expression.Parameter(typeof(object), "instance"),入参是object类型的,使用的时候是需要转换成其真实类型的,使用Expression.Convert(instanceExp, MemberInfo.DeclaringType),Expression.Convert是做类型转换的(Expression.TypeAs也可以类型转换,但转换类型如果是值类型会报错,只能用于转换为引用类型),然后再用Expression.PropertyOrField(instanceTypeExp, MemberInfo.Name),传入实例与成员名称就可以获取到成员值了,这个GetValue方法的逻辑就相当于下面的伪代码:
protected object GetValue(object obj) { var instance = (目标类型)obj; var value = instance.目标属性/字段; return (object)value; }
再看看SetValue委托的生成逻辑:
protected virtual Action<object, object> BuildSetValueMethod() { var instanceExp = Expression.Parameter(typeof(object), "instance"); var valueExp = Expression.Parameter(typeof(object), "memberValue"); var instanceTypeExp = Expression.Convert(instanceExp, MemberInfo.DeclaringType); var memberExp = Expression.PropertyOrField(instanceTypeExp, MemberInfo.Name); //成员赋值 var body = Expression.Assign(memberExp, Expression.Convert(valueExp, MemberType)); Expression<Action<object, object>> exp = Expression.Lambda<Action<object, object>>(body, instanceExp, valueExp); return exp.Compile(); }
赋值操作不需要有返回值,第一个参数是实例对象,第二个参数是成员对象,都通过Expression.Parameter方法声明,Expression.PropertyOrField是获取属性/字段的表达式相当于静态代码的instance.属性/字段名 这样的写法,成员赋值表达式:Expression.Assign(memberExp, Expression.Convert(valueExp, MemberType)),成员入参声明的是object,同样需要调用Expression.Convert(valueExp, MemberType) 来转换成真实类型。然后使用Expression.Lambda的Compile方法就可以生成目标委托了。
一个类里会有多个属性/字段,每个属性/字段都需要对应各自的GetValue/SetValue, 我们将GetValue/SetValue委托的生成统一放在了MemberDefinition类中,一个MemberDefinition只负责管理一个成员信息(PropertyInfo或FieldInfo)的读写委托的生成,然后在ObjectConverter里面维护了一个MemberDefinition列表public IEnumerable<MemberDefinition> MemberDefinitions 来映射当前类的多个属性/字段,每次对成员赋值或写值时,只需要找到对应的MemberDefinition,然后调用其GetValue/SetValue委托就可以了。
4.3 字典类型解析器 DictionaryConverter
DictionaryConverter为了处理Dictionary<,>与JsonObject之间互转换的,因为是泛型接口,键与值的类型需要用两个属性来保存
public Type KeyType { get; protected set; } public Type ValueType { get; protected set; }
这两个Type类型的属性是为了赋值/写值时类型转换用的。 与对象成员赋值的方法不一样,字典键值的读写可以通过索引器来完成,字典赋值委托:Action<object, object, object>,第一个参数是字典实例,第二个参数是key的值,第三个参数是value的值,执行这个委托就等于调用这句代码:dic[key]=value; 来看一下表达式生成这个委托的代码:
protected virtual Action<object, object, object> BuildSetKeyValueMethod(Type type) { var objExp = Expression.Parameter(typeof(object), "dic"); var keyParaExp = Expression.Parameter(typeof(object), "key"); var valueParaExp = Expression.Parameter(typeof(object), "value"); var dicExp = Expression.TypeAs(objExp, Type); var keyExp = Expression.Convert(keyParaExp, KeyType); var valueExp = Expression.Convert(valueParaExp, ValueType); //调用索引器赋值 var property = type.GetProperty("Item", new Type[] { KeyType }); var indexExp = Expression.MakeIndex(dicExp, property, new Expression[] { keyExp }); var body = Expression.Assign(indexExp, valueExp); var expression = Expression.Lambda<Action<object, object, object>>(body, objExp, keyParaExp, valueParaExp); return expression.Compile(); }
这个无返回值的委托有3个object类型的入参,都通过Expression.Parameter定义,再分别转换成各自真实的数据类型,然后反射找到索引器对应的PropertyInfo:type.GetProperty("Item", new Type[] { KeyType })(索引器默认属性名为Item),得到索引器Expression.MakeIndex(dicExp, property, new Expression[] { keyExp }),这句话相当于读key的值,对索引器赋值的话还需要用 Expression.Assign(indexExp, valueExp)来完成,这样通过索引器赋值的委托就搞定了。字典根据key获取value值的委托:Func<object, object, object>逻辑与赋值操作基本相同,只需要将索引器拿到的结果返回就完事,代码就不贴了。
4.4 可迭代类型(实现IEnumerable接口的类型)解析器EnumerableConverter
实现了IEnumerable接口的类型与JsonArray之间的互转主要用到了2个功能的委托:Func<object, IEnumerator> GetEnumerator和Action<object, object> AddItem,分别相当于读和写,读是拿到IEnumerable的迭代器GetEnumerator(),然后遍历迭代器;写是对集合添加元素,最终是集合调用自己的”Add“方法,由于不是所有集合添加数据的方法名字都叫Add,所以EnumerableConverter是一个抽象类,只实现了公共逻辑部分,具体实现由具体实现类来完成(比如:ListConverter,ArrayConverter...)。贴上获取迭代器委托的生成代码与集合添加数据委托的生成代码:
protected virtual Func<object, IEnumerator> BuildGetEnumberatorMethod(Type type) { var paramExp = Expression.Parameter(typeof(object), "list"); var listExp = Expression.TypeAs(paramExp, type); var method = type.GetMethod(nameof(IEnumerable.GetEnumerator));//实现了IEnumerable的类一定有GetEnumerator方法 var callExp = Expression.Call(listExp, method); //调用GetEnumerator()方法 var body = Expression.TypeAs(callExp, typeof(IEnumerator)); //结果转换为IEnumerator类型 var expression = Expression.Lambda<Func<object, IEnumerator>>(body, paramExp); return expression.Compile(); }
BuildGetEnumberatorMethod
protected virtual Action<object, object> BuildAddItemMethod(Type type) { var listExp = Expression.Parameter(typeof(object), "list"); var itemExp = Expression.Parameter(typeof(object), "item"); var instanceExp = Expression.Convert(listExp, type); var argumentExp = Expression.Convert(itemExp, ItemType); var addMethod = type.GetMethod(AddMethodName);//添加数据方法AddMethodName有实现的子类去指定,默认为Add var callExp = Expression.Call(instanceExp, addMethod, argumentExp); //调用添加数据方法 Expression<Action<object, object>> addItemExp = Expression.Lambda<Action<object, object>>(callExp, listExp, itemExp); return addItemExp.Compile(); }
BuildAddItemMethod
使用EnumerableConverter序列化对象时只需要调用GetEnumerator委托,拿到迭代器IEnumerator,遍历迭代器将每个item输出到json就可以了。反序列化对象时执行AddItem委托就等于集合调用自己添加数据的方法,从而完成对集合数据的填充。但是数组是不可变的,没有添加元素的方法如何处理呢?这里的处理方法是数组的构造先由List来完成,添加数据就可以用List.Add方法了,到最后统一调用List的ToArray()方法转换成目标数组。所以ArrayConverter是继承自ListConverter的,重写一下父类ListConverter的反序列化方法,在父类处理完后调用list的ToArray方法就完成了。
还有一大堆具体的实现这里也不去介绍了,主要是把表达式树实现这块的东西写出来当作学习笔记,顺便分享一下。
写这个项目主要是为了学习表达式树的运用与json的解析,其中一部分设计思路参考了Newtonsoft.Json源码,受限于本人的水平,加上项目也没有全面的测试,里面一定有不少问题,欢迎大佬们提出指正,希望能与大家共同学习进步。最后希望疫情早日结束,能尽早回到办公室搬砖,还是坐在公司里踏实。
贴上源码地址:https://github.com/zhangmingjian/RapidityJson
用ExpressionTree实现JSON解析器的更多相关文章
- 一起写一个JSON解析器
[本篇博文会介绍JSON解析的原理与实现,并一步一步写出来一个简单但实用的JSON解析器,项目地址:SimpleJSON.希望通过这篇博文,能让我们以后与JSON打交道时更加得心应手.由于个人水平有限 ...
- 这个东西,写C++插件的可以用到。 RapidJSON —— C++ 快速 JSON 解析器和生成器
点这里 原文: RapidJSON —— C++ 快速 JSON 解析器和生成器 时间 2015-04-05 07:33:33 开源中国新闻原文 http://www.oschina.net/p/ ...
- 如何编写一个JSON解析器
编写一个JSON解析器实际上就是一个函数,它的输入是一个表示JSON的字符串,输出是结构化的对应到语言本身的数据结构. 和XML相比,JSON本身结构非常简单,并且仅有几种数据类型,以Java为例,对 ...
- 自己动手实现一个简单的JSON解析器
1. 背景 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着诸多优点.比如易读性更好,占用空间更少等.在 ...
- 一个简单的json解析器
实现一个简单地json解析器. 两部分组成,词法分析.语法分析 词法分析 package com.mahuan.json; import java.util.LinkedList; import ja ...
- 高性能JSON解析器及生成器RapidJSON
RapidJSON是腾讯公司开源的一个C++的高性能的JSON解析器及生成器,同时支持SAX/DOM风格的API. 直击现场 RapidJSON是腾讯公司开源的一个C++的高性能的JSON解析器及生成 ...
- spring boot2 修改默认json解析器Jackson为fastjson
0.前言 fastjson是阿里出的,尽管近年fasjson爆出过几次严重漏洞,但是平心而论,fastjson的性能的确很有优势,尤其是大数据量时的性能优势,所以fastjson依然是我们的首选:sp ...
- .Net Core 3.0原生Json解析器
微软官方博客中描述了为什么构造了全新的Json解析器而不是继续使用行业准则Json.Net 微软博客地址:https://devblogs.microsoft.com/dotnet/try-the-n ...
- 高手教您编写简单的JSON解析器
编写JSON解析器是熟悉解析技术的最简单方法之一.格式非常简单.它是递归定义的,所以与解析Brainfuck相比,你会遇到轻微的挑战 ; 你可能已经使用JSON.除了最后一点之外,解析 Scheme的 ...
随机推荐
- 28.python操作excel表格(xlrd/xlwt)
python读excel——xlrd 这个过程有几个比较麻烦的问题,比如读取日期.读合并单元格内容.下面先看看基本的操作: 首先读一个excel文件,有两个sheet,测试用第二个sheet,shee ...
- Fastadmin 如何引入 layui 模块
FastAdmin基于RequireJS进行前端JS模块的管理,因此如果我们需要再引入第三方JS插件,则必按照RequireJS的规则进行载入.如果你还不了解什么是RequireJS,可以先简单了解下 ...
- Linux网络管理之多网卡绑定
一.bonding介绍 在企业Linux服务器管理里中,服务器的可靠性.可用性以及I/O速度都非常重要,保持服务器的高可用和安全性是生产环境的重要指标,其中最重要的一点是服务器网络连接的高可用性.通常 ...
- 升级添加到现有iOS Xcode项目的Flutter
如果你在2019年8月之前将Flutter添加到现有iOS项目,本文值得你一看. 在2019年7月30日,合并合并请求flutter / flutter#36793之前Flutter 1.8.4-pr ...
- 简简单单之Linux命令入门
show me the code and talk to me,做的出来更要说的明白 GitHub 项目JavaHouse同步收录 我是布尔bl,你的支持是我分享的动力! 引入 作为一名合格的后端开发 ...
- 一个命令解决linux重启nginx就丢失pid文件问题
sudo nginx -c /etc/nginx/nginx.conf
- 18个Java8日期处理的实践,对于程序员太有用了!
18个Java8日期处理的实践,对于程序员太有用了! Java 8 推出了全新的日期时间API,在教程中我们将通过一些简单的实例来学习如何使用新API. Java处理日期.日历和时间的方式一直为社区所 ...
- 去除Linux中的^M
(1)安装tofrodos sudo apt-get install tofrodos (2)做一些优化 ln -s /usr/bin/todos /usr/bin/unix2dos ln -s /u ...
- Django HttpResponse、render、redirect
一.HttpResponse 作业:返回相应的内容 格式: return HttpResponse("Hello, World") 二.render 作业:提交网页和字符串替换 提 ...
- Java入门 - 语言基础 - 16.数组
原文地址:http://www.work100.net/training/java-array.html 更多教程:光束云 - 免费课程 数组 序号 文内章节 视频 1 概述 2 声明数组变量 3 创 ...