博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
使用ASP.NET Web API构建Restful API
阅读量:3523 次
发布时间:2019-05-20

本文共 16767 字,大约阅读时间需要 55 分钟。

目录


在本文中,我们将构建测试并使用asp.net web api

介绍

让我们快速回顾一下ASP.NET MVC架构。因此,当请求到达我们的应用程序时,MVC Framework将该请求移交给控制器中的操作,此操作大部分时间返回一个视图,然后由razor视图引擎解析,最终将HTML标记返回给客户端。因此,在这种方法中,在服务器上生成html标记,然后返回到客户端。

https://www.codeproject.com/KB/webservices/1252477/client-server.PNG

有另一种生成HTML标记的方法,我们可以在客户端生成它。因此,不是我们的操作返回HTML标记,它们可以返回原始数据。

https://www.codeproject.com/KB/webservices/1252477/data.PNG

这种方法有什么好处?

在客户端上生成标记有很多好处。

  • 它需要更少的服务器资源(它潜在地提高了应用程序的可伸缩性,因为每个客户机将负责生成自己的视图)
  • 原始数据通常比HTML标记需要更少的带宽。因此,数据可能会更快到达客户端。这可以提高应用程序的感知性能。
  • 此方法支持广泛的客户端,如移动和平板电脑应用程序。

这些应用程序简称为端点获取数据并在本地生成视图。我们将这些端点称为数据服务(Web API),因为它们只返回数据,而不是标记。

Web API不仅限于交叉设备,它还广泛用于我们的Web应用程序中,以添加新功能,如youtubefacebooktwitter等许多热门网站,从而公开我们可以在我们的Web应用程序中使用的公共数据服务。我们可以将他们的数据与我们应用程序中的数据合并,并为新用户提供新的体验。这些都是好处。

这些数据服务不仅仅是为了获取数据,我们还提供修改数据的服务,例如添加客户等。我们用来构建这些数据服务的框架称为Web API。这个框架是在ASP.Net MVC之后开发的,但它遵循ASP.NET MVC的相同体系结构和原理,因此它具有路由,控制器,操作,操作结果等。我们在这里看到的细微差别也很少。在.Net Core中,Microsoft已将这两个框架(ASP.NET MVCASP.NET Web API)合并到一个框架中。

Restful约定

所以你知道什么是http服务,什么是web api。在这里,我们将开发一个支持几种不同类型请求的应用程序。

GET        / api / customers(获取客户列表)

GET       / api / customers / 1(获得单个客户)

POST     / api / customers(添加客户并在请求正文中添加客户数据)

不要混淆请求数据的GETPOST方式,我们使用get请求来获取资源或数据的列表。我们使用post请求来创建新的数据。

现在更新学生我们使用PUT请求。

PUT        / api / customers / 1

因此,客户的ID位于URL中,要更新的实际数据或属性将位于请求正文中。最后删除学生。

Delete     / api / customers / 1

我们将HttpDelete请求发送到端点。所以你在这里看到的,就请求类型和端点而言,是一个标准约定,被交付请求RESTRepresentational State Transfer

构建API

此类派生自ApiController,而不是Controller。如果您正在使用任何现有项目,则只需在controllers文件夹中添加一个新文件夹,并在此处添加api控制器。并添加这些操作,但在apis中定义操作之前,这是我的Customer模型类。

