相信很多童鞋们都被问到过这个问题,不管是在面试的时候被问过,还是笔试题里考过,甚至有些童鞋们找我要学习资料的时候我也考过这个问题,包括博主我自己,也曾被问过,而且博主现在有时作为公司的面试官,也喜欢问应试者这样的问题。

这确实是一道基础题,不管是在java里面还是在C#里面,都属于一道非常基础的题,但很多童鞋竟然说没遇到过这样的场景,或者说学习基础的时候没重视过,所以自然不会了,那行,今天博主在这里就针对这个问题,做出详细的解释并由这个问题,同时,也给大家做个扩展学习吧。

在实际的项目当中,这是个非常非常需要的,我们经常需要在不同用途的实体之间相互转换进行数据传输,最典型的当然就是Entity和DTO之间的相互转换咯(不懂什么是DTO的自己去科普下,或者在这儿你看成是ViewModel也行,这无所谓)。当然,开这篇文章肯定不是只讲简简单单反射这么简单,那我写这篇文章可能有点大动干戈了,所以,你们赚了,除了讲反射,自然也就引出了各种创建对象的效率问题,以及除了反射还有哪些方式可以转换实体。

首先说一下,我听到的最多的答案,就是:

1. new一个目标实体出来,然后把对应字段的值赋值上就好了

2. 通过反射获取对象的属性,然后挨个循环属性,把对应属性名赋值到另一个对象里面

3. 使用AutoMapper完成不同实体的转换。

上面这几个答案都是对的,只是答案1的童鞋钻漏子勉强回答对了,而答案2的通用性比较高了,答案3的就是拿来主义,有现成的直接用就好了,底层怎么实现的不知道…。可能有些童鞋以前答不上来的时候看到这些答案就恍然大悟了,而有的童鞋可能还是有点懵,这样的话确实应该回头再学一遍基础了。好吧,来点实际的吧。

那就从上面三个答案开始吧。

先准备两个实体吧:

public class Person

{

public string Name { get; set; }

public int Age { get; set; }

}

public class PersonDto

{

public string Name { get; set; }

public int Age { get; set; }

}

 

1. 硬编码实现实体的相互转换

最简单粗暴的方法也就是这样的咯,但如果面试这样回答,基本上不会加分。

Person p = new Person()

{

Age = 10,

Name = "张三"

};

PersonDto pd = new PersonDto()

{

Name = p.Name,

Age = p.Age

};

这样做,也不是不可以,毕竟效率是最高的,但是,通用性不强,如果要转换其他实体,每个都必须重新手写,不可复用,如果一个实体属性达到了几十上百个呢?这样写你不嫌手累嘛,看上去也不优雅是不。那下面这个,看上去稍微优雅点的:

 

2. 委托实现实体的相互转换

相比之上,稍微优雅了一点点,但,还是硬编码,面试这样回答的可能会加分;

Person p = new Person()

{

Age = 10,

Name = "张三"

};

Func<Person, PersonDto> func = c => new PersonDto()

{

Age = c.Age,

Name = c.Name

};

PersonDto pd = func(p);

这里直接写了个Func类型的委托,在委托里面硬编码给DTO实体的字段挨个赋值,看上去稍微优雅点了,毕竟用上了委托,哈哈……

那好,又优雅又不是硬编码的来了。

 

3. 使用AutoMapper完成不同实体的转换

当然,这拿来主义在实际项目中确实也用得非常多,包括博主我自己,在项目中也是用AutoMapper居多,毕竟人家实体转换效率还是蛮高的,面试中可能会加分。

Person p = new Person() { Age = 10, Name = "张三" };

Mapper.Initialize(m => m.CreateMap<Person, PersonDto>());

PersonDto pd = Mapper.Map<PersonDto>(p);

这代码,看上去确实很简单就完成了,拿来主义嘛,那要是就不准让你用AutoMapper,又要达到它这样的高性能,你咋办?!那好,后面有终极武器,比AutoMapper的转换效率更高效,所以看到这篇文章的童鞋们,你们赚大了!

 

4. 通过反射实现实体的相互转换

可能这样回答的人基础都是比较好的了,面试都会加分的。

首先准备一个方法,专门对实体进行映射:

