上一章的结尾留下了一个问题:同样是ObjectResult,在执行的时候又是如何被转换成string和JSON两种格式的呢?

本章来解答这个问题,这里涉及到一个名词:“内容协商”。除了这个,本章将通过两个例子来介绍如何自定义IActionResult和格式化类。(ASP.NET Core 系列目录)

一、内容协商

依然以返回Book类型的Action为例,看看它是怎么被转换为JSON类型的。

public Book GetModel()
{
return new Book() { Code = "", Name = "ASP" };
}

这个Action执行后被封装为ObjectResult,接下来就是这个ObjectResult的执行过程。

ObjectResult的代码如下:

public class ObjectResult : ActionResult, IStatusCodeActionResult
{
//部分代码略
public override Task ExecuteResultAsync(ActionContext context)
{
var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<ObjectResult>>();
return executor.ExecuteAsync(context, this);
}
}

它是如何被执行的呢?首先会通过依赖注入获取ObjectResult对应的执行者,获取到的是ObjectResultExecutor,然后调用ObjectResultExecutor的ExecuteAsync方法。代码如下:

public class ObjectResultExecutor : IActionResultExecutor<ObjectResult>
{
//部分代码略
public virtual Task ExecuteAsync(ActionContext context, ObjectResult result)
{
//部分代码略
var formatterContext = new OutputFormatterWriteContext(
context.HttpContext,
WriterFactory,
objectType,
result.Value); var selectedFormatter = FormatterSelector.SelectFormatter(
formatterContext,
(IList<IOutputFormatter>)result.Formatters ?? Array.Empty<IOutputFormatter>(),
result.ContentTypes);
if (selectedFormatter == null)
{
// No formatter supports this.
Logger.NoFormatter(formatterContext);
context.HttpContext.Response.StatusCode = StatusCodes.Status406NotAcceptable;
return Task.CompletedTask;
} result.OnFormatting(context);
return selectedFormatter.WriteAsync(formatterContext);
}
}

核心代码就是FormatterSelector.SelectFormatter()方法,它的作用是选择一个合适的Formatter。Formatter顾名思义就是一个用于格式化数据的类。系统默认提供了4种Formatter,如下图 1

图 1

它们都实现了IOutputFormatter接口,继承关系如下图 2:

图 2

IOutputFormatter代码如下:

public interface IOutputFormatter
{
bool CanWriteResult(OutputFormatterCanWriteContext context);
Task WriteAsync(OutputFormatterWriteContext context);
}

又是非常熟悉的方式,就像在众多XXXResultExecutor中筛选一个合适的Action的执行者一样,首先将它们按照一定的顺序排列,然后开始遍历,逐一执行它们的CanXXX方法,若其中一个的执行结果为true,则它就会被选出来。例如StringOutputFormatter的代码如下:

public class StringOutputFormatter : TextOutputFormatter
{
public StringOutputFormatter()
{
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
SupportedMediaTypes.Add("text/plain");
} public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
} if (context.ObjectType == typeof(string) || context.Object is string)
{
return base.CanWriteResult(context);
} return false;
}
//省略部分代码
}

从StringOutputFormatter的CanWriteResult方法中可以知道它能处理的是string类型的数据。它的构造方法中标识它可以处理的字符集为UTF8和Unicode。对应的数据格式标记为“text/plain”。同样查看HttpNoContentOutputFormatter和HttpNoContentOutputFormatter对应的是返回值为void或者task的,StreamOutputFormatter对应的是Stream类型的。

JsonOutputFormatter没有重写CanWriteResult方法,采用的是OutputFormatter的CanWriteResult方法,代码如下:

public abstract class OutputFormatter : IOutputFormatter, IApiResponseTypeMetadataProvider
{
//部分代码略
protected virtual bool CanWriteType(Type type)
{
return true;
} /// <inheritdoc />
public virtual bool CanWriteResult(OutputFormatterCanWriteContext context)
{
if (SupportedMediaTypes.Count == )
{
var message = Resources.FormatFormatter_NoMediaTypes(
GetType().FullName,
nameof(SupportedMediaTypes)); throw new InvalidOperationException(message);
} if (!CanWriteType(context.ObjectType))
{
return false;
} if (!context.ContentType.HasValue)
{
context.ContentType = new StringSegment(SupportedMediaTypes[]);
return true;
}
else
{
var parsedContentType = new MediaType(context.ContentType); for (var i = ; i < SupportedMediaTypes.Count; i++)
{
var supportedMediaType = new MediaType(SupportedMediaTypes[i]);
if (supportedMediaType.HasWildcard)
{
if (context.ContentTypeIsServerDefined
&& parsedContentType.IsSubsetOf(supportedMediaType))
{
return true;
}
}
else
{
if (supportedMediaType.IsSubsetOf(parsedContentType))
{
context.ContentType = new StringSegment(SupportedMediaTypes[i]);
return true;
}
}
}
} return false;
}
}