public class Customer{    public int Id { get; set; }    [Required]    [StringLength(255)]    public string Name { get; set; }    public bool IsSubscribedToNewsLetter { get; set; }    [Display(Name = "Date of Birth")]    public DateTime? Birthdate { get; set; }    [Display(Name = "Membership Type")]    public byte MembershipTypeId { get; set; }    // it allows us to navigate from 1 type to another    public MembershipType MembershipType { get; set; }}

这是我的DbContext

public class ApplicationDbContext : IdentityDbContext
{    public ApplicationDbContext()        :base("DefaultConnection", throwIfV1Schema: false)    {    }    public static ApplicationDbContext Create()    {        return new ApplicationDbContext();    }    public DbSet
Customers { get; set; }    public DbSet
MembershipTypes { get; set; }}

现在,您可以轻松地为Api编写操作。

public IEnumerable
GetCustomers(){}

因为我们正在返回一个对象列表。按约定,此操作将作出回应

//Get       / api / customers

所以这是ASP.Net Web API中内置的约定。现在,在这个操作中,我们将使用我们的上下文从数据库中获取客户。

namespace MyAPI.Controllers.Api{    public class CustomersController : ApiController    {        private readonly ApplicationDbContext _context;        public CustomersController()        {            _context = new ApplicationDbContext();        }        // GET /api/customers        public IEnumerable
GetCustomers()        {            return _context.Customers.ToList();        }    }}

如果找不到资源,我们返回找不到httpresponse,否则我们返回该对象。

// POST /api/customers[HttpPost]public Customer CreateCustomer(Customer customer){}

所以这是客户参数将在请求体中,ASP.NET Web API框架将自动初始化它。现在我们应该用HttpPost标记这个操作,因为我们正在创建资源。如果我们遵循命名约定,那么我们甚至不需要将操作动词放在操作上。

// POST /api/customerspublic Customer PostCustomer(Customer customer){}

但最初它不是那么好的方法,让我们假设您将来重构代码并重命名您的操作,那么您的代码肯定会破坏。所以总是喜欢在操作的顶部使用Http动词。

现在让我们使用api操作的post请求的将客户对象插入到数据库中。

// POST /api/customers[HttpPost]public Customer CreateCustomer(Customer customer){    if (!ModelState.IsValid)    {        throw new HttpResponseException(HttpStatusCode.BadRequest);    }    _context.Customers.Add(customer);    _context.SaveChanges();    return customer;}

另一个操作让我们假设我们要更新记录。

// PUT /api/customers/1[HttpPut]public void UpdateCustomer(int id, Customer customer){    if (!ModelState.IsValid)    {        throw new HttpResponseException(HttpStatusCode.BadRequest);    }    var custmr = _context.Customers.SingleOrDefault(x => x.Id == id);    // Might be user sends invalid id.    if (custmr == null)    {        throw new HttpResponseException(HttpStatusCode.NotFound);    }    custmr.Birthdate = customer.Birthdate;    custmr.IsSubscribedToNewsLetter = customer.IsSubscribedToNewsLetter;    custmr.Name = customer.Name;    custmr.MembershipTypeId = customer.MembershipTypeId;    _context.SaveChanges();}

在这种情况下,不同的人有不同的意见来返回void或对象。如果我们进行Api的删除操作。

// Delete /api/customers/1[HttpDelete]public void DeleteCustomer(int id){    var custmr = _context.Customers.SingleOrDefault(x => x.Id == id);    // Might be user sends invalid id.    if (custmr == null)    {        throw new HttpResponseException(HttpStatusCode.NotFound);    }    _context.Customers.Remove(custmr);    // Now the object is marked as removed in memory    // Now it is done    _context.SaveChanges();}

这就是我们如何使用restful约定来构建api

测试API

如果我们运行应用程序并请求api控制器,我们可以看到基于XML语言的客户列表。

<a href="http://localhost:53212/api/customers">http://localhost:53212/api/customers</a>

https://www.codeproject.com/KB/webservices/1252477/XML.PNG

因此,ASP.NET Web API具有我们所谓的媒体格式化程序。因此,我们从一个动作中返回的内容(在我们的情况下,客户列表将根据客户要求进行格式化)让我解释一下我的意思和我想说的内容。

在上面的屏幕上检查浏览器并刷新页面,在这里您将看到客户请求。

https://www.codeproject.com/KB/webservices/1252477/response.PNG

这里查看内容类型,如果您在我们的请求中没有设置内容类型标头,则默认情况下服务器采用application / xml

注意: General是请求标头,Response Headers是我们的响应标头。正如您在Request Header中看到的,我们没有任何内容类型。现在让我告诉你最好测试api的方式并返回json数据。

在您的计算机中安装Postman Desktop App。并使用localhost端口号复制浏览器链接并将其粘贴到

在这里,我们将请求的urllocalhost放在一起,而响应在json中返回。

https://www.codeproject.com/KB/webservices/1252477/json.PNG

如果我们点击Header选项卡,这里我们将看到我们的请求标头内容类型是application / json

https://www.codeproject.com/KB/webservices/1252477/app-json.PNG

大多数时候我们将使用json,因为它是javascript代码的原生代码,比xml快得多。XML媒体格式主要用于政府等大型组织,因为它们落后于现代技术。Json格式更轻量级,因为它没有像xml那样的多余的开启和关闭选项卡。

一点困惑:

有时当你使用Apipostman时,大多数人都会对Postman的界面感到困惑,因为他们之前从未使用过postman。记住这一点非常简单,

https://www.codeproject.com/KB/webservices/1252477/req_resp.PNG

因此,如果您正在处理请求并尝试更改请求的某些信息,请关注请求标头,如果您正在监视响应,请在响应标头中查看结果。因为它们看起来相同,有时当请求标题的向下滚动消失时。所以不要混淆事情。

现在让我们使用Api Post 操作在数据库中插入一个客户。

https://www.codeproject.com/KB/webservices/1252477/post.PNG

从下拉列表和请求正文选项卡中选择Post请求。您可以在单击表单数据时插入具有键值对的客户。但大多数时候我们使用json格式。所以点击raw并在这里写json

https://www.codeproject.com/KB/webservices/1252477/raw.PNG

不要将Id属性放在json中,因为当我们在数据库中插入数据时,它是硬性规则,在服务器上自动生成id

现在点击Send按钮,这里我已通过Post api操作成功插入数据并获得响应。

https://www.codeproject.com/KB/webservices/1252477/created.PNG

这里上面的块是请求块,下面的块是响应块。你可能会在这里遇到某种错误。

https://www.codeproject.com/KB/webservices/1252477/error.PNG

如果您阅读错误消息'此资源不支持请求实体的媒体类型'text / plain'。这是错误消息。

现在来解决这个错误。单击Header选项卡并添加content-type的值('application / json'

https://www.codeproject.com/KB/webservices/1252477/Solution.PNG

这里已经添加了值。查看请求的状态代码是200 OK,我们可以在下面看到响应正文。

现在让我们更新客户实体。

https://www.codeproject.com/KB/webservices/1252477/HttpPut.PNG

看看它已经更新。

https://www.codeproject.com/KB/webservices/1252477/Put_Response.PNG

现在让我们删除一条记录,只需在下拉列表中选择Delete,然后在url中指定带正斜杠的id,然后单击Send按钮。它会被自动删除。

最佳实践:

最佳实践是在构建api时以及在应用程序中使用api之前。通过Postman测试api会更好。

数据传输对象(DTO

所以现在我们已经构建了这个API,但这个设计存在一些问题。我们的api接收或返回Customer对象。现在你可能会想到这种方法有什么问题?实际上,Customer对象是我们应用程序的域模型的一部分。当我们在应用程序中实现新功能时,它被认为是可以经常更改的实现细节,并且这些更改可能会抓取依赖于客户对象的现有客户端,即如果我们重命名或删除我们的属性,这会影响依赖于属性的客户端。所以基本上我们使api的协议尽可能稳定。在这里我们使用DTO

DTO是普通数据结构,用于从服务器上的客户端传输数据,反之亦然,这就是我们称之为数据传输对象的原因。通过创建DTO,我们在重构域模型时减少了对API破坏的可能性。当然,我们应该记住,改变这些DTO可能会产生高昂成本。所以最重要的是我们的api不应该接收或返回Customer模型类对象。

API中使用域对象的另一个问题是我们在应用程序中打开了安全漏洞。黑客可以轻松地在json中传递其他数据,并将它们映射到我们的域对象。如果不应该更新其中一个属性,黑客可以轻易绕过这个,但如果我们使用DTO,我们可以简单地排除可以更新的属性。因此,在项目中添加名为DTO的新文件夹并添加CustomerDTO类,并将Customer域模型类的所有属性及其数据注释属性复制并粘贴到CustomerDTO中。现在从CustomerDTO中删除导航属性,因为它正在创建与MembershipType域模型类的依赖关系。

namespace MyAPI.DTOs{    public class CustomerDTO    {        public int Id { get; set; }        [Required]        [StringLength(255)]        public string Name { get; set; }        public bool IsSubscribedToNewsLetter { get; set; }        public DateTime? Birthdate { get; set; }        public byte MembershipTypeId { get; set; }    }}

接下来我们要在我们的api中使用CustomerDTO而不是Customer域类对象。因此,为了减少大量代码逐个绑定属性,我们使用Automapper

Automapper

Package Manager控制台安装automapper包。

PM > Install-Package Automapper -version:4.1

现在在App_StartMappingProfile.cs)中添加新类并继承Profile

using AutoMapper;namespace MyAPI.App_Start{    public class MappingProfile : Profile    {    }}

现在创建构造函数并在两种类型之间添加映射配置。

public class MappingProfile : Profile{    public MappingProfile()    {        Mapper.CreateMap
();        Mapper.CreateMap
();    }}

CreateMap的第一个参数是Source(源对象),第二个参数是destination(目标对象)。当我们使用CreateMap方法时,automapper会自动使用反射来扫描这些类型。它找到它们的属性并根据它们的名称对它们进行映射。这就是我们调用automapper(基于约定的映射工具)的原因,因为它使用属性名作为映射对象的约定。所以这里是映射配置文件,现在我们需要在应用程序启动时加载它。

现在打开Global.asax.cs文件并编写Application_Start()的代码

protected void Application_Start(){    Mapper.Initialize(c => c.AddProfile
());    GlobalConfiguration.Configure(WebApiConfig.Register);    AreaRegistration.RegisterAllAreas();    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);    RouteConfig.RegisterRoutes(RouteTable.Routes);    BundleConfig.RegisterBundles(BundleTable.Bundles);}

现在打开ApiCustomerController。让我们从这里开始改变。

// GET /api/customerspublic IEnumerable
GetCustomers(){    return _context.Customers.ToList();}

现在我们要返回CustomerDTO类型而不是Customer对象。现在我们需要将此Customer对象映射到CustomerDTO所以我们使用linq扩展方法。

// GET /api/customerspublic IEnumerable
GetCustomers(){    return _context.Customers.ToList()   .Select(Mapper.Map
);}

Mapper.Map <CustomerCustomerDTO>

这个代表进行映射。我们可以看到我们没有放置函数调用小括号,因为我们没有在这里调用函数。我们在这里引用它。映射函数在执行时自动调用。

// GET /api/customers/1public Customer GetCustomer(int id){    var customer = _context.Customers.SingleOrDefault(x => x.Id == id);    // This is part of the RESTful Convention    if (customer == null)    {        throw new HttpResponseException(HttpStatusCode.NotFound);    }    return customer;}

在这个函数中,因为我们返回一个对象所以我们不使用Select扩展方法。这里我们直接使用mapper

// GET /api/customers/1public CustomerDTO GetCustomer(int id){    var customer = _context.Customers.SingleOrDefault(x => x.Id == id);    // This is part of the RESTful Convention    if (customer == null)    {        throw new HttpResponseException(HttpStatusCode.NotFound);    }    return Mapper.Map
(customer);}