/// <summary>

/// 实体映射

/// </summary>

/// <typeparam name="TDestination">目标实体</typeparam>

/// <param name="source">源实体</param>

/// <returns></returns>

public static TDestination MapTo<TDestination>(object source) where TDestination : new()

{

TDestination dest = new TDestination();//创建目标实体对象

foreach (var p in dest.GetType().GetProperties())//获取源实体所有的属性

{

p.SetValue(dest, source.GetType().GetProperty(p.Name)?.GetValue(source));//挨个为目标实体对应字段名进行赋值

}

return dest;

}

然后在需要进行实体转换的地方直接调这个方法就OK了;

Person p = new Person() { Age = 10, Name = "张三" };

PersonDto pd = MapTo<PersonDto>(p);

很好,这很优雅,但是,博主我觉得,还不够优雅,毕竟,可能好多人没看懂?

那好,来个更简单更直接的办法:反序列化进行实体转换;

 

5. 通过Json反序列化实现实体的相互转换

到这儿为止,很多童鞋就想不到了,实体转换还能通过json来进行;确实可以的,这也方便,而且还自带支持多成实体的映射转换,比如实体里面还包含List这样的数据结构;一句话就能搞定实体映射,不信你看:

首先通过nuget把Newtonsoft.Json引进来,然后coding...

Person p = new Person() { Age = 10, Name = "张三" };

PersonDto pd = JsonConvert.DeserializeObject<PersonDto>(JsonConvert.SerializeObject(p));

卧槽,居然这就搞定了,还能这么优雅?!那你试试吧。

上面这几种方式都是经常用到的,但是,当你进行超大批量的实体转换时,你会发现,反射和反序列化的方式出现瓶颈了,那有没有办法来优化呢,肯定有啊,不能优化的话那怎么去支撑千万级数据?!那你想到的是什么?表达式树?emit?

既然反射效率那么低,那是不是就不能用反射了?还是离不开反射,前方带你弯道超车,还请各位站稳扶好!

如果你在面试当中用到了下面这几种方式,那么,可能面试官都会膜拜你了,最后的可能,就是你选公司而不是公司选你了,哈哈…,当然,可能有点夸大其词,毕竟面试你的也不是什么技术菜鸟,至少会给你的面试大大加分!

 

6. 表达式树实现实体的相互转换

来吧,拼接表达式,走起!

Person p = new Person() { Age = 10, Name = "张三" };

ParameterExpression parameterExpression = Expression.Parameter(typeof(Person), "p");

List<MemberBinding> memberBindingList = new List<MemberBinding>();//表示绑定的类派生自的基类,这些绑定用于对新创建对象的成员进行初始化(vs的注解。太生涩了,我这样的小白解释不了,大家将就着看)

foreach (var item in typeof(PersonDto).GetProperties()) //遍历目标类型的所有属性

{

MemberExpression property = Expression.Property(parameterExpression, typeof(Person).GetProperty(item.Name));//获取到对应的属性

MemberBinding memberBinding = Expression.Bind(item, property);//初始化这个属性

memberBindingList.Add(memberBinding);

}

foreach (var item in typeof(PersonDto).GetFields())//遍历目标类型的所有字段

{

MemberExpression property = Expression.Field(parameterExpression, typeof(Person).GetField(item.Name));//获取到对应的字段

MemberBinding memberBinding = Expression.Bind(item, property);//同上

memberBindingList.Add(memberBinding);

}

MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(PersonDto)), memberBindingList.ToArray());//初始化创建新对象

Expression<Func<Person, PersonDto>> lambda = Expression.Lambda<Func<Person, PersonDto>>(memberInitExpression, parameterExpression);

PersonDto pd = lambda.Compile()(p);

卧槽,看起来好复杂,不明觉厉啊!那,自己慢慢摸索下吧,都说了要站稳扶好的,这就不怪老司机了哈!

结果如上图所示,但是问题又来了,我们不可能只有一个类,也不可能只有一个Dto,那我们应该怎么实现呢? 对 ,可以用泛型来实现。

但是,博主我觉得还不够,每次转换还得写这么一大坨看不懂的东西,封装一下岂不更好用?那好,开始封装吧;

 

