浅析如何在Nancy中生成API文档

前言

前后端分离,或许是现如今最为流行开发方式,包括UWP、Android和IOS这样的手机客户端都是需要调用后台的API来进行数据的交互。

但是这样对前端开发和APP开发就会面临这样一个问题:如何知道每个API做什么?

可能,有人会在内部形成一份word文档、pdf;有人会建立一个单独的站点,然后将API的地址,参数等信息列在上面;有人会借助第三方的工具来生成一份文档等。

当然,这基本是取决于不同公司的规范。

说起API文档,就想到前段时间做的微信小程序,由于那个不完善的接口文档,从而导致浪费了很大一部分时间去询问接口相关的内容(用的是老的接口)。

为了处理这个问题,我认为,如果能在写某个API的时候就顺带将这个API的相关信息一并处理了是最好不过!

不过这并不是让我们写好一个接口后,再去打开word等工具去编辑一下这个API的信息,这样明显需要花费更多的时间。

下面就针对这一问题,探讨一下在Nancy中的实现。

如何实现

其实,想在Nancy中生成API文档,是一件十分容易的事,因为作者thecodejunkie已经帮我们在Nancy内部提前做了一些处理

便于我们的后续扩展,这点还是很贴心的。

下面我们先来写点东西,后面才能相应的API文档。