通过代码可以看出它主要是利用SupportedMediaTypes和context.ContentType做一系列的判断,它们分别来自客户端和服务端:

SupportedMediaTypes:它是客户端在请求的时候给出的,标识客户端期望服务端按照什么样的格式返回请求结果。

context.ContentType:它来自ObjectResult.ContentTypes,是由服务端在Action执行后给出的。

二者的值都是类似“application/json”、“text/plain”这样的格式,当然也有可能为空,即客户端或服务端未对请求做数据格式的设定。通过上面的代码可以知道,如果这两个值均未做设置或者只有一方做了设置并且设置为JSON时,这个CanWriteResult方法的返回值都是true。所以这样的情况下除了前三种Formatter对应的特定类型外的ObjectResult都会交由JsonOutputFormatter处理。这也就是为什么同样是ObjectResult,但string类型的Action返回结果是String类型,而Book类型的Action返回的结果是JSON类型。这个JsonOutputFormatter有点像当其他的Formatter无法处理时用来“保底”的。

那么SupportedMediaTypes和context.ContentType这两个值又是在什么时候被设置的呢? 在讲请求的模型参数绑定的时候,可以通过在请求Request的Header中添加“content-type: application/json”这样的标识来说明请求中包含的数据的格式是JSON类型的。同样,在请求的时候也可以添加“accept:xxx”这样的标识,来表明期望服务端对本次请求返回的数据的格式。例如期望是JSON格式“accept:application/json”,文本格式“accept: text/plain”等。这个值就是SupportedMediaTypes。

在服务端,也可以对返回的数据格式做设置,例如下面的代码:

 [Produces("application/json")]
public Book GetModel()
{
return new Book() { Code = "", Name = "ASP" };
}

通过这个ProducesAttribute设置的值最终就会被赋值给ObjectResult.ContentTypes,最终传递给context.ContentType。ProducesAttribute实际是一个IResultFilter,代码如下:

public class ProducesAttribute : Attribute, IResultFilter, IOrderedFilter, IApiResponseMetadataProvider
{
//部分代码省略
public virtual void OnResultExecuting(ResultExecutingContext context)
{
//部分代码省略
SetContentTypes(objectResult.ContentTypes);
} public void SetContentTypes(MediaTypeCollection contentTypes)
{
contentTypes.Clear();
foreach (var contentType in ContentTypes)
{
contentTypes.Add(contentType);
}
} private MediaTypeCollection GetContentTypes(string firstArg, string[] args)
{
var completeArgs = new List<string>();
completeArgs.Add(firstArg);
completeArgs.AddRange(args);
var contentTypes = new MediaTypeCollection();
foreach (var arg in completeArgs)
{
var contentType = new MediaType(arg);
if (contentType.HasWildcard)
{
throw new InvalidOperationException( Resources.FormatMatchAllContentTypeIsNotAllowed(arg));
} contentTypes.Add(arg);
} return contentTypes;
}
}

在执行OnResultExecuting的时候,会将设置的“application/json”赋值给ObjectResult.ContentTypes。所以请求最终返回结果的数据格式是由二者“协商”决定的。下面回到Formatter的筛选方法FormatterSelector.SelectFormatter(),这个方法写在DefaultOutputFormatterSelector.cs中。精简后的代码如下:

public class DefaultOutputFormatterSelector : OutputFormatterSelector
{
//部分代码略
public override IOutputFormatter SelectFormatter(OutputFormatterCanWriteContext context, IList<IOutputFormatter> formatters, MediaTypeCollection contentTypes)
{
//部分代码略
var request = context.HttpContext.Request;
var acceptableMediaTypes = GetAcceptableMediaTypes(request);
var selectFormatterWithoutRegardingAcceptHeader = false;
IOutputFormatter selectedFormatter = null;
if (acceptableMediaTypes.Count == )
{
//客户端未设置Accept标头的情况
selectFormatterWithoutRegardingAcceptHeader = true;
}
else
{
if (contentTypes.Count == )
{
//服务端未指定数据格式的情况
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
context,
formatters,
acceptableMediaTypes);
}
else
{
//客户端和服务端均指定了数据格式的情况
selectedFormatter = SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
context,
formatters,
acceptableMediaTypes,
contentTypes);
} if (selectedFormatter == null)
{
//如果未找到合适的,由系统参数ReturnHttpNotAcceptable决定直接返回错误
//还是忽略客户端的Accept设置再筛选一次
if (!_returnHttpNotAcceptable)
{
selectFormatterWithoutRegardingAcceptHeader = true;
}
}
} if (selectFormatterWithoutRegardingAcceptHeader)
{
//Accept标头未设置或者被忽略的情况
if (contentTypes.Count == )
{
//服务端也未指定数据格式的情况
selectedFormatter = SelectFormatterNotUsingContentType(
context,
formatters);
}
else
{
//服务端指定数据格式的情况
selectedFormatter = SelectFormatterUsingAnyAcceptableContentType(
context,
formatters,
contentTypes);
}
} if (selectedFormatter == null)
{
// No formatter supports this.
_logger.NoFormatter(context);
return null;
} _logger.FormatterSelected(selectedFormatter, context);
return selectedFormatter;
} // 4种情况对应的4个方法略
// SelectFormatterNotUsingContentType
// SelectFormatterUsingSortedAcceptHeaders
// SelectFormatterUsingAnyAcceptableContentType
// SelectFormatterUsingSortedAcceptHeadersAndContentTypes
}

DefaultOutputFormatterSelector根据客户端和服务端关于返回数据格式的设置的4种不同情况作了分别处理,优化了查找顺序,此处就不详细讲解了。

总结一下这个规则:

  1. 只有在Action返回类型为ObjectResult的时候才会进行“协商”。如果返回类型为JsonResult、ContentResult、ViewResult等特定ActionResult,无论请求是否设置了accept标识,都会被忽略,会固定返回 JSON、String,Html类型的结果。
  2. 当系统检测到请求是来自浏览器时,会忽略 其Header中Accept 的设置,所以会由服务器端设置的格式决定(未做特殊配置时,系统默认为JSON)。 这是为了在使用不同浏览器使用 API 时提供更一致的体验。系统提供了参数RespectBrowserAcceptHeader,即尊重浏览器在请求的Header中关于Accept的设置,默认值为false。将其设置为true的时候,浏览器请求中的Accept 标识才会生效。注意这只是使该Accept 标识生效,依然不能由其决定返回格式,会进入“协商”阶段。
  3. 若二者均未设置,采用默认的JSON格式。
  4. 若二者其中有一个被设置,采用该设置值。
  5. 若二者均设置且不一致,即二者值不相同且没有包含关系(有通配符的情况),会判断系统参数ReturnHttpNotAcceptable(返回不可接受,默认值为false),若ReturnHttpNotAcceptable值为false,则忽略客户端的Accept设置,按照无Accept设置的情况再次筛选一次Formatter。如果该值为true,则直接返回状态406。

涉及的两个系统参数RespectBrowserAcceptHeader和ReturnHttpNotAcceptable的设置方法是在 Startup.cs 中通过如下代码设置:

 services.AddMvc(
options =>
{
options.RespectBrowserAcceptHeader = true;
options.ReturnHttpNotAcceptable = true;
}
)

最终,通过上述方法找到了合适的Formatter,接着就是通过该Formatter的WriteAsync方法将请求结果格式化后写入HttpContext.Response中。JsonOutputFormatter重写了OutputFormatter的WriteResponseBodyAsync方法(WriteAsync方法会调用WriteResponseBodyAsync方法),代码如下:

public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
} if (selectedEncoding == null)
{
throw new ArgumentNullException(nameof(selectedEncoding));
} var response = context.HttpContext.Response;
using (var writer = context.WriterFactory(response.Body, selectedEncoding))
{
WriteObject(writer, context.Object);
// Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's
// buffers. This is better than just letting dispose handle it (which would result in a synchronous
// write).
await writer.FlushAsync();
}
}

这个方法的功能就是将结果数据转换为JSON并写入HttpContext.Response. Body中。至此,请求结果就按照JSON的格式返回给客户端了。

在实际项目中,如果上述的几种格式均不能满足需求,比如某种数据经常需要通过特殊的格式传输,想自定义一种格式,该如何实现呢?通过本节的介绍,可以想到两种方式,即自定义一种IActionResult或者自定义一种IOutputFormatter。