现在进行下一个CreateCustomer操作,

// POST /api/customers[HttpPost]public CustomerDTO CreateCustomer(CustomerDTO customerDto){    if (!ModelState.IsValid)    {        throw new HttpResponseException(HttpStatusCode.BadRequest);    }    var customer = Mapper.Map
(customerDto);    _context.Customers.Add(customer);    _context.SaveChanges();    // Here we make our CustomerDto completely fill, because after    // adding customer to Customer table (id is assigned to it)    // & Now we assigned this id to customerDto    customerDto.Id = customer.Id;    return customerDto;}

这就是我们如何使用DtoAutomapper

现在让我们更新UpdateCustomer动作api方法。

// PUT /api/customers/1[HttpPut]public void UpdateCustomer(int id, CustomerDTO customerDto){    if (!ModelState.IsValid)    {        throw new HttpResponseException(HttpStatusCode.BadRequest);    }    var custmr = _context.Customers.SingleOrDefault(x => x.Id == id);    // Might be user sends invalid id.    if (custmr == null)    {        throw new HttpResponseException(HttpStatusCode.NotFound);    }    Mapper.Map
(customerDto, custmr);    //custmr.Birthdate = customerDto.Birthdate;    //custmr.IsSubscribedToNewsLetter = customerDto.IsSubscribedToNewsLetter;    //custmr.Name = customerDto.Name;    //custmr.MembershipTypeId = customerDto.MembershipTypeId;    _context.SaveChanges();}

