2023年4月

1. 缓存

缓存指的是在软件应用运行过程中,将一些数据生成副本直接进行存取,而不是从原始源(数据库,业务逻辑计算等)读取数据,减少生成内容所需的工作,从而显著提高应用的性能和可伸缩性,使用好缓存技术,有利于提高我们提升用户体验性。

对于缓存的使用有以下一些注意点:

  • 缓存最适用于不常更改且生成成本很高的数据。
  • 代码应始终具有回退选项,以提取数据,而不依赖于可用的缓存值。
    我们应该以从不依赖于缓存数据的方式编写和测试应用。缓存是会失效的,我们在进行应用开发时应该考虑到缓存失效的情况,提供缓存失效时按照正常逻辑获取相关数据的方式。
  • 缓存使用短缺资源:内存。 我们应该限制缓存增长:
    • 不要将外部输入插入到缓存中。 例如,不建议使用用户提供的任意输入作为缓存键,因为输入可能会消耗不可预测的内存量。
    • 使用过期限制缓存增长。
    • 应当限制缓存的大小,避免缓存过度增长

软件开发中对缓存的使用一般有两种情况,一种是内存缓存,一种是分布式缓存。

2. NET Core 的内存缓存

内存缓存是最简单的一种缓存方式,就是使用应用所在的服务器的内存来保存一些数据副本,利用内存读写比磁盘、网络请求快的特点来提供应用性能。内存缓存一般应用于单机应用,一旦应用重启,内存缓存中的数据就会丢失。

如果是在服务器场(多个服务器)中运行的应用使用内存缓存,应确保在使用内存中缓存时会话是粘滞的。 粘滞会话可确保来自客户端的请求都转到同一服务器。

2. 1 内存缓存启用

.NET Core 框架下对于内存缓存的使用是通过 IMemoryCache ,可以通过将其注册到容器中,之后在需要的地方注入使用。对于大多数应用,在 Program.cs 中调用许多其他 Add{Service} 方法可以启用 IMemoryCache,例如 AddMvc、AddControllersWithViews、AddRazorPages、AddMvcCore().AddRazorViewEngine 等 。

如果应用中没有使用到上述的这些方法,我们也可以自行引入
Microsoft.Extensions.Caching.Memory
Nuget 包,通过
AddMemoryCache
方法启动内存缓存。为了方便演示,以下示例使用 .NET 6 控制台应用。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

Host.CreateDefaultBuilder(args)
	.ConfigureServices(services =>
	{
		services.AddMemoryCache();
	})
	.Build().Run();

2. 2 内存缓存基本用法

内存缓存的使用,只需要将 IMemoryCache 服务注入到类中进行使用即可。我们可以通过 TryGetValue 方法尝试从缓存中获取数据,通过 Set 方法向缓存中添加数据。

public interface ICacheService
{
	public void PrintDateTimeNow();
}

public class CacheService : ICacheService
{
	public const string CacheKey = "CacheTime";
	private readonly IMemoryCache _cache;
	public CacheService(IMemoryCache memoryCache)
	{
		_cache = memoryCache;
	}

	public void PrintDateTimeNow()
	{
		var time = DateTime.Now;
		if(!_cache.TryGetValue(CacheKey, out DateTime cacheValue))
		{
			cacheValue = time;
			_cache.Set(CacheKey, cacheValue);
		}
		time = cacheValue;

		Console.WriteLine("缓存时间:" + time.ToString("yyyy-MM-dd HH:mm:ss"));
		Console.WriteLine("当前时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
	}
}

using CacheSample;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder(args)
	.ConfigureServices(services =>
	{
		services.AddMemoryCache();
		services.AddTransient<ICacheService, CacheService>();
	})
	.Build();

var service = host.Services.GetRequiredService<ICacheService>();
service.PrintDateTimeNow();

Task.Delay(TimeSpan.FromSeconds(2)).Wait();

service.PrintDateTimeNow();

host.Run();

通过控制台打印结果,可以看到,当前时间已经改变,但是缓存的时间是之前的数据。

image

缓存系统将缓存项存储为键值对,内存缓存中键和值都可以是任意类型,不过一般情况下我们会将字符串作为键。之后会讲到的分布式缓存中,则要求值必须是 byte[] 类型。

除此之外,还可以通过 GetOrCreate 或 GetOrCreateAsync 将获取和添加的操作结合。

public class CacheService : ICacheService
{
	public const string CacheKey = "CacheTime";
	private readonly IMemoryCache _cache;
	public CacheService(IMemoryCache memoryCache)
	{
		_cache = memoryCache;
	}