7. 表达式树的封装实现通用实体的相互转换

public static TDestination ExpressionTreeMapper<TSource, TDestination>(TSource source)

{

ParameterExpression parameterExpression = Expression.Parameter(typeof(TSource), "p");

List<MemberBinding> memberBindingList = new List<MemberBinding>();//表示绑定的类派生自的基类,这些绑定用于对新创建对象的成员进行初始化(vs的注解。太生涩了,我这样的小白解释不了,大家将就着看)

foreach (var item in typeof(TDestination).GetProperties()) //遍历目标类型的所有属性

{

MemberExpression property = Expression.Property(parameterExpression, typeof(TSource).GetProperty(item.Name));//获取到对应的属性

MemberBinding memberBinding = Expression.Bind(item, property);//初始化这个属性

memberBindingList.Add(memberBinding);

}

foreach (var item in typeof(TDestination).GetFields())//遍历目标类型的所有字段

{

MemberExpression property = Expression.Field(parameterExpression, typeof(TSource).GetField(item.Name));//获取到对应的字段

MemberBinding memberBinding = Expression.Bind(item, property);//同上

memberBindingList.Add(memberBinding);

}

MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TDestination)), memberBindingList.ToArray());//初始化创建新对象

Expression<Func<TSource, TDestination>> lambda = Expression.Lambda<Func<TSource, TDestination>>(memberInitExpression, parameterExpression);

return lambda.Compile()(source); //拼装是一次性的

}

封装之后,调用倒是方便了,但我觉得没区别,封装之后每次调用这个方法不也得走一遍编译表达式树的过程,这个过程岂不多余了?这个过程性能瓶颈很大。

那好,能不能把编译之后的表达式树缓存起来?答案是肯定可以的,继续优化,走起;

 

8. 表达式树缓存实现通用实体的相互转换

实现思路:把每次编译后的表达式树缓存起来,如果存在,直接拿现成的编译好的表达式树调用就好了

/// <summary>

/// 生成表达式目录树。字典缓存

/// </summary>

public class ExpressionMapper

{

private static Dictionary<string, object> _dic = new Dictionary<string, object>();//缓存字典,缓存后的就是硬编码所以性能高。

/// <summary>

/// 字典缓存表达式树

/// </summary>

/// <typeparam name="TSource"></typeparam>

/// <typeparam name="TDestination"></typeparam>

/// <param name="source"></param>

/// <returns></returns>

public static TDestination Map<TSource, TDestination>(TSource source)

{

string key = $"funckey_{typeof(TSource).FullName}_{typeof(TDestination).FullName}";

if (!_dic.ContainsKey(key)) //如果该表达式不存在,则走一遍编译过程

{

ParameterExpression parameterExpression = Expression.Parameter(typeof(TSource), "p");

List<MemberBinding> memberBindingList = new List<MemberBinding>();//表示绑定的类派生自的基类,这些绑定用于对新创建对象的成员进行初始化(vs的注解。太生涩了,我这样的小白解释不了,大家将就着看)

foreach (var item in typeof(TDestination).GetProperties()) //遍历目标类型的所有属性

{

MemberExpression property = Expression.Property(parameterExpression, typeof(TSource).GetProperty(item.Name));//获取到对应的属性

MemberBinding memberBinding = Expression.Bind(item, property);//初始化这个属性

memberBindingList.Add(memberBinding);

}

foreach (var item in typeof(TDestination).GetFields())//遍历目标类型的所有字段

{

MemberExpression property = Expression.Field(parameterExpression, typeof(TSource).GetField(item.Name));//获取到对应的字段

MemberBinding memberBinding = Expression.Bind(item, property);//同上

memberBindingList.Add(memberBinding);

}

MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TDestination)), memberBindingList.ToArray());//初始化创建新对象

Expression<Func<TSource, TDestination>> lambda = Expression.Lambda<Func<TSource, TDestination>>(memberInitExpression, parameterExpression);

_dic[key] = lambda.Compile(); //拼装是一次性的

}

return ((Func<TSource, TDestination>)_dic[key]).Invoke(source);

}

}