这就是我们使用Automapper映射对象的方式。现在,automapper有一些在某些情况下可能会发现有用的功能,即如果您的属性名称不匹配,您可以覆盖默认约定,或者您可以从映射中排除某些属性,或者您可能想要创建自定义映射类。如果您想了解更多信息,可以从Automapper文档中学习。

IHttpActionResult

好吧看看这个CreateCustomer操作方法

// POST /api/customers[HttpPost]public CustomerDTO CreateCustomer(CustomerDTO customerDto){    if (!ModelState.IsValid)    {        throw new HttpResponseException(HttpStatusCode.BadRequest);    }    var customer = Mapper.Map
(customerDto);    _context.Customers.Add(customer);    _context.SaveChanges();    // Here we make our CustomerDto completely fill, because after    // adding customerDto to Customer table (id is assigned to it)    // & Now we assigned this id to customerDto    customerDto.Id = customer.Id;    return customerDto;}

在这里,我们只是返回CustomerDto,最终会产生这样的响应

https://www.codeproject.com/KB/webservices/1252477/200.PNG

但是在restful约定中当我们创建资源时,状态代码应为201created所以我们需要更多地控制操作的响应返回并实现这一点,而不是返回CustomerDto,我们返回IHttpActionResult这个接口类似于我们在MVC框架中的ActionResult所以它由几个不同的类实现,这里在ApiController中,我们有很多方法来创建一个实现IHttpActionResult接口的类的实例。