	public void PrintDateTimeNow()
	{
		//var time = DateTime.Now;
		//if(!_cache.TryGetValue(CacheKey, out DateTime cacheValue))
		//{
		//    cacheValue = time;
		//    _cache.Set(CacheKey, cacheValue);
		//}
		//time = cacheValue;

		var time = _cache.GetOrCreate(CacheKey, cacheEntry =>
		{
			return DateTime.Now;
		});

		Console.WriteLine("缓存时间:" + time.ToString("yyyy-MM-dd HH:mm:ss"));
		Console.WriteLine("当前时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
	}
}

image

除了 TryGetValue 方法外,如果你确定缓存中一定存在相应的数据,还可以通过 Get 方法获取数据,Get 方法支持泛型,可以直接进行类型转换,但是如果缓存中不存在该缓存项,则会返回对应类型的默认值。

var timeCache = _cache.Get<DateTime>(CacheKey);![image]

2.3 缓存过期设置

一般情况下,我们会对缓存数据设置过期时间,一个是为将一些长期未被访问的缓存条目移除,避免缓存过度增长,一方面是为了更新数据,避免长时间的数据副本和源数据不一致。

.NET Core 下内存缓存的过期时间设置可以通过以下的方式:

(1) 通过 Set 方法设置

var time = DateTime.Now;
if (!_cache.TryGetValue(CacheKey, out DateTime cacheValue))
{
	cacheValue = time;
	// 设置绝对过期时间
	// 两种实现的功能是一样的,只是时间设置的方式不同而已
	// 传入的是 AbsoluteExpirationRelativeToNow, 相对当前的绝对过期时间,传入时间差,会根据当前时间算出绝对过期时间
	_cache.Set(CacheKey, cacheValue, TimeSpan.FromSeconds(2));
	// 传入的是 AbsoluteExpiration,绝对过期时间,传入一个DateTimeOffset对象,需要明确的指定具体的时间
	// _cache.Set(CacheKey, cacheValue, DateTimeOffset.Now.AddSeconds(2));
}
time = cacheValue;

调整一下入口文件的代码,如下:

var service = host.Services.GetRequiredService<ICacheService>();
service.PrintDateTimeNow();

Task.Delay(TimeSpan.FromSeconds(1)).Wait();
service.PrintDateTimeNow();

Task.Delay(TimeSpan.FromSeconds(2)).Wait();
service.PrintDateTimeNow();

可以看到,在第二次输出的时候,缓存没过期,时间是不变的,第三次的时候缓存过期了,时间改变了。

image

对于缓存过期时间的设置,除了绝对过期时间,还有缓动过期时间。滑动过期时间是指,如果在规定的过期时间内缓存有被再一次调用,过期时间就会重新更新,从头开始计算,每次被调用都会重新开始。

Set 方法没有直接的参数设置滑动过期时间,只能通过 MemoryCacheEntryOptions 对象设置,当然相对过期时间等其他配置也可以通过该对象设置。

var memoryCacheEntryOption = new MemoryCacheEntryOptions();
// 滑动过期时间是一个相对时间
memoryCacheEntryOption.SlidingExpiration = TimeSpan.FromSeconds(3);
_cache.Set(CacheKey, cacheValue, memoryCacheEntryOption);

image

可以看到,缓存时间一直没有变,因为虽然三次输出时间加起来超过了三秒,但是三次输出之间的间隔都没有超过3秒,而每调用一次缓存都会刷新超时时间,所以缓存一直没有过期。

(2) 通过 CreateEntry 方法设置

var time = DateTime.Now;
if (!_cache.TryGetValue(CacheKey, out DateTime cacheValue))
{
	cacheValue = time;
	var entry = _cache.CreateEntry(CacheKey);
	// 设置绝对过期时间
	// 两种实现的功能是一样的,只是时间设置的方式不同而已
	// 传入的是 AbsoluteExpirationRelativeToNow, 相对当前的绝对过期时间,传入时间差,会根据当前时间算出绝对过期时间
	entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(2);
	// 传入的是 AbsoluteExpiration,绝对过期时间,传入一个DateTimeOffset对象,需要明确的指定具体的时间
	entry.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(2);
	entry.Value = cacheValue;
}
time = cacheValue;

(3) 通过 GetOrCreate 或 GetOrCreateAsync 方法设置

var time = _cache.GetOrCreate(CacheKey, cacheEntry =>
{
	// 两种实现的功能是一样的,只是时间设置的方式不同而已
	// 相对当前的绝对过期时间,传入时间差,会根据当前时间算出绝对过期时间
	cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(3);
	// 绝对过期时间,传入一个DateTimeOffset对象,需要明确的指定具体的时间
	// cacheEntry.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(2);
	// 滑动过期时间是一个相对时间
	cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
	return DateTime.Now;
});

GetOrCreate

GetOrCreateAsync
方法的回调参数其实就是实现了 ICacheEntry 接口的对象。

这里同时设置了绝对过期时间和滑动过期时间,对于一个缓存项,仅具有滑动过期时间的缓存项集有永不过期的风险。 如果在滑动过期间隔内重复访问缓存项,则该项永远不会过期。 将滑动过期与绝对过期相结合,以确保项目过期。 绝对过期时间设置一个上限,即在滑动过期间隔内未请求该项时,仍允许该项提前过期的时间。 如果经过了可调到期间隔或绝对到期时间,则会从缓存中逐出项。

可以看到输出结果如下:

image

第三次调用的时候,时间改变了,这是因为输入滑动过期时间一直在更新,但是绝对过期时间超过了,所以缓存失效了。

这一篇就先到这里,后面还有内容,但是考虑到如果全塞在一篇里面的话,这篇文章就太长了,大家的阅读体验会不大好,所以就拆成两篇了。



参考文章:
ASP.NET Core 中的内存中缓存



ASP.NET Core 系列:

目录:
ASP.NET Core 系列总结
上一篇:
ASP.NET Core - 选项系统之源码介绍

环境:
Oracle 19c ADG(主库:单实例;备库:RAC)

1.主库新建测试文件

主库在AWR的PDB中做测试,为了不影响其他测试,创建一个新的测试表空间tbs_test及对应数据文件:

SQL> conn awr@awr
Enter password:
Connected.
SQL> create tablespace tbs_test datafile '/flash/oradata/DEMO/awr/tbs_test01.dbf' size 30m;

Tablespace created.

2.主库创建测试表

主库在新建表空间上创建测试表awr.test:

SQL> create table awr.test tablespace tbs_test as select * from dba_users;

Table created.

SQL> select count(*) from awr.test;