二、自定义IActionResult

举个简单的例子,以第一节的第3个例子为例,该例通过 “return new JsonResult(new Book() { Code = "1001", Name = "ASP" })”返回了一个JsonResult。

返回的JSON值为:

{"code":"","name":"ASP"}

假如对于Book这种类型,希望用特殊的格式返回,例如这样的格式:

Book Code:[]|Book Name:<ASP>

可以通过自定义一个类似JsonResult的类来实现。代码如下:

public class BookResult : ActionResult
{
public BookResult(Book content)
{
Content = content;
}
public Book Content { get; set; }
public string ContentType { get; set; }
public int? StatusCode { get; set; } public override async Task ExecuteResultAsync(ActionContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
} var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<BookResult>>();
await executor.ExecuteAsync(context, this);
}
}

定义了一个名为BookResult的类,为了方便继承了ActionResult。由于是为了处理Book类型,在构造函数中添加了Book类型的参数,并将该参数赋值给属性Content。重写ExecuteResultAsync方法,对应JsonResultExecutor,还需要自定义一个BookResultExecutor。代码如下:

public class BookResultExecutor : IActionResultExecutor<BookResult>
{
private const string DefaultContentType = "text/plain; charset=utf-8";
private readonly IHttpResponseStreamWriterFactory _httpResponseStreamWriterFactory; public BookResultExecutor(IHttpResponseStreamWriterFactory httpResponseStreamWriterFactory)
{
_httpResponseStreamWriterFactory = httpResponseStreamWriterFactory;
} private static string FormatToString(Book book)
{
return string.Format("Book Code:[{0}]|Book Name:<{1}>", book.Code, book.Name);
} /// <inheritdoc />
public virtual async Task ExecuteAsync(ActionContext context, BookResult result)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
} if (result == null)
{
throw new ArgumentNullException(nameof(result));
} var response = context.HttpContext.Response;
string resolvedContentType;
Encoding resolvedContentTypeEncoding;
ResponseContentTypeHelper.ResolveContentTypeAndEncoding(
result.ContentType,
response.ContentType,
DefaultContentType,
out resolvedContentType,
out resolvedContentTypeEncoding);
response.ContentType = resolvedContentType; if (result.StatusCode != null)
{
response.StatusCode = result.StatusCode.Value;
} string content = FormatToString(result.Content);
if (result.Content != null)
{
response.ContentLength = resolvedContentTypeEncoding.GetByteCount(content);
using (var textWriter = _httpResponseStreamWriterFactory.CreateWriter(response.Body, resolvedContentTypeEncoding))
{
await textWriter.WriteAsync(content);
await textWriter.FlushAsync();
}
}
}
}

这里定义了默认的ContentType 类型,采用了文本格式,即"text/plain; charset=utf-8",这会在请求结果的Header中出现。为了特殊说明这个格式,也可以自定义一个特殊类型,例如"text/book; charset=utf-8",这需要项目中提前约定好。定义了一个FormatToString方法用于将Book类型的数据格式化。最终将格式化的数据写入Response.Body中。

这个BookResultExecutor定义之后,需要在依赖注入中(Startup文件中的ConfigureServices方法)注册:

public void ConfigureServices(IServiceCollection services)
{
//省略部分代码
services.TryAddSingleton<IActionResultExecutor<BookResult>, BookResultExecutor>();
}

至此,这个自定义的BookResult就可以被使用了,例如下面代码所示的Action:

public BookResult GetBookResult()
{
return new BookResult(new Book() { Code = "", Name = "ASP" });
}

用Fiddler访问这个Action测试一下,返回结果如下:

Book Code:[]|Book Name:<ASP>

Header值:

Content-Length:
Content-Type: text/book; charset=utf-

这是自定义了Content-Type的结果。

三、 自定义格式化类

对于上一节的例子,也可以对照JsonOutputFormatter来自定义一个格式化类来实现。将新定义一个名为BookOutputFormatter的类,也如同JsonOutputFormatter一样继承TextOutputFormatter。代码如下:

public class BookOutputFormatter : TextOutputFormatter
{
public BookOutputFormatter()
{
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
SupportedMediaTypes.Add("text/book");
} public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
} if (context.ObjectType == typeof(Book) || context.Object is Book)
{
return base.CanWriteResult(context);
} return false;
} private static string FormatToString(Book book)
{
return string.Format("Book Code:[{0}]|Book Name:<{1}>",book.Code,book.Name);
} public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
} if (selectedEncoding == null)
{
throw new ArgumentNullException(nameof(selectedEncoding));
} var valueAsString = FormatToString(context.Object as Book);
if (string.IsNullOrEmpty(valueAsString))
{
await Task.CompletedTask;
} var response = context.HttpContext.Response;
await response.WriteAsync(valueAsString, selectedEncoding);
}
}