现在,如果模型无效而不是抛出异常,请使用辅助方法BadRequest()

// POST /api/customers[HttpPost]public IHttpActionResult CreateCustomer(CustomerDTO customerDto){    if (!ModelState.IsValid)    {        return BadRequest();    }    var customer = Mapper.Map
(customerDto);    _context.Customers.Add(customer);    _context.SaveChanges();    // Here we make our CustomerDto completely fill, because after    // adding customerDto to Customer table (id is assigned to it)    // & Now we assigned this id to customerDto    customerDto.Id = customer.Id;    return Created(new Uri(Request.RequestUri + "/" + customer.Id),        customerDto);}

我们可以看到如果ModelState是无效的,它返回BadRequest,如果已经添加了customer,那么我们在Created()中使用我们最终创建新对象的对象返回具有此资源IDUri 

https://www.codeproject.com/KB/webservices/1252477/201.PNG

看这里我们创建了另一个资源,现在状态为201 Created如果我们查看位置选项卡

https://www.codeproject.com/KB/webservices/1252477/location.PNG

这就是新创造的客户的uri。这是restful约定的一部分。所以在Web Api中,我们更倾向于使用IHttpActionResult作为操作的返回类型。

现在让我们对此Web Api中的其余操作进行更改。这是我们的Api的完整代码

public class CustomersController : ApiController{    private readonly ApplicationDbContext _context;    public CustomersController()    {        _context = new ApplicationDbContext();    }    // GET /api/customers    public IHttpActionResult GetCustomers()    {        return Ok(_context.Customers.ToList()            .Select(Mapper.Map
));    }    // GET /api/customers/1    public IHttpActionResult GetCustomer(int id)    {        var customer = _context.Customers.SingleOrDefault(x => x.Id == id);        // This is part of the RESTful Convention        if (customer == null) {            return NotFound(); }        return Ok(Mapper.Map
(customer));    }    // POST /api/customers    [HttpPost]    public IHttpActionResult CreateCustomer(CustomerDTO customerDto)    {        if (!ModelState.IsValid) {            return BadRequest(); }        var customer = Mapper.Map
(customerDto);        _context.Customers.Add(customer);        _context.SaveChanges();        // Here we make our CustomerDto completely fill, because after        // adding customerDto to Customer table (id is assigned to it)        // & Now we assigned this id to customerDto        customerDto.Id = customer.Id;        return Created(new Uri(Request.RequestUri + "/" + customer.Id),            customerDto);    }       // PUT /api/customers/1    [HttpPut]    public IHttpActionResult UpdateCustomer(int id, CustomerDTO customerDto)    {        if (!ModelState.IsValid)        {            return BadRequest();        }        var custmr = _context.Customers.SingleOrDefault(x => x.Id == id);        // Might be user sends invalid id.        if (custmr == null)        {            return NotFound();        }        Mapper.Map
(customerDto, custmr);        _context.SaveChanges();        return Ok(custmr);    }       // Delete /api/customers/1    [HttpDelete]    public IHttpActionResult DeleteCustomer(int id)    {        var custmr = _context.Customers.SingleOrDefault(x => x.Id == id);        // Might be user sends invalid id.        if (custmr == null)        {            return NotFound();        }        _context.Customers.Remove(custmr);        // Now the object is marked as removed in memory        // Now it is done        _context.SaveChanges();        return Ok(custmr);    }}

我在这里提一下这一点,因为你可以看到我们在UpdateCustomer中有2个参数。如果参数是原始类型,就像我们是int id那样我们将把这个参数放在路由url或查询字符串中。如果我们想要像我们这样初始化我们的复杂类型我们有CustomerDTO,那么我们将始终从postman中的请求体初始化它。所以不要在这里困扰这件事。

现在让我们通过post更新和删除json对象。如果您专注于UpdateCustomer操作参数,这里我们有第一个参数是记录ID,第二个参数是Customer领域模型类对象。

看它在请求头中使用Id,因为我们的实体在这里完成了。