  COUNT(*)
----------
	37

3.查询表对应数据文件信息

通过dbms_rowid查看awr.test表对应行数据的文件号(rel_fno)、块号(blockno,)和行号(rowno):

select rowid, 
     dbms_rowid.rowid_relative_fno(rowid) rel_fno,        
     dbms_rowid.rowid_block_number(rowid) blockno,  
     dbms_rowid.rowid_row_number(rowid) rowno 
from awr.test    
order by rowid;

ROWID		      REL_FNO	 BLOCKNO      ROWNO
------------------ ---------- ---------- ----------
AAATR2AAdAAAACDAAA	   29	     131	  0
AAATR2AAdAAAACDAAB	   29	     131	  1
AAATR2AAdAAAACDAAC	   29	     131	  2
AAATR2AAdAAAACDAAD	   29	     131	  3
AAATR2AAdAAAACDAAE	   29	     131	  4
AAATR2AAdAAAACDAAF	   29	     131	  5
AAATR2AAdAAAACDAAG	   29	     131	  6
AAATR2AAdAAAACDAAH	   29	     131	  7
AAATR2AAdAAAACDAAI	   29	     131	  8
AAATR2AAdAAAACDAAJ	   29	     131	  9
AAATR2AAdAAAACDAAK	   29	     131	 10
...

4.模拟数据文件物理坏块

使用dd模拟数据文件的物理坏块:

dd if=/dev/zero of=/flash/oradata/DEMO/awr/tbs_test01.dbf bs=8192 conv=notrunc seek=131 count=1

5.查询对应测试表

再次查询被破坏数据文件上的表awr.test,发现客户端只是卡顿一下就正常出了结果,并没有任何显示的报错:

ALTER SYSTEM Flush buffer_cache;
select count(*) from awr.test;

6.进一步查询日志信息

上面查询表没有报错,但是从主库的alert日志中可以看到:

2023-04-03T12:08:02.602504+08:00
AWR(6):create tablespace tbs_test datafile '/flash/oradata/DEMO/awr/tbs_test01.dbf' size 30m
AWR(6):Completed: create tablespace tbs_test datafile '/flash/oradata/DEMO/awr/tbs_test01.dbf' size 30m
2023-04-03T12:15:21.021834+08:00
AWR(6):ALTER SYSTEM: Flushing buffer cache inst=0 container=6 global
AWR(6):TABLE AUDSYS.AUD$UNIFIED: ADDED INTERVAL PARTITION SYS_P368 (106) VALUES LESS THAN (TIMESTAMP' 2023-05-01 00:00:00')
2023-04-03T12:15:24.751443+08:00
AWR(6):Hex dump of (file 29, block 131) in trace file /u01/app/oracle/diag/rdbms/demo/demo/trace/demo_ora_11735.trc
AWR(6):
AWR(6):Corrupt block relative dba: 0x07400083 (file 29, block 131)
AWR(6):Completely zero block found during multiblock buffer read
AWR(6):
AWR(6):Reading datafile '/flash/oradata/DEMO/awr/tbs_test01.dbf' for corrupt data at rdba: 0x07400083 (file 29, block 131)
AWR(6):Reread (file 29, block 131) found same corrupt data (no logical check)
AWR(6):Starting background process ABMR
2023-04-03T12:15:24.763408+08:00
Corrupt Block Found
         TIME STAMP (GMT) = 04/03/2023 12:15:24
         CONT = 6, TSN = 5, TSNAME = TBS_TEST
         RFN = 29, BLK = 131, RDBA = 121634947
         OBJN = 78966, OBJD = 78966, OBJECT = TEST, SUBOBJECT =
         SEGMENT OWNER = AWR, SEGMENT TYPE = Table Segment
2023-04-03T12:15:24.766521+08:00
ABMR started with pid=132, OS id=11983
2023-04-03T12:15:24.767751+08:00
Automatic block media recovery service is active.
2023-04-03T12:15:24.767981+08:00
AWR(6):Automatic block media recovery requested for (file# 29, block# 131)
2023-04-03T12:15:27.096763+08:00
Automatic block media recovery successful for (file# 29, block# 131)
2023-04-03T12:15:27.097189+08:00
AWR(6):Automatic block media recovery successful for (file# 29, block# 131)

日志中显示自动启用了ABMR(Automatic block media recovery)成功修复了物理坏块。

7.确认当前参数设置

如果查询 db_block_checking 、 db_lost_write_protect 这些参数,会发现我这里并没有去特殊设置:

SQL> show parameter db_block

NAME				     TYPE	 VALUE
------------------------------------ ----------- ------------------------------
db_block_buffers		     integer	 0
db_block_checking		     string	 FALSE
db_block_checksum		     string	 TYPICAL
db_block_size			     integer	 8192
SQL> show parameter db_lost

NAME				     TYPE	 VALUE
------------------------------------ ----------- ------------------------------
db_lost_write_protect		     string	 NONE
SQL>

那么那些参数的意义呢?其实MOS文档:

  • Best Practices for Corruption Detection, Prevention, and Automatic Repair - in a Data Guard Configuration (Doc ID 1302539.1)

文档中有说明,物理坏块默认ADG就能检测,逻辑坏块要配合这些参数设置。包括上一步的日志信息中,在发现数据损坏时,也标注了
(no logical check)
非逻辑检查的提示。

当然,如果您想要获得更全面的保护,还是要按文档说明,额外设置这些参数。


本博文介绍CSS3中新增的选择器,包括属性选择器、结构伪类选择器和伪元素选择器。

1 属性选择器

属性选择器([属性])可以根据元素的属性和属性值来对符合要求的元素进行选择。

属性选择器的
基础语法
如下表:

语法 说明
标签[属性] {} 选择有目标属性的标签
标签[属性="value"] {} 选择有目标属性且属性值为"value"的标签
标签[属性^="x"] {} 选择有目标属性且属性值以"x"开头的标签
标签[属性$="x"] {} 选择有目标属性且属性值以"x"结尾的标签
标签[属性*="x"] {} 选择有目标属性且属性值包含"x"的标签

举两个实例:

① input[value] {} :选择具有value属性的input标签

② input[value="123"] {} :选择value属性值为"123"的input标签

注意事项:

(1)属性选择器(即[属性])的权重是0,0,1,0;

(2)标签[属性] 的权重是标签权重+属性选择器权重。

2 结构伪类选择器(上)

首先是第一类常用的结构伪类选择器,这类选择器常用于根据父级选择器来选择里面的子元素。

基础语法
如下表:

语法 说明
E:first-child 选择父元素中的第一个子元素,若该元素为E,则选中,否则选择器不生效
E:last-child 选择父元素中的最后一个子元素,若该元素为E,则选中,否则选择器不生效
E:nth-child(n) 根据n来选择父元素中的子元素,若选中的子元素为E,则选中,否则选择器不生效

注意,这类选择器的
选择步骤
如下:

(1)先给
所有子元素
从1开始
进行编号;

(2)根据选择器来进行选择。如:E:first-child就选择第一个子元素,如果这个子元素是E的话,那么就选中了;但如果第一个子元素不是E的话,那么这类选择器就不会生效。

总之,这类选择器是“
先编号,再选择,选择的元素为E,则选中
”。

可能语法太模糊?上实例:

① ul li:first-child :选择 ul 下的第1个子元素,若该元素为li,则选中该元素,否则不生效;

② ul li:nth-child(6)  :选择 ul 下的第6个子元素,若该元素为li,则选中该元素,否则不生效;

③ ul li:nth-child(2n) :选择 ul 下的所有第偶数个子元素(2n即为偶数),若其为 li 则选中。

接着,这里还要对
E:nth-child(n)
这一基本语法按照 n 的分类进行详细的说明:

E:nth-child(n)中 n 的类型 说明(选中的子元素为E则生效)
数字 选择第n个子元素
关键字"even" 选择所有第偶数个子元素
关键字"odd" 选择所有第奇数个子元素
公式 根据公式进行选择。注意:公式中的 n
从0开始计算
,但子元素是
从1开始计数
的,因此第0个子元素是不存在的

当E:nth-child(n)中 n 的类型为“公式”时,提供以下实例供理解(
n 从0开始计算
):

选择器 说明(选中的子元素为E则生效)
E:nth-child(n) 选择从第0个开始的所有子元素
E:nth-child(n+3) 选择从第3个开始的所有子元素
E:nth-child(2n) 选择所有第偶数个子元素
E:nth-child(2n+1) 选择所有第奇数个子元素
E:nth-child(-n+3) 选择前3个子元素

实际上,在日常使用中,由于nth-child(n)会给所有子元素进行编号(不管是不是E),因此E:nth-child(n) 中 n 的值和 E 作为子元素的位置往往是对应的(这样选择器才会生效)。

3 结构伪类选择器(下)

接着是第二类常用的结构伪类选择器,这类选择器也用于根据父级选择器来选择里面的子元素,但和第一类有些差别。

基础语法
如下表:

语法 说明
E:first-of-type 选择父元素中的第一个子元素E
E:last-of-type 选择父元素中的最后一个子元素E
E:nth-of-type(n) 根据n来选择父元素中的子元素E

注意,这类选择器的
选择步骤
如下:

(1)先给
所有子元素E
从1开始
进行编号;

(2)根据选择器来进行选择。如:E:first-of-type就选择第一个子元素E。

直接上实例:

① div p:first-of-type :选择 div 下的第1个子元素p;

② div p:nth-of-type(2) :选择 div 下的第2个子元素p。

4 结构伪类选择器的比较

结构伪类选择器 说明
不同点 :first-child、:last-child、:nth-child(n) 先给
所有子元素
编号,再选择,选中若是E则选择器有效,否则无效
:first-of-type、:last-of-type、:nth-of-type(n) 先给
所有子元素E
编号,再选择,选中一定是E
共同点 结构伪类选择器(如: first-child)的权重是0,0,1,0 E:first-child的权重是 E 的权重 + 0,0,1,0

5 伪元素选择器

伪元素选择器可以利用css创建新标签,简化HTML结构。

伪元素选择器的
基础语法
如下表:

语法 说明
E::before 在元素E内部的前面插入内容
E::after 在元素E内部的后面插入内容

需要注意的地方如下:

(1)创建的标签属于行内元素;

(2)新创建的元素在文档树中找不到;

(3)before和after必须有content属性;

(4)伪元素选择器(::before 和 ::after)权重为0,0,0,1。

提供一个实例:

<!DOCTYPE html>
<htmllang="en">
<head>
    <metacharset="UTF-8">
    <metahttp-equiv="X-UA-Compatible"content="IE=edge">
    <metaname="viewport"content="width=, initial-scale=1.0">
    <title>Document</title>
    <style>div::before{content:"前面的";
        }div::after{content:"后面的";
        }
    </style>
</head>
<body>
    <div>选择器</div>
</body>
</html>

页面效果如下:

可见,通过伪元素选择器,div元素中的文字“选择器”前后分别添加了文字“前面的”和“后面的”,这就是::before和::after的基础用法。

前几天写了篇关于fastjson的文章,
《fastjson很好,但不适合我》
。里面探讨到关于对象循环引用的序列化问题。作为spring序列化的最大竞品,在讨论fastjson的时候肯定要对比一下jackson的。所以我也去测试了一下Jackson在对象循环引用的序列化的功用,然后有了一点意外的小发现,在这里跟大家讨论一下。


首先还得解释一下,jackson的序列化是怎么跟@ControllerAdvice关联上的呢?
前篇文章里说过,对于对象循环引用的序列化问题,fastjson和jackson分别采取了两种态度,fastjson是默认处理了,而jackson是默认抛出异常。后者把主动权交给了用户。
既然这里抛出了异常,就涉及到异常的全局处理,跟事务一样,我们不可能以硬编码的方式在每个方法里分别处理异常,而是通过统一全局异常处理。


@ControllerAdvice 全局异常捕获

这里简单的做一下介绍,嫌弃啰嗦的朋友可直接略过,跳到第2部份。

Spring家族中,通过注解@ControllerAdvice或者 @RestControllerAdvice 即可开启全局异常处理,使用该注解表示开启了全局异常的捕获,我们只需在自定义一个方法使用@ExceptionHandler注解然后定义捕获异常的类型即可对这些捕获的异常进行统一的处理。

只要异常最终能够到达controller层,且与@ExceptionHandler定义异常类型相匹配,就能被捕获。

@RestControllerAdvice
public class GlobalExceptionHandler {

    Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(value = Exception.class)
    public Result exceptionHandler(Exception e){
        logger.error(e.getMessage(), e);
        return Result.error(e.getMessage());
    }

    @ExceptionHandler(value = RuntimeException.class)
    public Result exceptionHandlerRuntimeException(Exception e){
        logger.error(e.getMessage(), e);
        return Result.error(e.getMessage());
    }

    // 或者其它自定义异常
}

再定义一个统一的接口返回对象:

点击查看代码
public class Result<T> implements Serializable {
    private String code;
    private Boolean success;
    private T data;
    private String msg;

    public Result(String code, Boolean success, String msg) {
        this.code = code;
        this.success = success;
        this.msg = msg;
    }

    public Result(String code, String msg, T data) {
        this.code = code;
        this.data = data;
        this.msg = msg;
    }

    public Result() {
        this.code = ReturnCodeEnum.OK.getCode();
        this.success = true;
        this.msg = ReturnCodeEnum.OK.getMsg();
    }

    public void serverFailed() {
        this.serverFailed((Exception)null);
    }

    public void serverFailed(Exception e) {
        this.code = ReturnCodeEnum.SERVER_FAILED.getCode();
        this.success = false;
        if (e == null) {
            this.msg = ReturnCodeEnum.SERVER_FAILED.getMsg();
        } else {
            this.msg = e.getMessage();
        }

    }

    public static <T> Result<T> success(T data) {
        Result<T> success = new Result();
        success.setData(data);
        return success;
    }

    public static <T> Result<T> success() {
        return new Result();
    }

    public static <T> Result<T> error() {
        return new Result(ReturnCodeEnum.SERVER_FAILED.getCode(), false, ReturnCodeEnum.SERVER_FAILED.getMsg());
    }

    public static <T> Result<T> error(String message) {
        return new Result(ReturnCodeEnum.SERVER_FAILED.getCode(), false, message);
    }

    public static <T> Result<T> error(String code, String message) {
        return new Result(code, false, message);
    }

    public void resetWithoutData(Result result) {
        this.success = result.getSuccess();
        this.code = result.getCode();
        this.msg = result.getMsg();
    }

    public void resetResult(ReturnCodeEnum returnCodeEnum, boolean isSuccess) {
        this.code = returnCodeEnum.getCode();
        this.success = isSuccess;
        this.msg = returnCodeEnum.getMsg();
    }

    public static <T> Result<T> error(ReturnCodeEnum returnCodeEnum) {
        Result<T> error = new Result();
        error.code = returnCodeEnum.getCode();
        error.success = false;
        error.msg = returnCodeEnum.getMsg();
        return error;
    }

    public String getCode() {
        return this.code;
    }

    public Boolean getSuccess() {
        return this.success;
    }

    public T getData() {
        return this.data;
    }

    public String getMsg() {
        return this.msg;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public void setSuccess(Boolean success) {
        this.success = success;
    }

    public void setData(T data) {
        this.data = data;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof Result)) {
            return false;
        } else {
            Result<?> other = (Result)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                label59: {
                    Object this$code = this.getCode();
                    Object other$code = other.getCode();
                    if (this$code == null) {
                        if (other$code == null) {
                            break label59;
                        }
                    } else if (this$code.equals(other$code)) {
                        break label59;
                    }

                    return false;
                }

                Object this$success = this.getSuccess();
                Object other$success = other.getSuccess();
                if (this$success == null) {
                    if (other$success != null) {
                        return false;
                    }
                } else if (!this$success.equals(other$success)) {
                    return false;
                }

                Object this$data = this.getData();
                Object other$data = other.getData();
                if (this$data == null) {
                    if (other$data != null) {
                        return false;
                    }
                } else if (!this$data.equals(other$data)) {
                    return false;
                }

                Object this$msg = this.getMsg();
                Object other$msg = other.getMsg();
                if (this$msg == null) {
                    if (other$msg != null) {
                        return false;
                    }
                } else if (!this$msg.equals(other$msg)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(Object other) {
        return other instanceof Result;
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $code = this.getCode();
        int result = result * 59 + ($code == null ? 43 : $code.hashCode());
        Object $success = this.getSuccess();
        result = result * 59 + ($success == null ? 43 : $success.hashCode());
        Object $data = this.getData();
        result = result * 59 + ($data == null ? 43 : $data.hashCode());
        Object $msg = this.getMsg();
        result = result * 59 + ($msg == null ? 43 : $msg.hashCode());
        return result;
    }

    public String toString() {
        return "Result(code=" + this.getCode() + ", success=" + this.getSuccess() + ", data=" + this.getData() + ", msg=" + this.getMsg() + ")";
    }

    public Result(String code, Boolean success, T data, String msg) {
        this.code = code;
        this.success = success;
        this.data = data;
        this.msg = msg;
    }

统一状态码:

点击查看代码
public enum ReturnCodeEnum {
    OK("200", "success"),
    OPERATION_FAILED("202", "操作失败"),
    PARAMETER_ERROR("203", "参数错误"),
    UNIMPLEMENTED_INTERFACE_ERROR("204", "未实现的接口"),
    INTERNAL_SYSTEM_ERROR("205", "系统内部错误"),
    THIRD_PARTY_INTERFACE_ERROR("206", "第三方接口错误"),
    CRS_TOKEN_INVALID("401", "token无效"),
    PERMISSIONS_ERROR("402", "业务权限认证失败"),
    AUTHENTICATION_FAILED("403", "登陆超时,请重新登陆"),
    SERVER_FAILED("500", "server failed 500 !!!"),
    DATA_ERROR("10001", "数据获取失败"),
    UPDATE_ERROR("10002", "操作失败"),
    SIGN_ERROR("10010", "签名错误"),
    ACCOUNT_OR_PASSWORD_ERROR("4011", "用户名或密码错误"),
    ILLEGAL_PERMISSION("405", "权限不足"),
    FORBIDDON("410", "已被禁止"),
    TOKEN_TIME_OUT("4012", "session过期,需重新登录");

    private String code;
    private String msg;

    public String getCode() {
        return this.code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMsg() {
        return this.msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    private ReturnCodeEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

再定义一个测试对象:

@Getter
@Setter
//@ToString
//@AllArgsConstructor
//@NoArgsConstructor
public class Person {
    private String name;
    private Integer age;
    private Person father;

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

写一个测试接口,模拟循环依赖的对象,使用fastjson进行序列化返回。

public Result test2 (){
        List<Person> list = new ArrayList<>();
        Person obj1 = new Person("张三", 48);
        Person obj2 = new Person("李四", 23);
        obj1.setFather(obj2);
        obj2.setFather(obj1);

        list.add(obj1);
        list.add(obj2);

        Person obj3 = new Person("王麻子", 17);
        list.add(obj3);

        List<Person> young = list.stream().filter(e -> e.getAge() <= 45).collect(Collectors.toList());
        List<Person> children =  list.stream().filter(e -> e.getAge()< 18).collect(Collectors.toList());

        HashMap map = new HashMap();
        map.put("young", young);
        map.put("children", children);
        return Result.success(map);
    }

开启fastjson的
SerializerFeature.DisableCircularReferenceDetect
禁用循环依赖检测,使其抛出异常。
访问测试接口,后台打印日志

ERROR 21360 [http-nio-8657-exec-1] [com.nyp.test.config.GlobalExceptionHandler] : Handler dispatch failed; nested exception is java.lang.StackOverflowError

接口返回

{
	"code":"500",
	"data":null,
	"msg":"Handler dispatch failed; nested exception is java.lang.StackOverflowError",
	"success":false
}

证明异常在全局异常捕获处被成功捕获。且返回了500状态码,证明服务端出现了异常。

jackson的问题

我们现在换掉fastjson,使用springboot自带的jackson进行序列化。同样还是上面的代码。
后台打印了日志:

[2023-04-01 15:27:42.230] ERROR 17156 [http-nio-8657-exec-2] [com.nyp.test.config.GlobalExceptionHandler] : Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: com.nyp.test.model.Person["father"]->com.nyp.test.model.Person["father"]....

日志信息略有不同,是两种不同的序列化框架的差异,总之全局异常捕获也成功了。

再来看返回的结果如下:

这就很明显不对劲,后台已经抛出异常,并成功捕获了异常,前端怎么还接收到了200状态码呢?而且 data里面还有循环嵌套的数据!

返回的报文很长,仔细观察最后面,发现后面同时也返回了500状态码及异常信息。


长话短说,相当使用jackson,在默认情况下,对于循环对象引用,在添加了全局异常处理情况下,接口同时返回了两段相反的报文:

{
	"code":"200",
	"data":{"young":[{"name":"李四","age":23,"father":{"name":"张三","age":48}]}"
	"success":true
}
{
	"code":"500",
	"data":null,
	"msg":"Handler dispatch failed; nested exception is java.lang.StackOverflowError",
	"success":false
}


小朋友你是否有很多问号??

这种现象是在return后面抛出异常引起?

这就有点意思了。
造成这种现象的原因,我初步怀疑是在方法return返回过后再抛出异常导致的。

我这怀疑也不是毫无理由,具体请看我的另一篇文章
当transcational遇上synchronized
,里面提到过,
spring使用动态代理加AOP实现事务管理。那么一个加了注解事务的方法实际上需要简化成至少3个步骤:

void begin();

@Transactional
public synchronized void test(){
    // 
}

void commit();
// void rollback();

如果在读已提交及以上的事务隔离级别下,test方法执行完毕,更新了数据但这时候还没到commit事务,但已经释放了锁,另一个事务进来读到的还是旧数据。

类似地,这里的test方法实际上是一样的,jackson在做序列化操作在return之前,那么会不会return返回了一次200,在return过后再抛出异常后再返回了一次500状态码?

那就使用
TransactionSynchronization
模拟一次在return后面的异常看返回给前端什么信息。

@Transactional
    @RequestMapping( "/clone")
    public Result test2 (){
        List<Person> list = new ArrayList<>();
        Person obj1 = new Person("张三", 48);
        Person obj2 = new Person("李四", 23);
        obj1.setFather(obj2);
        obj2.setFather(obj1);

        list.add(obj1);
        list.add(obj2);

        Person obj3 = new Person("王麻子", 17);
        list.add(obj3);

        List<Person> young = list.stream().filter(e -> e.getAge() <= 45).collect(Collectors.toList());
        List<Person> children =  list.stream().filter(e -> e.getAge()< 18).collect(Collectors.toList());

        HashMap map = new HashMap();
        map.put("young", young);
        map.put("children", children);

        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                if (1 == 1) {
                    throw new HttpMessageNotWritableException("test exception after return");
                }
                TransactionSynchronization.super.afterCommit();
            }
        });
        return Result.success(map);
    }

重启调用测试接口,后台打印日志

[http-nio-8657-exec-1] [com.nyp.test.config.GlobalExceptionHandler] : test exception after return

返回客户端信息:

{"code":"500","success":false,"data":null,"msg":"test exception after return"}

测试表明,并不是这个原因造成的。


到这里,可能细心的朋友也发现了,对于前面的猜想,关于
jackson在做序列化操作在return之前,那么会不会return返回了一次200,在return过后再抛出异常后再返回了一次500状态码?
其实是不合理的。
我们在最开始接触java web开发的时候肯定是先学servlet,再学spring,springmvc,springboot这些框架,现在再回到最初的美好,想想servlet是怎样返回数据给客户端的?

通过
HttpServletResponse
获取一个输出流,不管是
OutputStream
还是
PrintWriter
,将我们手动序列化的json串输出到客户端。

@WebServlet(urlPatterns = "/testServlet")
public class TestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        // 通过PrintWriter 或者 OutputStream 创建一个输出流
        // OutputStream outputStream = response.getOutputStream();
        PrintWriter out = response.getWriter();
        try {
            // 模拟获取一个返回对象
            Person person = new Person("张三", 23);
            out.println("start!");
            // 手动序列化,并输出到客户端
            Gson gson = new Gson();
            out.println(Result.success(gson.toJson(person)));
            // outputStream.write();
            out.println("end");
        } finally {
            out.println("成功!");
            out.close();
        }
        super.doGet(request, response);
    }
}

我没看过springmvc这块的源码,想来也是同样的逻辑处理对吧。
在dispatchServlet里面invoke完毕目标controller获得了返回对象以后,再调用序列化框架jackson或者fastjson得到一个json对象,再通过输出流输出前端,最后一步操作可能是在servlet里也可能直接在序列化框架里面直接操作。
总之不管是在哪步,都有点不合理,如果是在序列化的时候,序列化框架直接异常了,也不应该输出200和500两段报文。


不管怎样,这里也算是验证了@ControolerAdvice能不能捕获目标controller方法在Return以后抛出的异常,答案是可以。

现在我们可以再来看看Fastjson在return以后进行序列化发生异常的时候,为什么不会输出200和500两段报文。


fastjson为什么没有问题


通过前文我们知道,在同样的情况下,fastjson序列化是可以正常返回给客户端500异常的报文。

我们现在将springmvc的序列化框架切换到fastjson。通过断点走一遍源码。观察为什么fastjson可以正常抛出异常。

通过调用栈信息,我们可以很明显的观察到我们很熟悉的
distpatchServlet
,再到
handleReturnValue
调用完成目标controller拿到返回对象,现到
AbstractMessageConverterMethodProcessor.writeWithMessageConverters
,最终到达
GenericHttpMessageConverter.write()
通过注释,哪怕是方法名和参数名,我们也知道这里就是开始调用具体的序列化框架重写这个方法输出返回报文到客户端了。

那么在这里开始打个断点,这是个接口方法,它有很多实现类,这里打断点会直接进入到具体实现类的方法。
最终来到了FastJsonHttpMessageConverter.writeInternal()

重点来了,如上图所示,执行到line 314行,也就是标记为1的地方就抛出异常,然后到了finally里面去了,
跳过了line 337即2处真正执行write输出到客户端的操作

我们不用去管line 314处所调用方法内部的序列化具体操作,我们只需要知道,它在序列化准备阶段直接异常了,并没有真正执行向客户端进行write的操作。

然后异常最终被@RestControllerAdvice所捕获,输出到客户端500。


jackson的输出流程


现在作为对比,再回过头来看看jackson是怎样完成上述的操作的。


打到与上小节fastjson一样的断点,最终进入了jackson的序列化方法,通过右边
inline watches
可以看到将要被序列化的value从对象的循环引用变成了具体的若干层嵌套循环了。

再一路断点,来到UTF8JsonGenerator,可以观察到,jackson不是将整个返回值value一起进行序列化,而是一个对象一个field顺序进行序列化。

这些值将临时进入了一个buffer缓冲区,在大于
outputend=8000
,就flush直接输出到客户端。

这里的_outputstream就是java.io.OutputStream对象。


小结

这里可以做一个小结了。

jackson为什么会在对象循环引用的时候同时向客户端输出200和500两段报文?

因为jackson的序列化是分阶段进行的,它使用了一种类似于
fail-safe
机制,延迟到后面再失败,而在失败之前,已经将200状态码的报文输出到客户端。

fastjson为什么能正常的只输出500报文?

因为Fastjson的序列化有一种
fail-fast
机制,它判断到有对象循环引用时可以直接抛出异常,然后被全局异常处理,最终只会向客户端输出500状态码报文。

@ControllerAdvice失效的场景

通过注释,我们知道@ControllerAdvice默认作用于全部的controller类方法。也可以手动设置package.

@RestControllerAdvice("com.nyp.test.controller")
或者
@RestControllerAdvice(basePackages = "com.nyp.test.controller")

那么让它失效的场景就是
1.异常到不了controller层,比如在service层里通过try-catch把异常吞了。又比如到达了controller层也抛出了,但在其它AOP切面通知里通过try-catch处理了。
2.或者不指向controller层或部份controller层,比如通过@RestControllerAdvice(basePackages = "com.nyp.test.else")

等等。

其它只要不触碰到以上情况,正确的配置了,即使是在return后面抛出异常也可以正确处理。
具体到本文jackson的这种情况,严格意义上来讲,@ControllerAdvice也是起了作用的。只不过是jackson在序列化的过程中本身出的问题。

总结

  1. @ControllerAdvice完全安全吗?
    只要正确配置,它是完全安全的。本文属于jackson这种特殊情况,它造成的异常情况不是@ControllerAdvice的问题。


2.造成同时返回200和500报文的原因是什么?

因为jackson的序列化是分阶段进行的,它使用了一种类似于
fail-safe
机制,延迟到后面再失败,而在失败之前,将200状态码的报文输出到客户端,失败之后,又将500状态码的报文输出到客户端。
而Fastjson的序列化因为有一种
fail-fast
机制,它判断到有对象循环引用时可以直接抛出异常,然后被全局异常处理,最终只会向客户端输出500状态码报文。


3. 怎么解决这种问题?

这本质上是一个jackson循环依赖的问题。通过注解
@JsonBackReference
@JsonManagedReference
@JsonIgnore
@JsonIdentityInfo
可以部份解决。


比如:

@JsonIdentityInfo(generator= ObjectIdGenerators.IntSequenceGenerator.class, property="name")
private Person father;

返回:

{
	"code": "200",
	"success": true,
	"data": {
		"young": [{
			"name": "李四",
			"age": 23,
			"father": {
				"name": 1,
				"name": "张三",
				"age": 48,
				"father": {
					"name": 2,
					"name": "李四",
					"age": 23,
					"father": 1
				}
			}
		}, {
			"name": "王麻子",
			"age": 17,
			"father": null
		}],
		"children": [{
			"name": "王麻子",
			"age": 17,
			"father": null
		}]
	},
	"msg": "success"
}

同时,对于对象循环引用这种情况,在代码中就应该尽量去避免。
就像spring处理依赖注入的情况,一开始使用@lazy注解解决,后面spring官方通过三层缓存来解决,再到后面springboot官方默认不支持依赖注入,如果有依赖注入默认启动就会报错。


一言以蔽之,本文说的是,关于spring mvc&spring boot使用jackson做序列化输出的时候,如果没有处理好循环依赖的问题,那么前端不能正确感知到服务器异常这个问题。

但是循环依赖并不常见,遇到了也能有解决方案,所以看起来本文好像并没有什么卵用。

不过,没人规定必须要解决吧,当我还是一个新手的时候,我没解决循环依赖,而同时前端又没有接收到正确的服务端异常时,总是会有疑惑的。

而且如果扩展开来的话,jackson在序列化中途导致失败,都有可能发生这种情况。

从这个角度来说,算不算是jackson的一个问题呢?

不管怎样,希望本文对你能够有所启发。

本文介绍基于
MATLAB
求取空间数据的
变异函数
,并绘制
经验半方差图
的方法。

由于本文所用的数据并不是我的,因此遗憾不能将数据一并展示给大家;但是依据本篇博客的思想与对代码的详细解释,大家用自己的数据,可以将空间数据变异函数计算与经验半方差图绘制的全部过程与分析方法加以完整重现。

1 数据处理

1.1 数据读取

本文中,我的初始数据为某区域
658
个土壤采样点的
空间位置

X

Y
,单位为

)、
pH值

有机质含量

全氮含量
。这些数据均存储于
data.xls
文件中;而后期操作多于
MATLAB
软件中进行。因此,首先需将源数据选择性地导入
MATLAB
软件中。

利用
MATLAB
软件中
xlsread
函数可以实现这一功能。具体代码附于本文的
1.3 正态分布检验及转换
处。

1.2 异常数据剔除

得到的采样点数据由于采样记录、实验室测试等过程,可能具有一定误差,从而出现个别异常值。选用
平均值加标准差法
对这些异常数据加以筛选、剔除。

分别利用
平均值加标准差法
中“
2S
”与“
3S
”方法加以处理,发现“
2S
”方法处理效果相对后者较好,故后续实验取“
2S
”方法处理结果继续进行。

其中,“
2S
”方法是指将数值
大于或小于

平均值±2倍标准差
的部分视作异常值,“
3S
”方法则是指将数值
大于或小于

平均值±3倍标准差
的部分视作异常值。

得到异常值后,将其从
658
个采样点中剔除;剩余的采样点数据继续后续操作。

本部分具体代码附于
1.3 正态分布检验及转换
处。

1.3 正态分布检验及转换

计算变异函数需建立在初始数据符合正态分布的假设之上;而采样点数据并不一定符合正态分布。因此,我们需要对原始数据加以正态分布检验。

一般地,正态分布检验可以通过
数值检验

直方图

QQ图
等图像加以直观判断。本文综合采取以上两种数值、图像检验方法,共同判断正态分布特性。

针对数值检验方法,我在一开始准备选择采用
Kolmogorov-Smirnov检验
方法;但由于了解到,这一方法仅仅适用于标准正态检验,因此随后改用
Lilliefors检验

Kolmogorov-Smirnov检验
通过样本的经验分布函数与给定分布函数的比较,推断该样本是否来自给定分布函数的总体;当其用于正态性检验时只能做标准正态检验。

Lilliefors检验
则将上述
Kolmogorov-Smirnov
检验改进,其可用于一般的正态分布检验。

QQ图
(Quantile Quantile Plot)是一种散点图,其横坐标表示某一样本数据的分位数,纵坐标则表示另一样本数据的分位数;横坐标与纵坐标组成的散点图代表同一个累计概率所对应的分位数。

因此,
QQ图
具有这样的特点:针对
y=x
这一直线,若散点图中各点均在直线附近分布,则说明两个样本为同等分布;因此,若将横坐标(纵坐标)表示为一个标准正态分布样本的分位数,则散点图中各点均在上述直线附近分布可以说明,纵坐标(横坐标)表示的样本符合或基本近似符合正态分布。本文采用将横坐标表示为正态分布的方式。

此外,
PP图
(Probability Probability Plot)同样可以用于正态分布的检验。
PP图
横坐标表示某一样本数据的累积概率,纵坐标则表示另一样本数据的累积概率;其根据变量的累积概率对应于所指定的理论分布累积概率并绘制的散点图,用于直观地检测样本数据是否符合某一概率分布。和
QQ图
类似,如果被检验的数据符合所指定的分布,则其各点均在上述直线附近分布。若将横坐标(纵坐标)表示为一个标准正态分布样本的分位数,则散点图中各点均在直线附近分布可以说明,纵坐标(横坐标)表示的样本符合或基本近似符合正态分布。

三种土壤属性,我选择首先以
pH数值
为例进行操作。通过上述数值检验、图像检验方法,检验得到剔除异常值后的原始
pH数值
数据并不符合正态分布这一结论。因此,尝试对原数据加以
对数

开平方
等转换处理;随后发现,原始
pH值
开平方数据的正态分布特征虽然依旧无法通过较为严格的
Lilliefors
检验,但其直方图、
QQ图
的图像检验结果较为接近正态分布,并较之前二者更加明显。故后续取开平方处理结果继续进行。

值得一提的是,本文后半部分得到
pH值
开平方数据的实验变异函数及其散点图后,在对其余两种空间属性数据(即有机质含量与全氮含量)进行同样的操作时,发现全氮含量数据在经过“
2S
”方法剔除异常值后,其原始形式的数据是可以通过
Lilliefors
检验的,且其直方图、
QQ图
分布特点十分接近正态分布。

我亦准备尝试对空间属性数据进行反正弦转换。但随后发现,已有三种属性数值的原始数据并不严格分布在
-1

1
的区间内,因此并未对其进行反正弦方式的转换。

经过上述检验、转换处理过后的图像检验结果如下所示。

以上部分代码如下:

clc;clear;
info=xlsread('data.xls');
oPH=info(:,3);
oOM=info(:,4);
oTN=info(:,5);
 
mPH=mean(oPH);
sPH=std(oPH);
num2=find(oPH>(mPH+2*sPH)|oPH<(mPH-2*sPH));
num3=find(oPH>(mPH+3*sPH)|oPH<(mPH-3*sPH));
PH=oPH;
for i=1:length(num2)
    n=num2(i,1);
    PH(n,:)=[0];
end
PH(all(PH==0,2),:)=[];
 
%KSTest(PH,0.05)
H1=lillietest(PH);
 
for i=1:length(PH)
    lPH(i,:)=log(PH(i,:));
end
 
H2=lillietest(lPH);
 
for i=1:length(PH)
    sqPH(i,:)=(PH(i,:))^0.5;
end
 
H3=lillietest(sqPH);
 
% for i=1:length(PH)
%     arcPH(i,:)=asin(PH(i,:));
% end
% 
% H4=lillietest(arcPH);
 
subplot(2,3,1),histogram(PH),title("Distribution Histogram of pH");
subplot(2,3,2),histogram(lPH),title("Distribution Histogram of Natural Logarithm of pH");
subplot(2,3,3),histogram(sqPH),title("Distributio n Histogram of Square Root of pH");
subplot(2,3,4),qqplot(PH),title("Quantile Quantile Plot of pH");
subplot(2,3,5),qqplot(lPH),title("Quantile Quantile Plot of Natural Logarithm of pH");
subplot(2,3,6),qqplot(sqPH),title("Quantile Quantile Plot of Square Root of pH");

2 距离量算

接下来,需要对筛选出的采样点相互之间的距离加以量算。这是一个复杂的过程,需要借助循环语句。

本部分具体代码如下。

poX=info(:,1);
poY=info(:,2);
dis=zeros(length(PH),length(PH));
for i=1:length(PH)
    for j=i+1:length(PH)
        dis(i,j)=sqrt((poX(i,1)-poX(j,1))^2+(poY(i,1)-poY(j,1))^2);
    end
end

3 距离分组

计算得到全部采样点相互之间的距离后,我们需要依据一定的范围划定原则,对距离数值加以分组。

距离分组首先需要确定步长。经过实验发现,若将步长选取过大会导致得到的散点图精度较低,而若步长选取过小则可能会使得每组点对总数量较少。因此,这里取步长为
500
米;其次确定最大滞后距,这里以全部采样点间最大距离的一半为其值。随后计算各组对应的滞后级别、各组上下界范围等。

本部分具体代码附于本文
4 平均距离、半方差计算及其绘图
处。

4 平均距离、半方差计算及其绘图

分别计算各个组内对应的点对个数、点对间距离总和以及点对间属性值差值总和等。随后,依据上述参数,最终求出点对间距离平均值以及点对间属性值差值平均值。

依据各组对应点对间距离平均值为横轴,各组对应点对间属性值差值平均值为纵轴,绘制出经验半方差图。

本部分及上述部分具体代码如下。

madi=max(max(dis));
midi=min(min(dis(dis>0)));
radi=madi-midi;
ste=500;
clnu=floor((madi/2)/ste)+1;
ponu=zeros(clnu,1);
todi=ponu;
todiav=todi;
diff=ponu;
diffav=diff;
for k=1:clnu
    midite=ste*(k-1);
    madite=ste*k;
    for i=1:length(sqPH)
        for j=i+1:length(sqPH)
            if dis(i,j)>midite && dis(i,j)<=madite
                ponu(k,1)=ponu(k,1)+1;
                todi(k,1)=todi(k,1)+dis(i,j); diff(k,1)=diff(k,1)+(sqPH(i)-sqPH(j))^2;
            end
        end
    end
    todiav(k,1)=todi(k,1)/ponu(k,1);
    diffav(k,1)=diff(k,1)/ponu(k,1)/2;
end
plot(todiav(:,1),diffav(:,1)),title("Empirical Semivariogram of Square Root of pH");
xlabel("Separation Distance (Metre)"),ylabel("Standardized Semivariance");

5 绘图结果

通过上述过程,得到
pH值
开平方后的实验变异函数折线图及散点图。

可以看到,
pH值
开平方后的实验变异函数较符合于有基台值的球状模型或指数模型。函数数值在距离为
0

8000
米区间内快速上升,在距离为
8000
米后数值上升放缓,变程为
25000
米左右;即其“先快速上升,再增速减缓,后趋于平稳”的图像整体趋势较为明显。但其数值整体表现较低——块金常数为
0.004
左右,而基台值仅为
0.013
左右。为验证数值正确性,同样对
有机质

全氮
进行上述全程操作。

得到二者对应变异函数折线图与散点图。

由以上三组、共计六幅的
pH值
开平方、
有机质

全氮
对应的实验变异函数折线图与散点图可知,不同数值对应实验变异函数数值的数量级亦会有所不同;但其整体“先快速上升,再增速减缓,后趋于平稳”的图像整体趋势是十分一致的。

此外,如上文所提到的,针对三种空间属性数据(
pH值

有机质含量

全氮含量
)中最符合正态分布,亦是三种属性数据各三种(原始值、取对数与开平方)、共九种数据状态中唯一一个通过
Lilliefors
正态分布检验的数值——
全氮
含量经过异常值剔除后的原始值,将其正态分布的图像检验结果特展示如下。

至此,我们就完成了全部的操作、分析过程~