恩,这样缓存起来,再执行重复类型的转换,就直接取缓存好的表达式树进行调用了,性能貌似提升了,非常棒!但博主我认为,这还有优化的余地,既然有了缓存,那我干脆直接把泛型搬到整个类上面,进行缓存起来,好的,走起;

 

9. 表达式树泛型缓存实现通用实体的相互转换

/// <summary>

/// 生成表达式目录树  泛型缓存

/// </summary>

public class ExpressionGenericMapper

{

private static object func;

public static TDestination Map<TSource, TDestination>(TSource source)

{

if (func is null)//如果表达式不存在,则走一遍编译过程

{

ParameterExpression parameterExpression = Expression.Parameter(typeof(TSource), "p");

var memberBindingList = new List<MemberBinding>();//表示绑定的类派生自的基类,这些绑定用于对新创建对象的成员进行初始化(vs的注解。太生涩了,我这样的小白解释不了,大家将就着看)

foreach (var item in typeof(TDestination).GetProperties()) //遍历目标类型的所有属性

{

MemberExpression property = Expression.Property(parameterExpression, typeof(TSource).GetProperty(item.Name));//获取到对应的属性

MemberBinding memberBinding = Expression.Bind(item, property);//初始化这个属性

memberBindingList.Add(memberBinding);

}

foreach (var item in typeof(TDestination).GetFields())

{

MemberExpression property = Expression.Field(parameterExpression, typeof(TSource).GetField(item.Name));//获取到对应的字段

MemberBinding memberBinding = Expression.Bind(item, property);//同上

memberBindingList.Add(memberBinding);

}

MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TDestination)), memberBindingList.ToArray());//初始化创建新对象

Expression<Func<TSource, TDestination>> lambda = Expression.Lambda<Func<TSource, TDestination>>(memberInitExpression, parameterExpression);

func = lambda.Compile();

}

return ((Func<TSource, TDestination>)func)(source); //拼装是一次性的

}

}

 

10. 最后,来个性能测试吧

我们挨个用这些方法,都循环映射1,000,000次,看谁跑得最快!

单线程测试代码:

static void Main(string[] args)