https://www.codeproject.com/KB/webservices/1252477/working.PNG

但是如果我们不在请求标头中提供id,我们将得到错误。

https://www.codeproject.com/KB/webservices/1252477/Put_error.PNG

exceptionMessage属性'Id'是对象的关键信息的一部分,无法修改。实际上这个异常发生在这一行,

Mapper.Map<CustomerDTO, Customer>(customerDto, custmr);

因为customerDto不包含Id,但custmrCustomer模型类的对象变量)具有Id属性。在这里,我们需要告诉Automapper在映射从customerDtocustmr时忽略Id。所以,来看映射配置文件

public class MappingProfile : Profile{    public MappingProfile()    {        Mapper.CreateMap
();        Mapper.CreateMap
()            .ForMember(c => c.Id, opt => opt.Ignore());    }}

看它现在正在工作,

https://www.codeproject.com/KB/webservices/1252477/created.PNG

使用Web API

经过适当的api测试后,现在是使用api的时候了。最重要的是我想在这里提一下。现在我们的api准备好了,你可以在任何客户端使用这个api。在这里,我们向您展示使用Visual Studio应用程序进行使用的示例。如果你用我们构建这个api,那么你可以在jquery ajax的帮助下在phppython,任何框架应用程序中使用它。现在我们将使用jquery来调用我们的api。看这个屏幕,这里我展示了一些客户。

https://www.codeproject.com/KB/webservices/1252477/view.PNG

现在我们想要的是单击删除按钮删除行。因此,如果您了解我们如何在屏幕上呈现记录项,显然使用foreach循环。所以在删除锚点标签上点击我们还要记录id以将此id传递给web api删除操作并在成功时删除该行。

@foreach (var customer in Model){            @Html.ActionLink(customer.Name, "Edit", "Customers", new { id = customer.Id }, null)        @if (customer.Birthdate != null)        {                            @customer.Birthdate                    }        else        {            Not Available        }                                }

这是html。现在我想通过ajax调用我的api

@section scripts{    }

这就是我们使用ajaxapi的方式。现在你可能在想这里我只是将id传递给客户api并定位Delete操作,在 success事件中我直接删除了行。您可能会以不同的方式思考这种情况,因为每个开发人员都有自己的品味。

您可能会想到,首先我们使用Delete方法删除记录,然后从Web ApiGetCustomers()方法获取所有记录,然后通过jquery的每个循环渲染所有这些项。但这种情况需要花费太多时间和效率。当我点击删除锚标签并在检查浏览器中显示结果时,状态为200 ok。这意味着一切正常,我们的代码(删除操作)正如我们所期望的那样工作。因此,我们不需要再次验证数据库中有多少项并通过每个循环呈现它们。

只需简化您的情况,并像我一样删除记录。

结论

因此,结论是当您使用Web Api时,始终遵循Restful约定。Web Api比基于SOAPWeb服务更加的轻量级。他们是跨平台的。Restful Http动词有助于在应用程序中插入,删除,更新,获取记录。在这里,我们看到我们如何使用postman,并有2个不同的窗格,如请求标头和响应标头。大多数时候,开发人员对如何使用jquery ajax使用Web Api操作感到困惑。在这里我们也使用了这个动作。

 

原文地址:

转载地址:http://qwzhj.baihongyu.com/

你可能感兴趣的文章
iOS组件化实践(基于CocoaPods)
查看>>
【iOS学习】RxSwift从零入手 - 介绍
查看>>
数据结构之栈
查看>>
Elastic Stack简介
查看>>
关于deepin系统安装design compiler的问题解答
查看>>
Java Agent简介及使用Byte Buddy和AspectJ LTW监控方法执行耗时
查看>>
记录一下最近的学习经历
查看>>
hadoop3.0+spark2.0两台云服务器集群环境配置。
查看>>
记第一次面试经历
查看>>
网站实现qq登录(springboot后台)
查看>>
简单的用户头像修改功能(springboot后台)
查看>>
springboot+mybatis实现分页
查看>>
leetcode332. 重新安排行程
查看>>
为什么局域网网段不同不能通信?
查看>>
itchat微信助手,kaggle 电影数据集分析,基于内容的电影推荐
查看>>
认识和使用JWT
查看>>
通过springboot框架,自己动手实现oauth2.0授权码模式认证
查看>>
条件表达式于运算符的点点滴滴的积累
查看>>
最短路径最基本的三种算法【此后无良辰】
查看>>
class的点点滴滴的总结
查看>>