首先在构造函数中定义它所支持的字符集和Content-type类型。重写CanWriteResult方法,这是用于确定它是否能处理对应的请求返回结果。可以在此方法中做多种判断,最终返回bool类型的结果。本例比较简单,仅是判断返回的结果是否为Book类型。同样定义了FormatToString方法用于请求结果的格式化。最后重写WriteResponseBodyAsync方法,将格式化后的结果写入Response.Body中。

BookOutputFormatter定义之后也需要注册到系统中去,例如如下代码:

 services.AddMvc(
options =>
{
options.OutputFormatters.Insert(,new BookOutputFormatter());
}
)

这里采用了Insert方法,也就是将其插入了OutputFormatters集合的第一个。所以在筛选OutputFormatters的时候,它也是第一个。此时的OutputFormatters如下图 3

图 3

通过Fiddler测试一下,以第一节返回Book类型的第4个例子为例:

public Book GetModel()
{
return new Book() { Code = "", Name = "ASP" };
}

当设定accept: text/book或者未设定accept的时候,采用了自定义的BookOutputFormatter,返回结果为:

Book Code:[]|Book Name:<ASP>

Content-Type值是:Content-Type: text/book; charset=utf-8。

当设定accept: application/json的时候,返回JSON,值为:

{"code":"1001","name":"ASP"}

Content-Type值是:Content-Type: application/json; charset=utf-8。

这是由于BookOutputFormatter类型排在了JsonOutputFormatter的前面,所以对于Book类型会首先采用BookOutputFormatter,当客户端通过Accept方式要求返回结果为JSON的时候,才采用了JSON类型。测试一下服务端的要求。将这个Action添加Produces设置,代码如下:

 [Produces("application/json")]
public Book GetModel()
{
return new Book() { Code = "", Name = "ASP" };
}

此时无论设定accept: text/book或者未设定accept的情况,都会按照JSON的方式返回结果。这也验证了第二节关于服务端和客户端“协商”的规则。

四、添加XML类型支持

第三、四节通过自定义的方式实现了特殊格式的处理,在项目中常见的格式还有XML,这在ASP.NET Core中没有做默认支持。如果需要XML格式的支持,可以通过NuGet添加相应的包。

在NuGet中搜索并安装Microsoft.AspNetCore.Mvc.Formatters.Xml,如下图 4

图 4

不需要像BookOutputFormatter那样都注册方式,系统提供了注册方法:

services.AddMvc().AddXmlSerializerFormatters();

或者

services.AddMvc().AddXmlDataContractSerializerFormatters();

分别对应了两种格式化程序:

System.Xml.Serialization.XmlSerializer;
System.Runtime.Serialization.DataContractSerializer;

二者的区别就不在这里描述了。注册之后,就可以通过在请求的Header中通过设置“accept: application/xml”来获取XML类型的结果了。访问上一节的返回结果类型为Book的例子,返回的结果如下:

<Book xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Code></Code>
<Name>ASP</Name>
</Book>

Content-Type值是:Content-Type: application/xml; charset=utf-8。