public class ProductsModule : NancyModule{   
 public ProductsModule() : base("/products")  
 
{        Get("/", _ =>{          
  return Response.AsText("product list");}, null, "GetProductList");    
     Get("/{productid}", _ =>{          
      return Response.AsText(_.productid as string);}, null, "GetProductByProductId");      
        Post("/", _ =>{          
          return Response.AsText("Add product");}, null, "AddProduct");        //省略部分..} }

基本的CURD,没有太多的必要去解释这些内容。当然这里需要指出一点。

正常情况下,我们基本都是只写前面两个参数的,后面两个参数是可选的。由于我们后面需要用到每个路由的名字

所以我们需要用到这里的第4个参数(当前路由的名字),也就意味着我们要在定义的时候写多一点东西!

注: 1.x和2.x的写法是有区别的!示例用的2.x的写法,所以各位要注意这点!

以GET为例,方法定义大致如下

API写好了,下面我们先来简单获取一下这些api的相关信息!

最简单的实现

前面也提到,我们是要把这个api和api文档放到同一个站点下面,免去编辑这一步骤!

世间万物都是相辅相成的,我们不想单独编辑,自然就要在代码里面多做一些处理!

新起一个Module名为DocModule,将api文档的相关内容放到这个module中来处理。

public class DocMudule : NancyModule{ 
   private IRouteCacheProvider _routeCacheProvider;  
   
     public DocMudule(IRouteCacheProvider routeCacheProvider) : base("/docs")
 
{    
        this._routeCacheProvider = routeCacheProvider;      
          Get("/", _ =>{            var routeDescriptionList = _routeCacheProvider.GetCache().SelectMany(x => x.Value).Select(x => x.Item2).Where(x => !string.IsNullOrWhiteSpace(x.Name)).ToList();          
           return Response.AsJson(routeDescriptionList);});} }

没错,你没看错,就是这几行代码,就可以帮助我们去生成我们想要的api文档!其实最主要的是IRouteCacheProvider这个接口。

它的具体实现,会在后面的小节讲到,现在先着重于使用!

先调用这个接口的GetCache方法,以拿到缓存的路由信息,这个路由信息有必要来看一下它的定义,因为不看它的定义,我们根本就没有办法继续下去!

后续的查找都是依赖于这些缓存信息!

public interface IRouteCache : IDictionary<Type, List<Tuple<int, RouteDescription>>>,
ICollection<KeyValuePair<Type, List<Tuple<int, RouteDescription>>>>,
 IEnumerable<KeyValuePair<Type, List<Tuple<int, RouteDescription>>>>, IEnumerable

{    bool IsEmpty(); }

看了上面的定义,就可以清楚的知道要用SelectMany去拿到那个元组的内容。再取出元组的RouteDescription

当然,这个时候我们取到的是所有的路由信息,这些信息都包含了什么内容呢?看看RouteDescription的定义就很清晰了。

public sealed class RouteDescription{    public RouteDescription(string name, string method, string path, Func<NancyContext, bool> condition);    //The name of the routepublic string Name { get; set; }    //The condition that has to be fulfilled inorder for the route to be a valid match.public Func<NancyContext, bool> Condition { get; }    //The description of what the route is for.public string Description { get; set; }    //Gets or sets the metadata information for a route.public RouteMetadata Metadata { get; set; }    //Gets the method of the route.public string Method { get; }    //Gets the path that the route will be invoked for.public string Path { get; }    //Gets or set the segments, for the route, that was returned by the Nancy.Routing.IRouteSegmentExtractor.public IEnumerable<string> Segments { get; set; }
}

在查询之后,我还过滤了那些名字为空的,不让它们显示出来。为什么不显示出来呢?理由也比较简单,像DocModule,我们只定义了一个路由

而且这个路由在严格意义上并不属于我们api的内容,而且这个路由也是没有定义名字的,所以显示出来的意义也不大。

过滤之后,就得到了最终想要的信息!简单起见,这里是先直接 返回一个json对象,便于查看有什么内容,便于在逐步完善后再把它结构化。

下面是最简单实现后的大致效果:

在图中,可以看到GetProductListGetProductByProductId这两个api的基本信息:请求的method,请求的路径和路由片段。

但是这些信息真的是太少了!连api描述都见不到,拿出来,肯定被人狠狠的骂一顿!!

下面我们要尝试丰富一下我们的接口信息!

丰富一点的实现

要让文档充实,总是需要一个切入点,找到切入点,事情就好办了。仔细观察上面的效果图会发现,里面的metadata是空的。当然这个也就是丰富文档内容的切入点了。

从前面的定义可以看到,这个metadata是一个RouteMetadata的实例

public class RouteMetadata{    
//Creates a new instance of the Nancy.Routing.RouteMetadata class.public RouteMetadata(IDictionary<Type, object> metadata);  
 //Gets the raw metadata System.Collections.Generic.IDictionary`2.public IDictionary<Type, object> Raw { get; }  
  //Gets a boolean that indicates if the specific type of metadata is stored.public bool Has<TMetadata>();    //Retrieves metadata of the provided type.public TMetadata Retrieve<TMetadata>(); }

这里对我们比较重要的是Raw这个属性,因为这个是在返回结果中的一部分,它是一个字典,键是类型,值是这个类型对应的实例。

先定义一个CustomRouteMetadata,用于返回路由的Metadata信息(可根据具体情况进行相应的定义)。这个CustomRouteMetadata就是上述字典Type。

public class CustomRouteMetadata{    // group by the modulepublic string Group { get; set; }    // description of the apipublic string Description { get; set; }    // path of the apipublic string Path { get; set; }    // http method of the apipublic string Method { get; set; }    // name of the apipublic string Name { get; set; }    // segments of the apipublic IEnumerable<string> Segments { get; set; }
}

定义好我们要显示的东西后,自然要把这些东西用起来,才能体现它们的价值。

要用起来还涉及到一个MetadataModule,这个命名很像NancyModule,看上去都是一个Module。

先定义一个ProductsMetadataModule,让它继承MetadataModule<RouteMetadata>
具体实现如下:

public class ProductsMetadataModule : MetadataModule<RouteMetadata>
{    public ProductsMetadataModule()    {            Describe["GetProductList"] = desc =>{                var dic = new Dictionary<System.Type, object>{{               
     typeof(CustomRouteMetadata),              
          new CustomRouteMetadata{Group = "Products",Description = "Get All Products from Database",Path = desc.Path,Method = desc.Method,Name = desc.Name,Segments = desc.Segments}}};            return new RouteMetadata(dic);};Describe["GetProductByProductId"] = desc =>{        
             var dic = new Dictionary<System.Type, object>{{                  
            typeof(CustomRouteMetadata),            
                    new CustomRouteMetadata{Group = "Products",Description = "Get a Product by product id",Path = desc.Path,Method = desc.Method,Name = desc.Name,Segments = desc.Segments}}};            return new RouteMetadata(dic);              };        //省略部分...} }

这里的写法就和1.x里写NancyModule的内容是一样的,应该也是比较熟悉的。就不再累赘了。其中的desc是一个委托Func<RouteDescription, TMetadata>

默认返回的是一个RouteMetadata实例,而要创建一个这样的实例还需要一个字典,所以大家能看到上面的代码中定义了一个字典。

并且这个字典包含了我们自己定义的信息,其中Group和Description是完全的自定义,其他的是从RouteDescription中拿。

当然,这里已经开了一个口子,想怎么定义都是可以的!

完成上面的代码之后,再来看看我们显示的结果

可以看到我们添加的metadata相关的内容已经出来了!可能这个时候,大家也都发现了,似乎内容有那么点重复的意思!

因为这些重复,就会让人感觉这里比较臃肿,所以我们肯定不需要取出太多重复的东西,目前只需要metadata下面的这些就可以了。

下面来对其进行简化!

简化一点的实现

简化分为两步:

第一步简化:DocModule的简化。

其实,DocModule已经是相当的简单了,但是还能在简洁一点点。这里用到了RetrieveMetadata这个扩展方法来处理。

前面的做法是拿到路由的信息后,用了两个Select来查询,而且查询出来的结果有那么一点臃肿,

而借助扩展方法,可以只取metadata里面的内容,也就是前面自定义的内容,这才是我们真正意义上要用到的。

下面是具体实现的示例:

Get("/", _ =>{//01//var routeDescriptionList = _routeCacheProvider//                            .GetCache()//                            .SelectMany(x => x.Value)//                            .Select(x => x.Item2)//                            .Where(x => !string.IsNullOrWhiteSpace(x.Name))//                            .ToList();//return Response.AsJson(routeDescriptionList);//02var routeDescriptionList = _routeCacheProvider.GetCache().RetrieveMetadata<RouteMetadata>().Where(x => x != null);return Response.AsJson(routeDescriptionList);});

经过第一步简化后,已经过滤了不少重复的信息了,效果如下:

第二步简化:Metadata的简化

在返回Metadata的时候,我们是返回了一个默认的RouteMetadata对象,这个对象相比自定义的CustomRouteMetadata复杂了不少

而且从上面经过第一步简化后的效果图也可以发现,只有value节点下面的内容才是api文档需要的内容。

所以还要考虑用自定义的这个CustomRouteMetadata去代替原来的。

修改如下:

public class ProductsMetadataModule : MetadataModule<CustomRouteMetadata>
{    public ProductsMetadataModule()    {Describe["GetProductList"] = desc =>{            return new CustomRouteMetadata{Group = "Products",Description = "Get All Products from Database",Path = desc.Path,Method = desc.Method,Name = desc.Name,Segments = desc.Segments};};Describe["GetProductByProductId"] = desc =>{            return new CustomRouteMetadata{Group = "Products",Description = "Get a Product by product id",Path = desc.Path,Method = desc.Method,Name = desc.Name,Segments = desc.Segments};};        //省略部分..}
}

由于MetadataModule<TMetadata> 中的TMetadata是自定义的CustomRouteMetadata,所以在返回的时候直接创建一个简单的实例即可

不需要像RouteMetadata那样还要定义一个字典。

同时,还要把DocModuleRetrieveMetadata的TMetadata也要替换成CustomRouteMetadata

 var routeDescriptionList = _routeCacheProvider.GetCache()                            //.RetrieveMetadata<RouteMetadata>()                                            .RetrieveMetadata<CustomRouteMetadata>().Where(x => x != null);

经过这两步的简化,现在得到的效果就是我们需要的结果了!

最后,当然要专业一点,不能让人只看json吧!怎么都要添加一个html页面,将这些信息展示出来:

当然,现在看上去还是很丑,文档内容也并不丰富,但是已经把最简单的文档做出来了,想要进一步丰富它就可以自由发挥了。

实现探讨

既然这样简单的代码就能帮助我们去生成api文档,很有必要去研究一下Nancy帮我们做了什么事!

从最开始的IRouteCacheProvider入手,这个接口对应的默认实现DefaultRouteCacheProvider

public class DefaultRouteCacheProvider : IRouteCacheProvider, IDiagnosticsProvider{   
 /// <summary>/// The route cache factory/// </summary>protected readonly Func<IRouteCache> RouteCacheFactory;    /// <summary>/// Initializes a new instance of the DefaultRouteCacheProvider class./// </summary>/// <param name="routeCacheFactory"></param>public DefaultRouteCacheProvider(Func<IRouteCache> routeCacheFactory)  
 
{        this.RouteCacheFactory = routeCacheFactory;}    /// <summary>/// Gets an instance of the route cache./// </summary>/// <returns>An <see cref="IRouteCache"/> instance.</returns>public IRouteCache GetCache()    {    
    return this.RouteCacheFactory();}    //省略部分..}

里面的GetCache方法是直接调用了定义的委托变量。最终是到了IRouteCache的实现类RouteCache,这个类算是一个重点观察对象!

内容有点多,就只贴出部分核心代码了

它在构造函数里去生成了路由的相关信息。

public RouteCache(    INancyModuleCatalog moduleCatalog,    INancyContextFactory contextFactory,    IRouteSegmentExtractor routeSegmentExtractor,    IRouteDescriptionProvider routeDescriptionProvider,    ICultureService cultureService,    IEnumerable<IRouteMetadataProvider> routeMetadataProviders){ 
   this.routeSegmentExtractor = routeSegmentExtractor;  
    this.routeDescriptionProvider = routeDescriptionProvider;  
     this.routeMetadataProviders = routeMetadataProviders;  
      var request = new Request("GET", "/", "http");  
       using (var context = contextFactory.Create(request)){        this.BuildCache(moduleCatalog.GetAllModules(context));} }

具体的生成方法如下:遍历所有的NancyModule,找到每个Module的RouteDescription集合(一个Module可以包含多个路由)

然后找到每个RouteDescription的描述,路由片段和metadata的信息。最后把这个Module路由信息添加到当前的对象中!

private void BuildCache(IEnumerable<INancyModule> modules){    

foreach (var module in modules){        var moduleType = module.GetType();    

   var routes =module.Routes.Select(r => r.Description).ToArray();    
       foreach (var routeDescription in routes){routeDescription.Description = this.routeDescriptionProvider.GetDescription(module, routeDescription.Path);routeDescription.Segments = this.routeSegmentExtractor.Extract(routeDescription.Path).ToArray();routeDescription.Metadata = this.GetRouteMetadata(module, routeDescription);}        this.AddRoutesToCache(routes, moduleType);} }

前面提到RouteDescription的描述,路由片段和metadata的信息都是通过额外的方式拿到的,这里主要是拿metadata来做说明

毕竟在上面最后的一个例子中,用到的是metadata的内容。

先调用定义的私有方法GetRouteMetadata,这个方法里面的内容是不是和前面的MetadataModule有点类似呢,字典和创建RouteMetadata的实例。

private RouteMetadata GetRouteMetadata(INancyModule module, RouteDescription routeDescription){    var data = new Dictionary<Type, object>();    foreach (var provider in this.routeMetadataProviders){        var type = provider.GetMetadataType(module, routeDescription);        var metadata = provider.GetMetadata(module, routeDescription);        if (type != null && metadata != null){data.Add(type, metadata);}}    return new RouteMetadata(data);
}

重点的是provider。这个provider来源来IRouteMetadataProvider,这个接口就两个方法。

Nancy这个项目中还有一个抽象类是继承了这个接口的。但是这个抽象类是没有默认实现的。

public abstract class RouteMetadataProvider<TMetadata> : IRouteMetadataProvider{    

public Type GetMetadataType(INancyModule module, RouteDescription routeDescription)  
 
{        return typeof(TMetadata);
 }  
 public object GetMetadata(INancyModule module, RouteDescription routeDescription)  
 
{    
    return this.GetRouteMetadata(module, routeDescription);}  
      protected abstract TMetadata GetRouteMetadata(INancyModule module, RouteDescription routeDescription); }

注:前面的原理分析都是基于Nancy这个项目。

这个时候,另外一个项目Nancy.Metadata.Modules就起作用了。我们编写的MetadataModule也是要添加这个的引用才能正常使用的。

从上面编写的MetadataModule可以看出这个项目的起点应该是MetadataModule,而且有关metadata的核心也在这里了。

public abstract class MetadataModule<TMetadata> : IMetadataModule where TMetadata :
class{  
 private readonly IDictionary<string, Func<RouteDescription, TMetadata>> metadata;  
  protected MetadataModule()  
 
{    
     this.metadata = new Dictionary<string, Func<RouteDescription, TMetadata>>();}  
 // Gets <see cref="RouteMetadataBuilder"/> for describing routes.public RouteMetadataBuilder Describe{        get { return new RouteMetadataBuilder(this); }}  
 // Returns metadata for the given RouteDescription.public object GetMetadata(RouteDescription description)  
 
{    
   if (this.metadata.ContainsKey(description.Name)){        
       return this.metadata[description.Name].Invoke(description);}        return null;}    // Helper class for configuring a route metadata handler in a module.public class RouteMetadataBuilder{        private readonly MetadataModule<TMetadata> parentModule;  
       
       public RouteMetadataBuilder(MetadataModule<TMetadata> metadataModule)    
   
{            this.parentModule = metadataModule;}  
       // Describes metadata for a route with the specified name.public Func<RouteDescription, TMetadata> this[string name]{        
          set { this.AddRouteMetadata(name, value); }}      
          
    protected void AddRouteMetadata(string name, Func<RouteDescription, TMetadata> value)        {            this.parentModule.metadata.Add(name, value);}}    //省略部分..}

到这里,已经将GetCache的内内外外都简单分析了一下。至于扩展方法RetrieveMetadata就不在细说了,只是selectmany和select的一层封装。

写在最后

本文粗略讲解了如何在Nancy中生成API文档,以及简单分析了其内部的处理。

下一篇将继续介绍这一块的内容,不过主角是Swagger。

原文地址:http://www.cnblogs.com/catcher1994/p/6791352.html


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

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

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

相关文章

一文告诉你如何导出 Git 变更文件

转载自 一文告诉你如何导出 Git 变更文件 有时候我们想导出某次版本提交时有哪些变更的文件&#xff0c;在 svn 中有一个 export 功能&#xff0c;很方便&#xff0c;如下图所示。 在 Git 中我也找到了以下两种方法。 方法1 使用 git 自带命令 git archive, 语法如下。 g…

Entity Framework Core的贴心:优雅处理带默认值的数据库字段

对于用于保存记录添加时间的数据库日期字段&#xff0c;我们通常会设置一个 GETDATE() 的默认值&#xff0c;而不是在应用程序的代码中获取当前时间进行保存&#xff0c;这样可以避免由于web服务器时钟不同步引起的时间偏差。 Entity Framework Core 在设计时贴心地考虑到这个…

JS中使用工厂模式创建对象

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title> </head> <body> <script>function createPerson(name , age ,gender){//创建一个新的对象var obj new Object…

深度历险:Redis 内存模型详解

转载自 深度历险&#xff1a;Redis 内存模型详解 Redis 是目前最火爆的内存数据库之一&#xff0c;通过在内存中读写数据&#xff0c;大大提高了读写速度&#xff0c;可以说 Redis 是实现网站高并发不可或缺的一部分。 我们使用 Redis 时&#xff0c;会接触 Redis 的 5 种对…

.NET的一点历史故事:误入歧途,越陷越深

移动计算的时代其实早已有了苗头&#xff0c;起码微软的 Windows CE、诺基亚的 Symbian 都曾经给手机用户多少带来了一些便利&#xff0c;黑莓则由于接入了企业邮件等商业应用而日进斗金&#xff0c;甚至 Sun 也给自己的 Java 平台做了一个 J2ME 标准&#xff0c;试图用 J2EE 那…

bootstrap样式代码案例

运行结果如下所示&#xff1a; 代码如下所示&#xff1a; <!DOCTYPE html> <html><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width,user-scalableno,initial-scale1.0,maximum-scale1.0,mi…

JS中用构造函数创建对象

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title> </head> <body> <script>/* 构造函数的执行流程&#xff1a;* 1.立刻创建一个新的对象* 2.将新建的对象设置…

SELECT * FROM user WHERE username LIKE #{aaa}与SELECT * FROM user WHERE username LIKE '%${value}%'

在Mybatis中模糊查询like有两种写法&#xff1a; 第一种为SELECT * FROM user WHERE username LIKE #{aaa} 另一种SELECT * FROM user WHERE username LIKE ‘%${value}%’ LIKE #{aaa}执行的SQL为&#xff1a; 使用的是&#xff1f;占位符&#xff1a;对用的是preparedStatem…

bootstrap组件的案例代码

运行结果如图所示&#xff1a; 代码如下所示&#xff1a; <!DOCTYPE html> <html><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width,user-scalableno,initial-scale1.0,maximum-scale1.0,mi…

了解 C# foreach 内部语句和使用 yield 实现的自定义迭代器

在本期专栏中&#xff0c;我将介绍我们在编程时经常用到的 C# 核心构造&#xff08;即 foreach 语句&#xff09;的内部工作原理。了解 foreach 内部行为后&#xff0c;便可以探索如何使用 yield 语句实现 foreach 集合接口&#xff0c;我将对此进行介绍。 虽然 foreach 语句编…

JS中的原型

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title></title><script type"text/javascript">/** 原型 prototype* * 我们所创建的每一个函数&#xff0c;解析器都会向函数中添加一个属性prototype* …

扫盲,为什么分布式一定要有Redis?

转载自 扫盲&#xff0c;为什么分布式一定要有Redis? 考虑到绝大部分写业务的程序员&#xff0c;在实际开发中使用 Redis 的时候&#xff0c;只会 Set Value 和 Get Value 两个操作&#xff0c;对 Redis 整体缺乏一个认知。所以我斗胆以 Redis 为题材&#xff0c;对 Redis …

Mybatis insert操作细节【ID】

默认情况下映射文件中插入数据&#xff1a; <insert id"saveUser" parameterType"com.itheima.domain.User">INSERT INTO user (username,address,sex,birthday) VALUES (#{username},#{address},#{sex},#{birthday})</insert>单元测试 Testp…

关于人脸识别最近浏览器打不开摄像头的解决方案

好久没有发公众号啦&#xff0c;因为最近没有在技术方面有更高的提升&#xff0c;关于人脸识别浏览器兼容问题一直很头疼&#xff0c;时至今日&#xff0c;随着浏览器的更新&#xff0c;代码也不得不更新一下了&#xff0c;今天主要是给大家解决一个谷歌浏览器里面的错&#xf…

C# 7 中的模范和实践

原文地址:https://www.infoq.com/articles/Patterns-Practices-CSharp-7 关键点 遵循 .NET Framework 设计指南&#xff0c;时至今日&#xff0c;仍像十年前首次出版一样适用。API 设计至关重要&#xff0c;设计不当的API大大增加错误&#xff0c;同时降低可重用性。始终保持&q…

JS重写toString(),打印想要的值

<!DOCTYPE html> <html> <head><meta charset"UTF-8"><title></title><script type"text/javascript">function Person(name , age , gender){this.name name;this.age age;this.gender gender;}//修改Perso…

Mybatis实体类属性名与数据库类名不对应的两种解决方法

在Mybatis开发时&#xff0c;如果 Bean的属性名与数据库的类名不一致时&#xff0c;CRUD将出现问题。 数据库类名 Bean的属性名&#xff1a;&#xff08;默认&#xff09; 调整Bean中的属性名&#xff1a;&#xff08;测试不一致&#xff09; 此时原有代码将会报错&#xff…

揭开Java 泛型类型擦除神秘面纱

转载自 揭开Java 泛型类型擦除神秘面纱 泛型&#xff0c;一个孤独的守门者。 大家可能会有疑问&#xff0c;我为什么叫做泛型是一个守门者。这其实是我个人的看法而已&#xff0c;我的意思是说泛型没有其看起来那么深不可测&#xff0c;它并不神秘与神奇。泛型是 Java 中一…

ASP.Net防范XSS漏洞攻击的利器HtmlSanitizer

项目名称:HtmlSanitizer NuGet安装指令:Install-Package HtmlSanitizer 官方网站:https://github.com/mganss/HtmlSanitizer 开源协议:MIT 可靠程度:更新活跃,目前已经是3.x版,成熟靠谱。 1、 什么是XSS漏洞? XSS漏洞又称为“跨站脚本”漏洞,指的是网站对于用户输入的内…

阿里巴巴制定了这 16 条

转载自 阿里巴巴制定了这 16 条 本文内容整理自《阿里巴巴Java开发手册 1.4.0》&#xff0c;获取完整版请在公众号后台回复关键字&#xff1a;手册。 1、【强制】存储方案和底层数据结构的设计获得评审一致通过&#xff0c;并沉淀成为文档。 说明&#xff1a;有缺陷的底层数…