{

Person p = new Person() { Age = 10, Name = "张三" };

Stopwatch sw = new Stopwatch();

sw.Start();

for (int i = 0; i < 1_000_000; i++)

{

PersonDto pd = new PersonDto() { Name = p.Name, Age = p.Age };

}

sw.Stop();

Console.WriteLine($"使用硬编码映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

Func<Person, PersonDto> func = c => new PersonDto()

{

Age = c.Age,

Name = c.Name

};

sw.Restart();

for (int i = 0; i < 1_000_000; i++)

{

PersonDto pd = func(p);

}

sw.Stop();

Console.WriteLine($"使用委托映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

sw.Restart();

Mapper.Initialize(m => m.CreateMap<Person, PersonDto>());

for (int i = 0; i < 1_000_000; i++)

{

PersonDto pd = Mapper.Map<PersonDto>(p);

}

sw.Stop();

Console.WriteLine($"使用AutoMapper映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

sw.Restart();

for (int i = 0; i < 1_000_000; i++)

{

PersonDto pd = MapTo<PersonDto>(p);

}

sw.Stop();

Console.WriteLine($"使用反射映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

sw.Restart();

for (int i = 0; i < 1_000_000; i++)

{

PersonDto pd = JsonConvert.DeserializeObject<PersonDto>(JsonConvert.SerializeObject(p));

}

sw.Stop();

Console.WriteLine($"使用Json反序列化映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

sw.Restart();

for (int i = 0; i < 1_000_000; i++)

{

PersonDto pd = ExpressionTreeMapper<Person, PersonDto>(p);

}

sw.Stop();

Console.WriteLine($"使用编译表达式树映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

sw.Restart();

for (int i = 0; i < 1_000_000; i++)

{

PersonDto pd = ExpressionMapper.Trans<Person,PersonDto>(p);

}

sw.Stop();

Console.WriteLine($"使用缓存编译表达式树映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

sw.Restart();

for (int i = 0; i < 1_000_000; i++)

{

PersonDto pd = ExpressionGenericMapper.Map<Person,PersonDto>(p);

}

sw.Stop();

Console.WriteLine($"使用泛型缓存编译表达式树映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

sw.Restart();

Console.ReadKey();

}

测试结果:

并行计算测试代码:

static void Main(string[] args)

{

Person p = new Person() { Age = 10, Name = "张三" };

Stopwatch sw = new Stopwatch();

sw.Start();

Parallel.For(0, 1_000_000, (l, state) =>

{

PersonDto pd = new PersonDto() { Name = p.Name, Age = p.Age };

});

sw.Stop();

Console.WriteLine($"使用硬编码映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

Func<Person, PersonDto> func = c => new PersonDto()

{

Age = c.Age,

Name = c.Name

};

sw.Restart();

Parallel.For(0, 1_000_000, (l, state) =>

{

PersonDto pd = func(p);

});

sw.Stop();

Console.WriteLine($"使用委托映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

sw.Restart();

Mapper.Initialize(m => m.CreateMap<Person, PersonDto>());

Parallel.For(0, 1_000_000, (l, state) =>

{

PersonDto pd = Mapper.Map<PersonDto>(p);

});

sw.Stop();

Console.WriteLine($"使用AutoMapper映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

sw.Restart();

Parallel.For(0, 1_000_000, (l, state) =>

{

PersonDto pd = MapTo<PersonDto>(p);

});

sw.Stop();

Console.WriteLine($"使用反射映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

sw.Restart();

Parallel.For(0, 1_000_000, (l, state) =>

{

PersonDto pd = JsonConvert.DeserializeObject<PersonDto>(JsonConvert.SerializeObject(p));

});

sw.Stop();

Console.WriteLine($"使用Json反序列化映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

sw.Restart();

Parallel.For(0, 1_000_000, (l, state) =>

{

PersonDto pd = ExpressionTreeMapper<Person, PersonDto>(p);

});

sw.Stop();

Console.WriteLine($"使用编译表达式树映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

sw.Restart();

Parallel.For(0, 1_000_000, (l, state) =>

{

PersonDto pd = ExpressionMapper.Trans<Person, PersonDto>(p);

});

sw.Stop();

Console.WriteLine($"使用缓存编译表达式树映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

sw.Restart();

Parallel.For(0, 1_000_000, (l, state) =>

{

PersonDto pd = ExpressionGenericMapper.Map<Person, PersonDto>(p);

});

sw.Stop();

Console.WriteLine($"使用泛型缓存编译表达式树映射1000000次耗时{sw.Elapsed.TotalMilliseconds}ms");

sw.Restart();

Console.ReadKey();

}

测试结果:

很显然,不管是串行代码还是并行代码,硬编码效率肯定是最高的,至于为什么直接new的硬编码在并行的时候似乎慢了点,这就需要你们从多线程和内存的角度考虑下了,Newtonsoft.Json虽然是高性能的序列化插件,但是使用不当也会造成性能瓶颈的,你们如果用JavascriptSerializer序列化,估计看出来的差距更大了,所以一开始我也就选择Newtonsoft.Json来做本次的评测,也能看出来,表达式树不缓存的时候也是非常耗性能的,毕竟每次都要走一遍编译过程,所以就慢了,而一旦加上了缓存机制,就得到了质的飞跃,而最后的表达式树泛型缓存的写法,性能最接近硬编码的写法,所以,优化无止境!

至于为什么AutoMapper的性能还没有表达式树高,有兴趣的童鞋可以去github研究下它的源码,它的底层实现其实是用的emit写法,这篇文章就不准备再讲emit了,估计这个更容易把你们给搞晕,毕竟涉及到很多中间语言代码(MSIL),所以,有兴趣的你,可以作为扩展研究。

经典面试题之——如何自由转换两个没有继承关系的字段及类型相同的实体模型,AutoMapper?的更多相关文章

  1. Oracle中国移动经典面试题(附代码跟两种答案)

    /*中国移动sql面试题: create table test(   id number(10) primary key,   type number(10) ,   t_id number(10), ...

  2. Python 经典面试题汇总之基础篇

    基础篇 1:为什么学习Python 公司建议使用Python,然后自己通过百度和向有学过Python的同学了解了Python.Python这门语言,入门比较简单,它简单易学,生态圈比较强大,涉及的地方 ...

  3. 李洪强iOS经典面试题34-求两个链表表示的数的和

    李洪强iOS经典面试题34-求两个链表表示的数的和 问题 给你两个链表,分别表示两个非负的整数.每个链表的节点表示一个整数位. 为了方便计算,整数的低位在链表头,例如:123 在链表中的表示方式是: ...

  4. 经典.net试题

    经典.net面试题目 1. 简述 private. protected. public. internal 修饰符的访问权限. 答 . private :   私有成员, 在类的内部才可以访问. pr ...

  5. pyntho经典面试题

    Python基础篇 1:为什么学习Python 2:通过什么途径学习Python 3:谈谈对Python和其他语言的区别 Python的优势: 4:简述解释型和编译型编程语言 5:Python的解释器 ...

  6. 【转】.net 经典面试题

    [转].net 经典面试题  1. 简述 private. protected. public. internal 修饰符的访问权限. 答 . private : 私有成员, 在类的内部才可以访问.  ...

  7. 李洪强iOS经典面试题156 - Runtime详解(面试必备)

    李洪强iOS经典面试题156 - Runtime详解(面试必备)   一.runtime简介 RunTime简称运行时.OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制. 对于C ...

  8. 李洪强经典面试题152-Runtime

    李洪强经典面试题152-Runtime   Runtime Runtime是什么 Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平时编写的 OC 代码, ...

  9. 李洪强iOS经典面试题147-WebView与JS交互

    李洪强iOS经典面试题147-WebView与JS交互   WebView与JS交互 iOS中调用HTML 1. 加载网页 NSURL *url = [[NSBundle mainBundle] UR ...

随机推荐

  1. ssm批量删除

    ssm批量删除 批量删除:顾名思义就是一次性删除多个.删除是根据前台传给后台的id,那么所谓批量删除,就是将多个id传给后台,那么如何传过去呢,前后台的交互该如何实现? 1.jsp页面,先选中所有的要 ...

  2. hive中的日期转换函数

    1.unix时间戳转时间函数   语法: from_unixtime(bigintunixtime[, string format]) 返回值: string   说明: 转化UNIX时间戳(从197 ...

  3. 代码的结合性:继承 扩展 组合 变换--swift暗含的四根主线

    类型继承: 类型扩展: 类型组合: 类型变换:

  4. int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restric mutex)

    mutex:为了保护条件变量而存在的: cond:为了线程通信而存在的. 整个机制都是为了保护条件变量和线程间通信而存在的. pthread_cond_wait()函数一进入wait状态就会自动rel ...

  5. 事物分析、静态分析(结构分析)与UML

    事物分析: 1)要素分析: 2)结构(组织.关系)分析: 符合软件中的数据库观点和UML观点: 符合数据结构的观点. 符合由点到面的观点. 将关系和元素提到了同等重要的地位. 符合哲学中普遍联系的观点 ...

  6. VUE 基础配置

    原文:https://www.cnblogs.com/LearningOnline/p/9368838.html 1.安装Node.js等软件 报错: 解决: 原文:https://pdf-lib.o ...

  7. GitHub上如何创建文件夹

    看了网上很多关于如何在git上创建空文件夹的文章后,发现大家写的都是用指令在本地创建一个空文件夹后再上传指令和步骤都太繁琐且复杂了,对于用git不是很熟练得到人来说太麻烦了,而且在本地于github上 ...

  8. JVM和ClassLoader

    JVM和ClassLoader 2019-11-08 目录 1 JVM架构整体架构 1.1 类加载器子系统 1.1.1 加载 1.1.2 链接 1.1.3 初始化 1.2 运行时数据区(Runtime ...

  9. mac 搭建Java Spring boot 环境(idea)

    首先安装插件 安装下面的这个插件 然后重启idea,新建工程 选择新建 Springboot 框架 改写项目名称 选择类型 设置工程名称 删除多余的文件 编译工程 然后运行 1. 2. 参考: htt ...

  10. Mongoose 预定义模式修饰符 Getters 与 Setters 自定义修饰符

    mongoose 预定义模式修饰符 mongoose 提供的预定义模式修饰符,可以对我们增加的数据进行一些格式化,主要有:lowercase.uppercase .trim,这里不一一演示,对trim ...