ASP.NET Core 2.2 : 二十一. 内容协商与自定义IActionResult和格式化类的更多相关文章

  1. ASP.NET Core 2.2 : 二十. Action的多数据返回格式处理机制

    上一章讲了系统如何将客户端提交的请求数据格式化处理成我们想要的格式并绑定到对应的参数,本章讲一下它的“逆过程”,如何将请求结果按照客户端想要的格式返回去. 一.常见的返回类型 以系统模板默认生成的Ho ...

  2. ASP.NET Core学习之二 菜鸟踩坑

    对于像我这样没接触过core的人,坑还是比较多的,一些基础配置和以前差别很大,这里做下记录 一.Startup 1.注册服务 // This method gets called by the run ...

  3. 学习ASP.NET Core Razor 编程系列十一——把新字段更新到数据库

    学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...

  4. ASP.NET Core 2.2 : 二十六. 应用JWT进行用户认证

    本文将通过实际的例子来演示如何在ASP.NET Core中应用JWT进行用户认证以及Token的刷新方案(ASP.NET Core 系列目录) 一.什么是JWT? JWT(json web token ...

  5. ASP.NET Core 2.2 : 二十七. JWT与用户授权(细化到Action)

    上一章分享了如何在ASP.NET Core中应用JWT进行用户认证以及Token的刷新,本章继续进行下一步,用户授权.涉及到的例子也以上一章的为基础.(ASP.NET Core 系列目录) 一.概述 ...

  6. ASP.NET Core 2.2 : 二十二. 多样性的配置方式

    大多数应用都离不开配置,本章将介绍ASP.NET Core中常见的几种配置方式及系统内部实现的机制. 说到配置,第一印象可能就是“.config”类型的xml文件或者“.ini”类型的ini文件,在A ...

  7. ASP.NET Core 2.2 : 二十三. 深入聊一聊配置的内部处理机制

    上一章介绍了配置的多种数据源被注册.加载和获取的过程,本节看一下这个过程系统是如何实现的.(ASP.NET Core 系列目录) 一.数据源的注册 在上一节介绍的数据源设置中,appsettings. ...

  8. ASP.NET Core 3.0 : 二十五. TagHelper

    什么是TagHelper?这是ASP.NET Core 中新出现的一个名词,它的作用是使服务器端代码可以在Razor 文件中参与创建和呈现HTML 元素.(ASP.NET Core 系列目录) 一.概 ...

  9. ASP.NET Core 3.0 : 二十四. 配置的Options模式

    上一章讲到了配置的用法及内部处理机制,对于配置,ASP.NET Core还提供了一种Options模式.(ASP.NET Core 系列目录) 一.Options的使用 上一章有个配置的绑定的例子,可 ...

随机推荐

  1. 28岁,转行学 IT 靠谱吗?

    前几天在知乎上,刷到这么一个问题 鉴于有不少人看了我的blog给我私信一些职业规划相关的问题,讨论很多的就是担心自己年龄是否还适合转行. 于是决定静心下来码了一篇回答, 同时搬到博客园来供大家消遣.. ...

  2. alpine 镜像 java 日志中文问号乱码

    0x00 前言 吾使用 alpine 作为基础镜像构建了 jdk8 镜像,为线上业务的 java 微服务架构提供支持,但是有容器运行的 java 服务中打印的日志中一旦出现中文,就会出现诸如以下的 ? ...

  3. python中如何调用函数交换两个变量的值

    python中如何调用函数交换两个变量的值 所有代码来在python3.7.1版本实现 以下实例通过用户输入两个变量,并相互交换:  方法一: def swap(a,b): # 创建临时变量,并交换 ...

  4. Unity进阶之ET网络游戏开发框架 02-ET的客户端启动流程分析

    版权申明: 本文原创首发于以下网站: 博客园『优梦创客』的空间:https://www.cnblogs.com/raymondking123 优梦创客的官方博客:https://91make.top ...

  5. Nginx在linux下安装及简单命令

    安装环境:Centos7 创建目录及切换至目录 # mkdir /usr/local/nginx # cd /usr/local/nginx/ 下载nginx包,访问http://nginx.org下 ...

  6. web项目jsp中无法引入js问题

    https://blog.csdn.net/C1042135353/article/details/80274685#commentBox 这篇文章超赞的,几个小时的时间看了这篇文章豁然开朗,瞬间懂了 ...

  7. spring-boot-plus项目目录结构(六)

    spring-boot-plus项目目录结构 目录结构 bin:启动/重启命令脚本目录 logs:部署后记录日志目录 assembly:maven打包配置文件目录 java:源代码目录 resourc ...

  8. WPF DataGrid点击列头选择全列并具有背景色

    完成这个操作,主要是XAML的代码. 主要思路是通过绑定多路数据,在多路转换器中返回布尔值,在通过数据触发器来设置被选择的全列的背景色. XAML页面主要代码 首先定义DataGridCell < ...

  9. 利用SSH端口转发实现远程访问内网主机远程桌面(一) 建立SSH转发

    近期家里更换了移动的宽带,拨号后拿到的是10开头的内网IP,就不能像之前一样通过路由器的端口映射实现从外网访问主机的远程桌面.这种情况下可以利用一台具有公网IP的服务器充当中转,利用SSH的隧道转发功 ...

  10. 一文了解:Redis主从复制

    Redis主从复制 主从复制 主从复制,将一台Redis服务器的数据,复制到其他Redis服务器.前者称为主(master)节点,后者称为从(slave)节点 . 在默认的情况下,Redis都是主节点 ...