写给新手的WebAPI实践

此篇是写给新手的Demo,用于参考和借鉴,用于发散思路。老鸟可以忽略了。

自己经常有这种情况,遇到一个新东西或难题,在了解和解决之前总是说“等搞定了一定要写篇文章记录下来”,但是当掌握了之后,就感觉好简单呀不值得写下来了。其实这篇也一样,决定写下来是想在春节前最后再干一件正经事儿,不能天天回去打Dota了!

目录:

  1. 请求响应的设计

  2. 请求的Content-Type和模型绑定

  3. 自定义ApiResult和ApiControllerBase

  4. 权限验证

  5. 模型生成

  6. 文档生成

 

一、请求响应的设计


 RESTFul风格响亮很久了,但是我没用过,以后也不打算用。当系统稍微复杂时,为了符合RESTFul要吃力地创建一些不直观的名词,这不是我的风格。所以此文设计的不是RESTFul风格,是只最常用的POST和GET请求。

请求部分就是调用API的参数,抽象出一个接口如下:

    public interface IRequest{ResultObject Validate();}

这里面只定义了一个Validate()方法,用于验证请求参数的有效性,返回值是响应里的东西,下面会讲到。

对于请求对象,传递到业务逻辑层,甚至是数据访问层都可以,因为它本身就是用来传输数据的,俗话叫DTO(Data Transfer Object),不过定义多层传输对象,然后复制来复制去也是可以的~。但是有时候业务处理会需要当前登录人的信息,而这个信息我并不希望直接从接口层向下传递,所以这里我再抽象一个UserRequestBase,用于附加登录人相关信息:

    public abstract class UserRequestBase : IRequest{        public int ApiUserID { get; set; }      
public string ApiUserName { get; set; }
// ......可以添加其他要专递的登录用户相关的信息public abstract ResultObject Validate();}

ApiUserID和ApiUserName这样的字段是不需要客户端传递的,我们会根据登录人信息自动填充。

根据实际中的经验,我们往往会做分页查询,会用到页码和每页条数,所为我们再定义个PageRequestBase:

    public abstract class PageRequestBase : UserRequestBase{        public int PageIndex { get; set; }     
public int PageSize { get; set; }}

因为.net只能继承单个父类,而且有些分页查询可能需要用户信息,所以我们选择继承UserRequestBase。

当然,还可以根据自己的实际情况抽象出更多的公用类,在这不一一枚举。

 

响应的设计分为两部分,第一个是实际响应部分,第二个会把响应包装一下,加上code和msg,用于表示调用状态和错误信息(好老的方法了,大家都懂)。

响应接口IResponse里什么也没有,就是一个标记接口,不过我们也可以抽象出来两个常用的公用类用于响应列表和分页数据:

    public class ListResponseBase<T> : IResponse{        public List<T> List { get; set; }}    public class PageResponseBase<T>: ListResponseBase<T>{        /// <summary>/// 页码数        /// </summary>public int PageIndex { get; set; }        /// <summary>/// 总条数        /// </summary>public long TotalCount { get; set; }        /// <summary>/// 每页条数        /// </summary>public int PageSize { get; set; }        /// <summary>/// 总页数        /// </summary>public long PageCount { get; set; }}

 包装响应的时候,有两种情况,第一种是操作类接口,比如添加商品,这些接口是不用响应对象的,只要返回是否成功就行了,第二种查询类,这个时候必须要返回一些具体的东西了,所以响应包装设计成两个类:

public class ResultObject{        /// <summary>/// 等于0表示成功        /// </summary>public int Code { get; set; }        /// <summary>/// code不为0时,返回错误消息        /// </summary>public string Msg { get; set; }} 

public class ResultObject<TResponse> : ResultObject where TResponse : IResponse{ public ResultObject(){} public ResultObject(TResponse data){Data = data;} /// <summary>/// 返回的数据        /// </summary>public TResponse Data { get; set; }}

IRequest接口的Validate()方法返回值就是第一个ResultObject,当请求参数验证不通过的时候,肯定是没有数据返回了。

再业务逻辑层,我选择以包装类作为返回类型,因为有很多错误都会在业务逻辑层出现,我们的接口是需要这些错误信息的。

 

二、请求的Content-Type和模型绑定


 现在前后端分离大行其道,我们做后端的通常会返回JSON格式给前端,响应的Content-Type为application/json,前端通过一些框架可以直接作为js对象使用。但是前端请求后端的时候还有很多是以form表单形式,也就是请求的Content-Type为:application/x-www-form-urlencoded,请求体为id=23&name=loogn这样的字符串,如果数据格式复杂了,前端不好传,后端解析起来也麻烦。还有的直接用一个固定参数传递json字符串,比如json={id:23,name:'loogn'},后端用form[‘json’]取出来后再反序列化。这些方法都可以,但是不够好,最好的方法是前端也直接传json,幸好现在很多web服务器都是支持请求的Content-Type为application/json的,这个时候请求的参数会以有效负荷(Payload)的形式传递过去,比如用jQuery的ajax来请求:

    $.ajax({type: "POST",url: "/product/editProduct",       
contentType: "application/json; charset=utf-8",data: JSON.stringify({id:1,name:"name1"}),success: function (result) {console.log(result);}})

 除了contentType,还要注意使用了JSON.stringify把对象转换成了字符串。其实ajax使用的XmlHttpRequest对象只能处理字符串(json字符串呀,xml字符串呀,text纯文本呀,base64呀)。这些数据到了后端之后,从请求流里读出来就是json形式的字符串了,可直接反序列化成后端对象。

然而这些考虑,.net mvc框架已经帮我们做好了,这都要归功于DefaultModelBinder。

关于Form表单形式的请求,可以参见这位园友的文章:你从未知道如此强大的ASP.NET MVC DefaultModelBinder

我这里想说的是,DefaultModelBinder足够智能,并不需要我们自己做什么,它会根据请求的contentType的不同,用不同的方式解析请求,然后绑定到对象,遇到contentType为application/json是,就直接反序列化得到对象,遇到application/x-www-form-urlencoded就用form表单的形式绑定对象,唯一要注意的就是前端同学,不要把请求的contentType和请求的实际内容搞错就行了。你告诉我你送过来一只猫,而实际上是一只狗,我以对待猫的方式对待狗当然就有被咬一口的危险了(肯定会报错)。

 

三、自定义ApiResult和ApiControllerBase


因为我不需要RESTFul风格,也不需要根据客户端的意愿返回json或xml,所以我选择AsyncController作为控制器的基类。AsyncController是直接继承Controller的,而且支持异步处理,具体Controller和ApiController的区别,想了解的同学可以看这篇文章difference-between-apicontroller-and-controller-in-asp-net-mvc ,或者直接阅读源码。

Controller里的Action需要返回一个ActionResult对象,结合上面的响应包装对象ResultObject,我决定自定义一个ApiResult作为Action的返回值,同时在这里处理jsonp调用、跨域调用、序列化的小驼峰命名和时间格式问题。

里面都是一些常规的逻辑,不做说明了,其中的JsonSetting就是设置序列化的小驼峰和日期格式的:

    public class JsonSetting{public static JsonSerializerSettings Settings = new JsonSerializerSettings{ContractResolver = new CamelCasePropertyNamesContractResolver(),DateFormatString = "yyyy-MM-dd HH:mm:ss",};}

这个时候有个问题,如果一个时间的字段需要"yyyy-MM-dd"这种格式怎么办呢?这个时候要定义一个JsonConverter的子类,来实现自定义日期格式:

    /// <summary>/// 日期格式化器/// </summary>public class CustomDateConverter : DateTimeConverterBase{private IsoDateTimeConverter dtConverter = new IsoDateTimeConverter { };public CustomDateConverter(string format){dtConverter.DateTimeFormat = format;}public CustomDateConverter() : this("yyyy-MM-dd") { }public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer){          
return dtConverter.ReadJson(reader, objectType, existingValue, serializer);}public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer){dtConverter.WriteJson(writer, value, serializer);}}

在需要的响应属性上加上 [JsonConverter(typeof(CustomDateConverter))] 或  [JsonConverter(typeof(CustomDateConverter),"yyyy年MM月dd日")] 即可。

ApiResult定义好了,再定义一个控制器基类,目的是便于处理ApiResult:

    /// <summary>/// API控制器基类/// </summary>public class ApiControllerBase : AsyncController{public ApiResult Api<TRequest>(TRequest request, Func<TRequest, ResultObject> handle){        
try{ var requestBase = request as IRequest;
if (requestBase != null){ //处理需要登录用户的请求var userRequest = request as UserRequestBase;
if (userRequest != null){
var loginUser = LoginUser.GetUser(); if (loginUser != null){userRequest.ApiUserID = loginUser.UserID;userRequest.ApiUserName = loginUser.UserName;}}
var validResult = requestBase.Validate(); if (validResult != null){
return new ApiResult(validResult);}}
var result = handle(request); //处理请求return new ApiResult(result);} catch (Exception exp){ //异常日志:return new ApiResult { ResultData = new ResultObject { Code = 1,
Msg = "系统异常:" + exp.Message } };}}public ApiResult Api(Func<ResultObject> handle){ try{ var result = handle();//处理请求return new ApiResult(result);} catch (Exception exp){ //异常日志return new ApiResult { ResultData = new ResultObject { Code = 1, Msg = "系统异常:" + exp.Message } };}} /// <summary>/// 异步api/// </summary>/// <typeparam name="TRequest"></typeparam>/// <param name="request"></param>/// <param name="handle"></param>/// <returns></returns>public Task<ApiResult> ApiAsync<TRequest, TResponse>(TRequest request, Func<TRequest, Task<TResponse>> handle) where TResponse : ResultObject{ return handle(request).ContinueWith(x =>{ return Api(() => x.Result);});}}

最常用的应该就是第一个Api<TRequest>方法,里面处理了请求参数的验证,把用户信息赋给需要的请求对象,异常记录等。第二个方法是对没有请求参数的api调用处理。第三个方法是异步处理,可以对异步IO处理做一些优化,比如你提供的这个接口是调用的另一个网络接口的情况。

 

四、权限验证


 关于这个问题,我在一篇文章中贴了一些代码,其实只要是知道怎么回事之后,自己可以想怎么玩就怎么玩了,下面讲的的没有涉及角色的权限。

根据以往经验,我们可以把资源(也就是一个接口)的权限分为三个等级(标红的第二点很重要,会大大简化后台权限管理的工作):

1,公开和访问

2,登录用户可访问

3,有权限的登录用户可访问

所以我们如此设计验证的过滤器:

    public class AuthFilterAttribute : ActionFilterAttribute{        /// <summary>/// 匿名可访问        /// </summary>public bool AllowAnonymous { get; set; }        /// <summary>/// 登录用户就可以访问        /// </summary>public bool OnlyLogin { get; set; }        /// <summary>/// 使用的资源权限名,比如多个接口可以使用同一个资源的权限,默认是/ControllerName/ActionName        /// </summary>public string PowerName { get; set; }      

public sealed override void OnActionExecuting(ActionExecutingContext filterContext){ //跨域时,客户端会用OPTIONS请求来探测服务器if (filterContext.HttpContext.Request.HttpMethod == "OPTIONS"){ var origin = filterContext.HttpContext.Request.Headers["Origin"]; if (true) //可以维护一个允许跨域的域名集合,类判断是否可以跨域{filterContext.HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", origin ?? "*");}filterContext.Result = new EmptyResult();
return;} if (AllowAnonymous) return;
var user = LoginUser.GetUser(); if (user == null){filterContext.Result = new ApiResult{ResultData = new ResultObject { Code = -1, Msg = "未登录" },JsonRequestBehavior = JsonRequestBehavior.AllowGet}; return;}
if (OnlyLogin) return;
var url = PowerName;
if (string.IsNullOrEmpty(url)){url = "/" + filterContext.ActionDescriptor.ControllerDescriptor.ControllerName + "/" + filterContext.ActionDescriptor.ActionName;}
var hasPower = true; //可以根据 user和url等信息判断是否有权限if (!hasPower){filterContext.Result = new ApiResult{ResultData = new ResultObject { Code = -2, Msg = "无权限" },JsonRequestBehavior = JsonRequestBehavior.AllowGet};}}}

AllowAnonymous属性和OnlyLogin属性的功能已经说过了,匿名访问就是公开的,一个系统总会需要这样的接口,登录可访问一般针对安全性比较低,比如字典数据的获取,只要登录了,就可以访问,在权限管理里也不用配置了。

PowerName的属性是出于什么考虑呢?有些时候,两个接口的权限级别是绑定在一起的,比如一个商品的添加和修改接口,可以设置成同一个资源权限,所以都可以设置成/product/edit,这样我们在权限管理里,只要维护/product/edit,而不需要分别维护/product/add和/product/update了(例子可能不太恰当,因为很多时候添加和修改本来就是一个接口,但是这个情况的确存在,设置PowerName也是为了简化后台的权限管理)。

对于跨域的情况,上面代码也有注释,客户端会用OPTIONS动作来探测服务器,除了上述代码,在web.config也需要配置一下:

  <system.webServer><httpProtocol><customHeaders><!--<add name="Access-Control-Allow-Origin" value="*" />--><add name="Access-Control-Allow-Headers" value="Origin, X-Requested-With, 
Content-Type, Accept,apiToken"
/></customHeaders></httpProtocol></system.webServer>

配置中注释掉的一行,我故意留着,就是因为要和代码里有个对应的地方,在配置中只能配置为“*” 和特定域名,我们要更灵活,所以在程序里控制,可以允许一个域名列表。

 LoginUser的逻辑和上面的连接里的代码差不多,不再贴了,下载里也有,apiToken从cookie和http头部都可以取得,这样不管是同域名网页,跨域,app都是可以调用接口的。

 

五、模型生成


以前的模型生产器很多,现在使用T4模板的也不少,而且VS里自带T4模板。但是我不太喜欢用T4(主要是没有智能提示)。我感觉Razor引擎就挺好呀,完全可以用来生成模型。自己写的一个ORM新加了两个方法,来获取数据库表的元数据,目前支持MSSql和MySql,稍微写点代码就可以生成模型了,下面是cshtml的内容,截图是为了展示代码高亮效果,哈哈(完整代码在最下方有下载)

所以有时候,自己动动手还是挺好的。其实所有web语言都可以生成,jsp,php,nodejs,和动态生成页面返回给客户端是一样的,这个只不过是写到文件里。

 

六、文档生成


这里自然说的是API文档,和上面那个生成模型不太一样,虽说生成基本上都是:模板+数据=结果,但是这个生成在获取数据的时候有点难点,先看效果图:

api文档自动生成的重要性想必大家都知道了,如果还是手动写word或excel,工作量大不说,是很难保持一致性的。

   1. asp.net webapi 自带一个Help Page 有兴趣可以了解。

   2. Swagger 是个生成api的框架,很强大,也支持接口测试,但是.net下的swagger好像只能使用在webapi中,一般的mvc不行,有兴趣的也可以了解。

下面主要说一下本轮子的实现。从一个类型得到一个该类型的对象图,在不严谨的情况下,还是比容易实现的,主要用反射和递归就可以了。

上面截图中的C#类:

public class GetProductRequest : IRequest{        /// <summary>/// 商品编号        /// </summary>public int? ProductID { get; set; }       
public ResultObject Validate(){ if (ProductID == null || ProductID.Value <= 0){
return new ResultObject { Code = 1, Msg = "商品编号有误" };} return null;}} public class GetProductResponse : IResponse{ /// <summary>/// 编号        /// </summary>public int? ID { get; set; } /// <summary>/// 商品名称        /// </summary>public string Name { get; set; } /// <summary>/// 颜色集合        /// </summary>public List<string> Colors { get; set; }

public List<ProductTag> TagList { get; set; }}

public class ProductTag{ /// <summary>/// 标签编号        /// </summary>public int ID { get; set; } /// <summary>/// 标签名称        /// </summary>public string TagName { get; set; }}

 转换成JSON字符串:

{"data": {"id": 0,"name": "str","colors": ["str"],"tagList": [{"id": 0,"tagName": "str"}]},"code": 0,"msg": "str"
}

 这样我们就显示了对象的结构,但是如果加上注释呢? 如何显示成下面的结果呢?这也是本轮子的特色,还是以json的格式展示中文说明。

{"data": {"id": "编号","name": "商品名称","colors": ["颜色集合"],"tagList": [{"id": "标签编号","tagName": "标签名称"}]},"code": "等于0表示成功","msg": "code不为0时,返回错误消息"
}

 思考一下,一个什么样的对象才能被序列化成上面显示的JSON字符串呢?

沿着这个思路,我打算在生成对象图的时候再生成一个对象B,对象B用字典表示,而且末端的值填充成为对象图对应属性的Summary。

比如 一个C#类:

    public class A{        /// <summary>/// 编号/// </summary>public int ID { get; set; }        /// <summary>/// 字符串列表/// </summary>public List<string> StrList { get; set; }public List<Sub> SubList { get; set; }public class Sub{            /// <summary>/// Sub名称/// </summary>public int SubName { get; set; }}}

 在构建A的对象图的同时会像执行如下代码一样构建另一个对象B:

    Dictionary<string, object> dict = new Dictionary<string, object>();dict.Add("ID", "编号");dict.Add("StrList", new List<string> { "字符串列表" });  
var subDict = new Dictionary<string, object>();subDict.Add("SubName", "Sub名称");dict.Add("SubList", new List<Dictionary<string, object>> { subDict });

 这段代码是很不完善的,但是目前够用了,不够用可以再改嘛,javascript数据类型本来也不多,接口定义当然也是越简单越好了。可巧的是webapi的 help page里也有个同名同功的ObjectGenerator,它的实现是比较完善的,但是只返回了对象图,我开始还打算要在它上面按照我的思路修改一下呢,尝试之后就作罢了,改动太多了,而且对我来说,上面代码够用了。

 上面的summaryDict可以从外部读取注释文件获取,要读取哪些项目的注释都需要设置一下:

读取的代码也很简单,因为我只关注属性的注释,所以我只读取属性的:

        Dictionary<string, string> getSummaryDict(){            var path = Server.MapPath("~/") + "bin\\";         
var files = Directory.GetFiles(path, "*.xml");Dictionary<string, string> msDict = new Dictionary<string, string>();
foreach (var file in files){ XmlDocument xmldoc = new XmlDocument();xmldoc.Load(file);
var memberNodes = xmldoc.SelectNodes("/doc/members/member"); foreach (XmlNode item in memberNodes){
var name = item.Attributes["name"].Value;
if (name.StartsWith("P:")) //只取属性{ var summaryNode = item
.SelectSingleNode("summary");
if (summaryNode != null){msDict[name] = summaryNode.InnerText.Trim();}}}} return msDict;}

 


平时如果打游戏,一般11点就睡了;写博客的话,总是需要几个凌晨后。想想真是佩服那些坚持写博客的人!

Demo并不完整,没有真正读取数据库,有兴趣的同学可以下载下来玩玩。(由于上传大小有限,我把packages文件夹删除了)

原文地址:http://www.cnblogs.com/loogn/p/6275659.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/327153.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

用limit 实现java的简单分页

https://blog.csdn.net/xinyuezitang/article/details/84324359 用limit 实现java的简单分页 xinyuezitang 2018-11-21 16:01:13 4447 收藏 9 分类专栏&#xff1a; Java 小Demo 文章标签&#xff1a; 分页 limit mysql 实现java分页 版权 一 mysql 中limit 用法 select …

Mybatis使用IN语句查询

https://blog.csdn.net/u011781521/article/details/79669180 Mybatis使用IN语句查询 lfendo 2018-03-23 16:45:03 201525 收藏 140 分类专栏&#xff1a; MyBatis 文章标签&#xff1a; mybatis JAVA sql 版权 一、简介 在SQL语法中如果我们想使用in的话直接可以像如下一…

IdentityServer4 实现 OpenID Connect 和 OAuth 2.0

关于 OAuth 2.0 的相关内容&#xff0c;点击查看&#xff1a;ASP.NET WebApi OWIN 实现 OAuth 2.0 OpenID 是一个去中心化的网上身份认证系统。对于支持 OpenID 的网站&#xff0c;用户不需要记住像用户名和密码这样的传统验证标记。取而代之的是&#xff0c;他们只需要预先在一…

Java动态代理机制详解(JDK 和CGLIB,Javassist,ASM)

转载自 Java动态代理机制详解&#xff08;JDK 和CGLIB&#xff0c;Javassist&#xff0c;ASM&#xff09; class文件简介及加载 Java编译器编译好Java文件之后&#xff0c;产生.class 文件在磁盘中。这种class文件是二进制文件&#xff0c;内容是只有JVM虚拟机能够识别的机器码…

彻底理解JAVA动态代理

转载自 彻底理解JAVA动态代理 代理设计模式 定义&#xff1a;为其他对象提供一种代理以控制对这个对象的访问。 代理模式的结构如下图所示。 动态代理使用 java动态代理机制以巧妙的方式实现了代理模式的设计理念。 代理模式示例代码 public interface Subject { pu…

docker 安装redis 挂载到宿主机

1.首先去redis获取对应版本的配置文件redis.conf&#xff1a; http://download.redis.io/releases/ 我选择的是 6.0.9 解压以后 有一个redis.conf 2.将 bind 127.0.0.1注释&#xff0c;daemonize yes注释掉&#xff0c;如果需要redis密码则找到 requirepass 并填上你的密码 …

搭建consul 集群

上图是官网提供的一个事例系统图&#xff0c;图中的Server是consul服务端高可用集群&#xff0c;Client是consul客户端。consul客户端不保存数据&#xff0c;客户端将接收到的请求转发给响应的Server端。Server之间通过局域网或广域网通信实现数据一致性。每个Server或Client都…

Java类加载的那些事

转载自 Java类加载的那些事 前言 Java源代码被编译成class字节码&#xff0c;最终需要加载到虚拟机中才能运行。整个生命周期包括&#xff1a;加载、验证、准备、解析、初始化、使用和卸载7个阶段。 加载 1、通过一个类的全限定名获取描述此类的二进制字节流&#xff1b; …

JAVA集合(笔记)

集合简介 概念&#xff1a;对象的容器&#xff0c;定义了对多个对象进项操作的的常用方法。可实现数组的功能。和数组的区别&#xff1a; 数组长度固定&#xff0c;集合长度不固定。数组可以存储基本类型和引用类型&#xff0c;集合只能存储引用类型。 位置&#xff1a; jav…

公司技术需求备忘录

业务现状领导要求 1) 部署环境要求: 公有云&#xff0c;私有云&#xff0c;原有院内系统。三套环境&#xff0c;兼容部署&#xff0c;一套代码多环境支持。2) 数据库要求&#xff1a;sqlserver&#xff0c;orcale&#xff0c;mysql要兼容&#xff0c;一套代码多库运行。3) 性能…

equals 和 hashCode 到底有什么联系?一文告诉你

转载自 equals 和 hashCode 到底有什么联系&#xff1f;一文告诉你 写在前面 Java的基类Object提供了一些方法&#xff0c;其中equals()方法用于判断两个对象是否相等&#xff0c;hashCode()方法用于计算对象的哈希码。equals()和hashCode()都不是final方法&#xff0c;都可…

如何将Excel的单元格设置成下拉选项?-excel设置下拉菜单

https://www.qiaoshan022.cn/excel/excel14169.html 如何将Excel的单元格设置成下拉选项&#xff1f;-excel设置下拉菜单 作者&#xff1a;乔山办公网日期&#xff1a;2019-09-24 21:13:23 返回目录&#xff1a;excel表格制作 在用Excel表录入数据时&#xff0c;有时需要限制…

写一个高性能的敏感词检测组件

最近写了一个高性能的敏感词检测组件【ToolGood.Words】。 一、高性能&#xff0c;它的效率到底有多快&#xff1f; 如果将正则表达式的算法效率设为1&#xff0c;高性能可达到正则表达式的1.5万倍。 二、选一个巧妙的算法&#xff1a; AC自动机&#xff08;Aho-Corasick Autom…

win10系统excel2019单元格显示完整的年月日时分秒设置方法

https://www.pianshen.com/article/88671983757/ win10系统excel2019单元格显示完整的年月日时分秒设置方法 技术标签&#xff1a; Windows excel显示年月日时分秒 excel显示完整的详细时间 excel2019显示完整时间 excel2019显示具体时间 excel显示具体的时间 描述&#…

Visual Studio现可使用EditorConfig

Visual Studio 2017的首个候选发布版&#xff08;VS2017RC&#xff09;中提供了支持EditorConfig标准的特性。秉承该标准可使开发人员一次性地定义一种编码风格&#xff0c;即可轻易地在不同的编辑器中使用该风格。另一显著优点是对于在EditorConfig文件中定义的风格&#xff0…

分库分表的事务处理机制

转载自 分库分表的事务处理机制 分布式事务 由于我们将单表的数据切片后存储在多个数据库甚至多个数据库实例中&#xff0c;所以依靠数据库本身的事务机制不能满足所有场景的需要。但是&#xff0c;我们推荐在一个数据库实例中的操作尽可能使用本地事务来保证一致性&#xff0…

如何优雅的使用RabbitMQ

RabbitMQ无疑是目前最流行的消息队列之一&#xff0c;对各种语言环境的支持也很丰富&#xff0c;作为一个.NET developer有必要学习和了解这一工具。消息队列的使用场景大概有3种&#xff1a; 1、系统集成&#xff0c;分布式系统的设计。各种子系统通过消息来对接&#xff0c;这…

hutool中身份证工具-IdcardUtil

JAVA工具例大全--根据身份编号获取户籍省份 发布于 2020-10-10 | 后端技术 | 浏览&#xff08;61&#xff09; | 评论&#xff08;0&#xff09;开场语(刷新后不一样):人生若只如初见&#xff0c;何事秋风悲画扇。作为一名IT人&#xff0c;你当然也想有自己一片天地&…