数据专栏

智能大数据搬运工,你想要的我们都有

科技资讯:

科技学院:

科技百科:

科技书籍:

网站大全:

软件大全:

使用DragonBonesPro制作补间动画、龙骨动画 1.开场动画 2.小丑盒子 3.跑步的人 4.跳跳羊 5.猴子荡树 一.开场动画 1.导入素材 2.将素材拖入舞台内,并调整位置以及图层 3.多处设置关键帧,创建补间动画 4.最后运行 二.小丑盒子动画 1.导入素材 2.将素材拖入舞台,并调整层级 3.创建骨骼,调整场景树 4.添加关键帧,创建补间动画 5.最后运行 三.跑步的人 1.导入json文件 2. 创建手臂和腿的骨骼 3. 创建头部和身体的骨骼 4.所有骨骼建成效果 5.所有场景树 6.添加动作(run) 7.创建其他动作 8.不同状态运行结果 四.跳跳羊 1.导入json素材 2.给羊创建骨骼,调整层级,场景树如图 3.选择网格模式,对跳跳羊创建网格 4.添加边线 5.添加里面的各个点 6.添加关键帧,创建补间动画 7.最终运行图 五.猴子荡树 1.导入素材 2.创建网格以及骨骼,将网格绑定在骨骼上 3.设置关键帧,创建补间动画 4.最终运行
来源:OSCHINA
发布时间:2020-05-15 19:03:00
开发软件:DragonBonesPro 1、开场动画实例 2、小丑盒子实例 3、跑步的人实例 4、跳跳样实例 1、开场动画 步骤: 1、导入素材: 2、将素材拖入到舞台并调整位置 3、调整图层位置 4、设置关键帧,创建补间动画 5、成品图 2、小丑盒子实例 步骤: 1、导入素材 2、将素材拖入到舞台,调整层级 3、创建骨骼 4、创建补间动画 5、成果图 小丑头会左摇右晃,辫子会上下摆动,同时弹簧会有收缩 3、跑步的人实例 步骤: 1、导入.json文件 2、创建手臂和腿的骨骼,层级(手臂:innerarm_upper—innerarm lower-innerarm- hand 腿部:innerleg_upper—innerleg lower-innerleg- foot) 3、创建头部和身体的骨骼,层级(lowerbody- upperbody-head) 4、整理骨骼层级 5、制作补间动画 最低位姿态第0帧: 第二个最低位姿态第4帧: 最高位姿态第2帧: 对第一帧和第三帧进行微调,使动作更流畅 用同样的方法完成另一个半步。例如把第0帧的所有关键帧复制到第8帧 创建其他动作: 4、跳跳羊实例 步骤 1、导入.json文件到舞台 2、给跳跳羊创建骨骼,层级如下图 3、选择网格模式,对跳跳羊创建网格、添加顶点 4、制作补间动画 5.运行结果
来源:OSCHINA
发布时间:2020-05-15 16:28:00
当我们查看别人的shader,如果没有在代码里找到声明那多半是使用了UNITY内置的文件和变量。 一、包含文件 UNITY可以使用#include 来包含部分文件,文件后缀.cginc,类似C++头文件/java的包 例如 CGPROGRAM ... #include "UnityCG.cginc" ... ENDCG 通过这种方式可以引用UNITY已经封装好的函数/变量我们可以通过 http://unity3d.com/cn/get-unity/download/archive 下载(虽然网站没法访问) 常用的UNITY内置文件 UnityCG.cginc 包含了最常用的函数结构体和宏等 UnityShaderVariables.cginc 在编译UNITY SHADER时 会自动被包含进来 ,包含了很多全局变量 如 UNITY_MATRIX_MVP转换矩阵 Lighting.cginc 包含了各种光照模型,如果编写Surface Shader的话 会被自动包含进来 HLSLSupport.cginc 在编译UNITY SHADER时被自动包含进来,声明了很多用于跨平台编译的宏和定义 UnityStandardBRDF.cginc、UnityStandardCore.cginc 这些文件里面包含了用于基于物理的渲染 ----------------------------------------------------------- 其中UnityCG.cginc是最常用的组件 他里面提供了很多常用的定义和文件 常用结构体名 包含变量 appdata_base 顶点位置 顶点法线 第一组纹理坐标 appdata_tan 顶点位置 顶点法线 顶点切线 第一组纹理坐标 appdata_full 顶点位置 顶点法线 顶点切线 第四组(或者更多)纹理坐标 appdata_img v2f_img 顶点位置 第一组纹理坐标 裁剪空间中的位置 纹理坐标 UnityCG.cginc常用的函数 函数名 描述 float3 WordSpaceViewDir(float4 v) 输入一个模型空间的顶点位置,返回在世界空间中这个点到摄像机的方向 float3 ObjSpaceViewDir(flaot4 v) 输入一个模型空间的顶点位置,返回在模型空间中这个点到摄像机的方向 float3 WordSpaceLightDir(float4 v) 仅可用于前向渲染,输入一个模型空间的顶点,返回世界空间中该点到光源的光照方向,没有被归一化 flaot3 ObjSpaceLightDir(float4 v) 仅可用于前向渲染,输入一个模型空间的顶点,返回模型空间中该点到光源的光照方向,没有被归一化 float3 UnityObjToWorldNormal(flaot3 normal) 把法线方向从模型空间转换到世界空间 float3 UnityObjToWorldDir(int float3 dir) float3 UnityWorldToObjDir(int float3 dir) 把方向矢量从模型空间转换到世界空间中 把方向矢量从世界空间转换到模型空间中 二、内置变量 除了上述常用的函数和文件,unity还提供了很多常用的变量,如用于访问环境光、雾效、时间、光照等目的的变量,这些变量大都在UnityShaderVariables.cginc中 光照的变量还位于 Lighting.cginc 和AutoLight.cginc 三、Unity支持的语义 SV_POSITION/SV_Target/POSITION/COLOR0等等都是CG/HLSL提供的语义 可以在微软的DirectX的文档里找到说明(网址倒是能打开 ,就是没看懂) http://msdn.microsoft.com/en-us/library/windows/desktop/bb509674(v=vs8.5).aspx#VS 语义其实就是赋给shader的一个输入输出的字符串,这个字符串表达了参数的含义,这些语义可以让shader知道从哪里读取数据,并把数据输出到哪里 他们在CG/HLSL的shader流水线中是不可获取的,需要注意的是 Unity并没有支持所有的语义 通常情况下 这些输入输出的参数并不需要特别有意义,我们可以自行决定这些变量的用途.变量本身存储什么 shader并不关心 UNITY为了方便数据传输,对一些特殊的语义进行了特殊的含义规定,例如TEXCOORD0,在顶点着色器的入参结构体a2v的内部我们用它描述texcoord,UNITY会识别该语义并把第一组纹理坐标填充给texcoord,需要注意即使语义一样出现的位置不同,意义也可能不同,如TEXCOORD0出现在输出结构体内时,这个修饰的变量由我自己定义。 在DirectX10之后出现了一种新语义, 系统语义,SV开头 System Value Semantics 例如上面的SV_POSITION修饰pos,那么就表示pos包含了可以用于光栅化的变换后的顶点坐标,即齐次裁剪空间坐标,这些语义修饰的变量是不可随便赋值的,因为流水线需要靠他们去实现一些特殊的目的,例如渲染引擎会把SV_POSITION的坐标经过光栅化后显示在屏幕上,有的时候我们会看到同样的变量在不同的shader里用不同的语义修饰,例如一些shader会使用POSITION而不是SV_POSITION来修饰顶点的着色器输出,SV_POSITION 和POSITION在大多数平台上是等价的,但在某些平台上必须使用SV_POSITION来修饰输出,同样的情况还会在COLOR0和SV_Target上出现, 所以我们尽量使用SV开头的修饰符。 语义名称 描述 POSITION 模型空间中的顶点位置,通常是float4类型 NORMAL 顶点法线,通常是float3类型 TANGENT 顶点切线,通常是float4类型 TEXCOORDn COLOR 第n组纹理坐标,通常是float2和float4类型 顶点颜色,通常是float4或者fixed4 TEXCOORDn的n是由shader model决定的 ,在shader model2 的时候 n最大为4,在shader model 3的时候为8,在shader model 4的时候为16 通常情况下 一个模型的纹理坐标不超过2,即我们往往只使用TEXCOORD0/TEXCOORD1,在UNITY内置的appdata_full中,最多使用6个坐标纹理组 从顶点着色器传递到片元着色器的常用语义 语义 描述 SV_POSITION 裁剪空间里的顶点坐标,输出结构体必须包含这个修饰的变量,DirectX9里面是POSITION,但最好使用SV_POSITION COLOR0 通常用于输出的第一组颜色 但是不是必须的 COLOR1 TEXCOORD0~TEXCOORD7 通常用于输出的第二组颜色,但是不是必须的 通常用于输出纹理坐标,但是不是必须的 上面的语义 除了SV_POSITION之外,其他语义对变量没有明确的要求,也就是说我们可以存储任意值到语义描述的变量中,通常我们把一些自定义数据从顶点着色器传递到片元着色器,使用TEXCOORD0修饰。 片元着色器输出时使用的常用语义 SV_Target:输出值会存储到渲染目标Render Target中,等同于DirectX9中的COLOR语义,但最好使用SV_Target。 四、如何定义复杂的变量类型 上面提到的语义多半用来描述矢量或者标量类型的变量,例如fixed2 float float4 fixed4 下面的代码给出了一些使用语义来修饰不同变量的例子 struct v2f{ float4 pos:SV_POSITION; fixed3 color0:COLOR0; fixed4 color1:COLOR1; half value0:TEXCOORD0; float2 value1:TEXCOORD1; } 需要注意 一个语义可以使用的寄存器最多只能使用四个浮点值,因此如果我们想要定义矩阵类型,如float3*4,float4*4等变量就需要使用更多的空间,另一种方法就是把这些变量拆分成多个变量,如float4*4 可以拆分成4个float4,每个变量存储一行矩阵的元素。
来源:OSCHINA
发布时间:2020-05-15 14:37:00
影响服务器租用的因素有什么 ? 什么是好的 服务器租用 商呢 ? 其实对于企业来说性能稳定便是最好的要求 , 但是在选择服务商的时候也需要进行对比 , 因为不同的判断不同的考量会选择不同的服务商 , 影响服务器租用的因素有很多 , 那么应该注意什么呢 ? 宽带和防御 比如说 ,BGP 多线线路机房 ,6M 宽半的服务器 , 价格就要比 10M 兆宽带的价格便宜一些 。 宽带与防御因素 , 其实也是某些不良运营商做动手比较容易切入的地方 , 所以如果你真的遇到服务器租用价格比较低的运营商的话 , 那么很可能是商家在这个方面缩减了成本 。 所以一定不要盲目选择这种价格较低的合作伙伴 。 地域因素 也就是说即便是同一运营商 , 如果机房服务器是在不同的地域 , 那么所产生的价格也是不一样的 。 所以说 , 企业在选择服务器租用合作商的时候 , 一定要考虑一下地域因素 。 尽量选择机房离得近 , 公司比较近 , 同时租用价格还比较低这样的合作商 。 是配置因素 这里所说的配置元素 , 主要包括服务器机房线路 , 服务器处理器以及内存等等要素 , 可以说配置越高的话 , 服务器租用的价格也就越贵 , 这一点大家应该都心知肚明 。 总的来说 , 影响企业服务器租用价格的主要因素就是以上三个要素了 。 一般来讲 , 在选择服务器租用运营商的时候 , 企业工作人员要切记 , 运营商要有相关行业资格证 , 这样才能选出真正适合自己需求的高品质运营商 。
来源:OSCHINA
发布时间:2020-05-14 18:03:00
参考<>第五章 一、如何获取其他模型数据 在001里介绍了通过POSITION获取顶点位置坐标,如果想的到更多的模型数据,比如我们想要得到模型上每个顶点的纹理坐标和法线方向 PS:我们可以通过纹理坐标来访问纹理 法线坐标一般用来计算光照 因此我们需要给顶点着色器定义一个新的参数·这个参数将不是一个简单的数据类型 而是一个结构体 Shader "Customer/SimpleShader003"{ SubShader{ Pass{ CGPROGRAM #pragma vertex vert #pragma fragment frag struct a2v{ float4 vertex: POSITION; float3 normal:NORMAL; float4 texcoord:TEXCOORD0; }; float4 vert(a2v v): SV_POSITION{ return mul(UNITY_MATRIX_MVP,v.vertex); } fixed4 frag(): SV_Target{ return fixed4(1.0,1.0,1.0,1.0); } ENDCG } } } 在上面的代码中 声明的结构体a2v 他包含了顶点着色器需要的模型数据 在a2v的定义中,我们用到了更多的UNITY支持的语义,如 NORMAL 和TEXCOORD0,当他们作为顶点着色器的输入时都是有特定含义的 因为UNITY会根据这些语义来填充这个结构体,对于顶点着色器的输出,UNITY 支持的语义有POSITION,TANGENT,NORMAL,TEXCOORD0,TEXCOORD1,TEXCOORD2 TEXCOORD3,COLOR等。 为了使用一个自定义的结构体,我们必须使用如下格式 struct name{ Type Name:Semantic; Type Name:Semantic; ...... }; a表示应用 v表示顶点着色器 a2v的意义就是从应用阶段传递到顶点着色器中 那么填充到POSITION,TANGENT,NORMAL的语义里的数据酒究竟是从哪里来的呢? 在UNITY中,它们是由使用该材质的MeshRender组件提供的。 在每帧调用DRAW CALL的时候,MeshRender组件会把他负责渲染的模型的数据发送给UNITY SHADER, 我们知道,一个模型通常包含了一组三角面片。(我们不知道啊QAQ) 每个三角面片由三个顶点构成,而每个顶点又包含了一些数据,例如顶点位置、发现坐标、顶点颜色等等 通过上面的方法,我们就可以在顶点着色器中访问顶点的这些模型数据。 二、顶点、片元着色器之间如何相互通信 我们在实际开发的过程中,往往希望从顶点着色器输出一些数据,例如把模型的法线纹理坐标等传递给片元着色器 这就涉及到了顶点着色器和片元着色器之间的通信 为此我们需要新建一个结构体.修改后代码如下 Shader "Customer/SimpleShader004"{ SubShader{ Pass{ CGPROGRAM #pragma vertex vert #pragma fragment frag struct a2v{ float4 vertex:POSITION; float3 normal:NORMAL; float4 texcoord:TEXCOORD0; }; //使用一个结构体来定义顶点着色器的输出 struct v2f{ float4 pos :SV_POSITION; fixed3 color:COLOR0; }; v2f vert(a2v v):SV_POSITION{ v2f o; o.pos = mul(UNITY_MATRIX_MVP,v.vertex); o.color = v.normal*0.5 + fixed3(0.5,0.5,0.5); return o; } fixed4 frag(v2f i):SV_Target{ return fixed4(i.color,1.0); } ENDCG } } } 在上面的代码中,我们声明了一个新的结构体v2f v2f中也需要指定每个变量的语义,在本例中我们定义了SV_POSITION和COLOR0,顶点着色器的输出结构中,必须包含SV_POSITION,否则无法获取 裁剪空间的顶点坐标,COLOR0则由用户自己定义,一般是用来存颜色,例如逐顶点的漫反射颜色等,类似语义还有COLOR1 至此我们完成了从顶点着色器往片元着色器传递数据,顶点着色器是逐顶点的,片元着色器是逐片元的,因此片元着色器的输入实际是把顶点着色器的输出进行插值的结果 二、如何使用属性 unityshader和材质密不可分,shader提供了一些可以设置的参数来调试材质的效果,这些参数需要写在properties语义块里面 比如我们现在有一个新的需求,需要在面板上显示一个颜色拾取器,为此需要修改上面的代码 Shader "Customer/SimpleShader004"{ Properties{ //声明一个color类型的属性 _Color("Color Tint",Color) = (1.0,1.0,1.0,1.0) } SubShader{ Pass{ CGPROGRAM #pragma vertex vert #pragma fragment frag fixed4 _Color; struct a2v{ float4 vertex:POSITION; float3 normal:NORMAL; float4 texcoord:TEXCOORD0; }; struct v2f{ float4 pos:SV_POSITION; float3 color:COLOR0; }; v2f vert(a2v v) :SV_POSITION{ v2f o; o.pos = mul(UNITY_MATRIX_MVP,v.vertex); o.color = v.color*0.5+fixed3(0.5,0.5,0.5); return o; } fixed4 frag(v2f i):SV_Target{ fixed3 = i.color; c *=_Color.rgb; return fixed4(c,1.0); } ENDCG } } } 在上面的语句里我们定义了一个color,(1.0,1.0,1.0,1.0)代表的是白色 为了在CG代码里面访问他,我们还需要提前自己定义一个新变量 ,该变量名必须和属性里的变量名一致 ShaderLab中变量和CG类型中变量对应的关系如下所示 有时 CG变量前会有一个 uniform 关键字 uniform fixed4 _Color; 该关键字在CG里是用来修饰变量,仅仅提供该变量初始值的是如何指定和存储的相关信息,UNITY SHADER里该关键字可以省略。
来源:OSCHINA
发布时间:2020-05-14 17:31:00
今天正式开始学习shader, 准确的说是学习如何编写顶点/片元着色器 一、顶点/片元着色器的基本结构 shader包含subshader、shader、property、fallback等语句块  PS:每个UnityShader文件可以包含多个SubShader语义块,但至少要有一个。 当Unity需要加载这个UnityShader时, Unity会扫描所有的SubShader语义块, 然后选择一个能够在目标平台上运行的SubShader。 如果都不支持的话,Unity 就会使用FallBack语义指定的UnityShader。 同样子着色器中的Pass也是可以有很多个, 当执行子着色器内的渲染方案时,所有的Pass会被依次执行 Shader "MyShader"{ Properties{//属性} SubShader{//针对显卡A的SubShader Pass{ //设置渲染状态和标签 //开始CG代码片段 CGPROGRAM //该代码片段编译指令,例如 #pragma vertex vert #pragma fragment frag //CG代码 ENDCG //其他设置 } //其他需要的Pass } SubShader{//针对显卡B的SubShader} //上面的subshader全都失败之后用于回调的unity shader Fallback "VertexLit" } 其中最重要的是Pass语义块,绝大部分代码都是在Pass里实现的 EXAMPLE: Shader "Customer/SimpleShader"{ SubShader{ Pass{ CGPROGRAM #pragma vertex vert #pragma fragment frag float4 vert(float4 v :POSITION) :SV_POSITION{ return mul(UNITY_MATRIX_MVP,v) } fixed4 frag() : SV_Target{ return fixed4(1.0,1.0,1.0,1.0) } ENDCG } } } 上面的代码里没有用到Properties语义 Properties语义并不是必须的 可以不声明任何材质属性 subshader里没有进行标签设置,所以将使用默认的渲染和标签设置 在SubShader语义块中,我们定义了一个Pass, 这个pass里也没有设置任何自定义的渲染和标签 接着就是CGPROGRAM和ENDGC所包围的CG代码片段 #pragma vertex vert #pragma fragment frag 这两个指令将告诉UNITY哪个函数包含了顶点着色器的代码 哪个包含了片元着色器的代码 float4介绍 参考 https://www.cnblogs.com/jiahuafu/p/6136871.html GPU是以四维向量为基本单位来计算的。4个浮点数所组成的float4向量是GPU内置的最基本类型。使用GPU对两个float4向量进行计算,与CPU对两个整数或两个浮点数进行计算一样简单,都是只需要一个指令就可以完成。 HLSH的基本数据类型定义了float、int和bool等非向量类型,但是它们实际上都会被Complier转换成float4的向量, 只要把float4向量的其中3个数值忽略,就可以把float4类型作为标量使用 。 使用贴图坐标时,只需要二维向量,HLSL定义了float2类型作为二维向量使用。 Shader经常会用到矩阵,HLSL有一个内置类型float4x4,它可以用来表示一个4*4矩阵。float4x4并不是GPU的内置类型,float4x4实际上是由4个float4所组成的数组。其他的还有float3x3、float2x2,分表代表3*3矩阵、2*2矩阵。 Shader也可以声明数组,4*4矩阵实际上就是一个float4 m[4]的数组。注意,Shader中的所有的变量都使用寄存器,没有其他内存空间可以使用,所以越大的数组会占用越多的寄存器,甚至会超出寄存器的数量限制。 在使用float4向量中的个别数值时,可以用xyzw或rgba,都可以用来表示四维向量中的数值。但不能把它们混用,例如不能用xyba,把它视为颜色时就用rgba,否则就是用xyzw,不能把这二者混合使用。 rgba : 前三个值(红绿蓝)的范围为0到255之间的整数或者0%到100%之间的百分数。这些值描述了红绿蓝三原色在预期色彩中的量。 第四个值,alpha值,制订了色彩的透明度/不透明度,它的范围为0.0到1.0之间,0.5为半透明。 rgba(255,255,255,0)则表示完全透明的白色; rgba(0,0,0,1)则表示完全不透明的黑色; rgba(0,0,0,0)则表示完全不透明的白色,也即是无色; 二、顶点着色器结构 具体看一下vert函数的定义 float4 vert(float4 v: POSITION):SV_POSITION{ return mul(UNITY_MATRIX_MVP,v); } 这个就是本例所使用的顶点着色器的代码,他是逐顶点执行的。mul(UNITY_MATRIX_MVP,v)的意思是把顶点坐标从模型空间转换为裁剪空间 vert函数输入的v包含顶点的位置 这个是通过POSITION语义指定的 他的返回值是一个float4类型的变量 POSITION/SV_POSITION都是CG/HLSL里的语义,不可省略 这些语义将要告诉系统用户需要哪些输入值以及用户输出的是什么 。 简单地说POSITION语意用于顶点着色器,用来指定这些个位置坐标值,是在变换前的顶点的object space坐标。SV_POSITION语意则用于像素着色器,用来标识经过顶点着色器变换之后的顶点坐标 本例中POSITION 将告诉UNITY 把模型的顶点坐标填充到输入参数v中 SV_POSITION将告诉UNITY顶点着色器输出的是裁剪空间中的顶点坐标 三、片元着色器结构 现在看一下frag函数 fixed4 frag() :SV_Target{ return fixed4(1.0,1.0,1.0,1.0); } 上面的代码输出的上一个fixed4类型的变量,并且使用了SV_Target语义进行限定,SV_Target也是一个系统语义,它告诉渲染器,把用户输出的颜色存储到一个渲染目标里,这里将输出到默认的帧缓存中,片元着色器返回的fixed4类型的变量,片元着色器输出的颜色在[0,1]中,其中(0,0,0)是黑色,(1,1,1)是白色. PS:FIXED类型数据 参考 https://www.cnblogs.com/jiahuafu/p/6237859.html 在处理图形运算,特别是3D图形生成运算时,往往要定义一个Fixed数据类型,我称它为定点数,定点数其时就是一个整形数据类型,他的作用就是把所有数 进行转换,从而得到相应类型的整型表达,然后使用定点数进行整行运算,取到最终值并将其转换回实际的基本数据类型。因此它是通过避免大量的浮点运算来加快 图形处理的一个方式。 Fixed类型说了一堆,究竟来做什么的? 比如上例中,Y轴每次都要偏移0.4,而这个数是个浮点,严重影响了运算速度。比如,我们后台有一个数,用来计量Y轴本次的坐标,就叫做变量YY吧。X每 次都加1,也就是XX++,Y每次加0.4,也就是YY+=0.4。为了提高速度,我们将YY升级到Fixed类型,YY每次加Fixed的0.4,也就 是0.4*65536=26214,然后再四舍五入到整数类型,即YY+=26214,Y=(YY+32768)>>16。这样,就得到了每 次的整数Y,并且都是整数的加减和位运算,速度非常快。 SV_Target介绍 参考 https://zhuanlan.zhihu.com/p/113237579 SV_Target SV_POSITION 这两个都是 语义绑定 (semantics binding)。什么是语义绑定呢?语义绑定可以理解为关键字,我们都知道图形渲染是按照一步一步地进行的,又叫做渲染管线。那么为什么是要一步一步地进行呢?因为GPU的架构与CPU非常不同,cpu更像是人的大脑,可以同时思考不同的问题,而GPU则相当于计算机,通过有特定的输入,通过计算得到最终的输出。GPU没有像CPU一样的堆栈来存取变量与值,所以通过语义绑定将一个计算好的值放在一个物理存储位置,再用的话就利用语义绑定去特定物理位置取出,这也是GPU不能像CPU一样工作的原因之一吧。 SV_前缀的变量代表system value的意思,在DX10+的语义绑定中被使用代表特殊的意义,SV_POSITION在用法上和POSITION是一样的,区别是 SV_POSTION一旦被作为vertex函数的输出语义,那么这个最终的顶点位置就被固定了,不得改变。 四、效果 把写好的shader文件赋值给material文件,然后新建一个3DObject 修改mesh render里面的material为我们自己创建的 效果如下 可以尝试修改shader,参考 https://zhuanlan.zhihu.com/p/85594617 Shader "Custom/SimpleShader02" { SubShader { Pass { Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD1; }; v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } float4 frag(v2f i) : SV_Target { return float4(i.uv.r, i.uv.g,0,1); } ENDCG } } 先在两个结构体里面加入一些东西 struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD1; }; 在vertex shader做的事情:将拿到的uv直接通过v2f传给frag函数 v2f vert(appdata v) { v2f o; o.vertex = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = v.uv return o; } 接下来把uv的值填入颜色的返回值中,得到下图 float4 frag(v2f i) : SV_Target { float4 color = float4(i.uv.r, i.uv.g, 0, 1); return color; } PS:TEXCOORD0 TEXCOORD1里的uv是什么? 纹理坐标,也就是俗称的UV,是顶点数据的一部分。 在建模软件比如3D Max中,建模时有个过程俗称展开UV,其实就是指定模型每个顶点的UV。因为纹理可以有不止一张,所以UV也可以有不止一组。 建模软件导出的模型数据自然也包含顶点的UV数据,在引擎中渲染模型之前,创建顶点缓冲时,包含了UV信息的顶点数组被复制到顶点缓冲里用于接下来的渲染。 在渲染过程中,vs阶段的输入就是(当前)顶点数据,包括各组UV。当然你可以选择性使用,比如说你只需要一组UV,就可以只选择绑定一个TEXCOORD0 texcoord0和texcoord1分别表示两层UV,有时候我们模型上的贴图需要多个图片一起贴在一处,那么贴两层就会有两层UV。 参考<>第五章 参考 https://zhuanlan.zhihu.com/p/113237579 SV_Target介绍 参考 https://zhuanlan.zhihu.com/p/85594617 shader简单着色编写 参考 https://docs.unity3d.com/Manual/SL-VertexProgramInputs.html 官方实例及API介绍 参考 https://www.zhihu.com/question/353273260/answer/876813032 TEXCOORD0 的uv是什么 参考 https://www.cnblogs.com/leeplogs/p/7339097.html TEXCOORD0 参考 https://www.cnblogs.com/jiahuafu/p/6237859.html FIXED类型的数据
来源:OSCHINA
发布时间:2020-05-14 11:50:00
什么是服务器日志 ? 服务器日志要怎么看 ? 什么是服务器日志 虽然现在很多站长懂得做搜索排名知识 , 但是懂得 SEO, 并不代表就懂得服务器日志了 , 那么服务器日志是什么呢 ? 其实 , 服务器日志 (server log) 是一个或多个由服务器自动创建和维护的日志文件 , 其中包含其所执行活动的列表 。 简而言之 , 服务器日记就是记录网站被访问的全过程 , 通过服务器日志 , 站长就可以知道什么时间到什么时间有哪些人来过 , 并且还知道什么搜索引擎来过 , 有没有收录你的网页 , 从你的网站工作的第一天你的日记就有了 。 假如你想做好 SEO, 那么你就要好好的了解下服务器日志了 , 因为它可以让你更了解搜索引擎爬虫 。 服务器日志怎么看 1、 开始 —— 管理工具 —— 事件查看器 —— 系统或者控制面板 —— 管理工具 —— 事件查看器 —— 系统 。 2、 在远程客户端 , 运行 IE 浏览器 , 在地址栏中输入 “https://Win2003 服务器 IP 地址 :8098”, 如 “https://192.168.1.1:8098”。 在弹出的登录对话框中输入管理员的用户名和密码 , 点击 “ 确定 ” 按钮即可登录 Web 访问接口管理界面 。 接着在 “ 欢迎使用 ” 界面中点击 “ 维护 ” 链接 , 切换到 “ 维护 ” 管理页面 , 然后点击 “ 日志 ” 链接进入 。 到日志管理页面后 , 在日志管理页面中 , 管理员可以查看 、 下载或清除 windows 2003 服务器日志 。 选择系统日志可进行查看 , 并且在日志管理页面中可列出 windows 2003 服务器的所有日志分类 , 如应用程序日志 、 安全日志 、 系统日志 、Web 管理日志等 。
来源:OSCHINA
发布时间:2020-05-13 17:24:00
一般来讲,配音分为广义和狭义之分。广义的配音就是指的是音乐、动效、音响、 游戏配音等等的处理。而狭义的配音则指专门为对白、独白、内心独白、解说和旁白等语言的后期配制而进行的一系列创作活动。   影视配音和动画配音都是配音艺术,但是,这两种配音类型的创作依据却是不一样的。接下来,我们就一起了解一下影视配音和动画配音创作依据有哪些不同。 影视配音,简单来说就是替前面的演员说话,由配音员为已经完成了的电视剧中的某一人物录制台词,通过后期配音的方法准确、生动的塑造出人物形象。影视剧配音以演员的形象为依据,需要对作品有深刻的感悟和体验,是一种再创造的过程。   动画配音是配音演员以文字形象和动画设计师绘出的画面形象,也就是视觉形象为依据,通过自身对作品的深刻理解,运用自己的声音展现动画人物造型的个性,把握语言节奏的艺术创造,而不是再创造。但是,动画配音是一种假定的艺术,不受人物形象的限制,不需要模仿生活,所以,动画配音是不会受到任何物质现实的制约。   影视剧配音和动画配音的创作依据是不同的,影视剧配音员需要把握表演者的风格,具有很强的约束性,而动画配音员则没有那么多制约。
来源:OSCHINA
发布时间:2020-05-13 12:01:00
「Creator星球游戏开发社区公众号」有2年半了,一直以游戏开发技术内容为主,对于游戏开发如何挣钱还是一个小孩子。 晓衡在不断的探索中成长,计划将游戏开发、运营、挣钱的整个完整流程揉碎,掰开了一点点学习,并与大家分享其中心得!下图是晓衡粗解的小游戏开发运营挣钱模型。 长期以来,个人开发者们主要是停留在立项、开发阶段,其中开发又涉及程序、美术、策划以及测试。 大多数个人开发者,游戏一旦上线效果不好,很可能快速放弃,又开始了一个新的项目,反复轮回,徘徊在放弃的边缘! 最近,有过两款开发经验的开发者跟晓衡沟通,除了微店源码服务外,能否帮忙对接流量服务,满足前期游戏调优和后期游戏运营需要。 游戏调优是个怎么样的阶段呢?不要小看这个,他是链接游戏开发与运营的重要环节! 调优的内容大概包括: 游戏性能调优 分享裂变调优 用户留存调优 广告收益调优 当我们完成一款游戏的核心主体功能时,就可以发布上线,这时游戏可以被玩家接触到,想当于我们的种子用户,我们需要获得玩家的反馈,对游戏进行从技术到留存,再到广收益等调整。 但是在微信平台上有一个非常残酷的问题:“没有自然流量”!!!很多开发者连最初的1000个用户都需要较长的时间和巨大的精力才能达成。 由于样本量太低,并不能指导开发者对游戏的调整,从而导致很多具有潜力的游戏沦为数字垃圾安静的躺在开发者的电脑里。 经过晓衡的不懈努力终于对接到一家以儿童机器人(类似智能音箱)运营为主的流量资源,有意向寻找合适流量的小游戏主可进行流量合作。欢迎加我微信: z6346289 ,关注我的微信公众号: Creator游戏开发社区 ,晓衡在线等你! 完整原文: https://mp.weixin.qq.com/s/4GpsEidOEcYzf4AHAI6P8A
来源:OSCHINA
发布时间:2020-05-13 09:27:00
如何传一个固定的Vec3数组: osg::Uniform *uniform3 = new osg::Uniform(osg::Uniform::Type::FLOAT_VEC3, "arr3", 8); uniform3->setElement(0, osg::Vec3(1, 0, 0)); uniform3->setElement(1, osg::Vec3(1, 0, 1)); uniform3->setElement(2, osg::Vec3(1, 1, 0)); 与一般的写法不同, Element的数目是固定的
来源:OSCHINA
发布时间:2020-05-12 17:58:00
常用于摄像机追随玩家 实现平滑跟踪 void Update () { if (player != null) { float x = Mathf.Lerp (this.transform.position.x , player.position.x , 0.2f); float y = Mathf.Lerp (this.transform.position.y , player.position.y , 0.2f); this.transform.position = new Vector3 (x , y , this.transform.position.z); } }
来源:OSCHINA
发布时间:2020-05-12 16:07:00
我国不同的地方有不同的地方语言,俗称“方言”,近些年来,方言配音在游戏中被广泛应用。不同的方言有其独特之处,不同的方言有不同的发音,声调也有不同。对于声调的方言有什么差异?下面 游戏方言配音小编就给大家说说吧!   现代汉语方言大致分为七大方言区,根据各方言的声调状况,可把它们归并为两大类。 1、北方方言,各地北方话的系统比较一致。大多数地区的声调数目是四个,没有入声调; 2、南方方言,声调数目较多,许多方言都有六七个声调,大多数南方方言都有入声调。 就全国来说,声调差异很大,虽然大部分地区有阴阳上去四类,但表现的调值是不同的。 调类上看,有三类的,也有十类的。而调值的把握不是件很难的事,如果能认清自己方言中的声调,再和普通话的声调做对比。 得出他们之间的对应规律,就会取得事半功倍的效果。 举例说明: 比如阴平在你的方言里是多少,和普通话有什么区别?普通话中“天”这个阴平是高平调,而天津人发“天”时也是阴平,但调值是低平调,找到规律后就好改了。 方言是我国的语言多样性的特色,我们来自不同的地区,不同地区的人会说不同的地方语言,这是中国方言的一大特色。由于方言的声调具有差异。只有了解这些声调知识,才能让方言配音游刃有余。
来源:OSCHINA
发布时间:2020-05-12 11:43:00
使用scala的开发的软件 spark , apache flink , kafka 等 scala 与java 区别例子 Scala vs Java HelloWorld public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World..."); } } Scala每行代码并不强求使用;结束,但是Java是必须的 object HelloWorld { def main(args : Array[String]) { println("Hello World...") } } sacla 语法 数字类型转换 判断是否属于某个数字类型 val vs var val:值 final val 值名称:类型 = xxx var:变量 var 值名称:类型 = xxx Scala基本数据类型 Byte/Char Short/Int/Long/Float/Double Boolean lazy scala 函数 函数/方法的定义 def 方法名(参数名:参数类型): 返回值类型 = { // 括号内的叫做方法体 // 方法体内的最后一行为返回值,不需要使用return } 默认参数: 在函数定义时,允许指定参数的默认值 $SPARK_HOME/conf/spark-defaults.conf 可变参数: JDK5+ : 可变参数 循环表达式 to Range until 定义与使用 命名参数 调用参数可以顺序不一样 可变参数 即参数可以有任意多个 /** * 可变参数 ,可以有多个 类似 java 的 ... * @param numbers */ def sum(numbers:Int*): Unit ={ 表达式 if else 等 循环表达式 def loop(): Unit ={ var num1 = 1 to 10 var num2 = 1.to(10) // 与 1 to 10 写法一样的 for (elem <- num1) { println(elem) } for( i <- 1 to 10 if i%2 == 0){ println(i) } val courses=Array("语文","数学") for (elem <- courses) { } courses.foreach(f => println(f)) courses.foreach(f => { println(f) } ) } 面向对象 占位符 _ 下划线,即 使用默认值,不赋值 类的定义与使用 package org.train.scala.c4 object SimpleApp { def main(args: Array[String]): Unit = { val person=new People(); person.name="ss" println(person.name+ person.age) person.eat() person.printInfo() } } class People{ //变量,自动生成 get /set 方法 var name="" var nick : String =_ //不可变常量,没有 get/set val age:Int = 10 def eat(): String={ name +"eat ..." } //只能在class 内可以访问,类外不可以 private [this] val gender="male" // [this] 可去掉: private val gender="male" def printInfo(): Unit ={ println(gender) } } 构造器 package com.imooc.scala.course04 object ConstructorApp { def main(args: Array[String]): Unit = { // val person = new Person("zhangsan", 30) // println(person.name + " : " + person.age + " : " + person.school) // // // val person2 = new Person("PK", 18, "M") // println(person2.name + " : " // + person2.age + " : " // + person2.school + " :" // + person2.gender // ) val student = new Student("PK", 18, "Math") println(student.name + " : " + student.major) println(student) } } // 主构造器 class Person(val name:String, val age:Int) { println("Person Constructor enter....") val school = "ustc" var gender:String = _ // 附属构造器 def this(name:String, age:Int, gender:String) { this(name, age) // 附属构造器的第一行代码必须要调用主构造器或者其他附属构造器 this.gender = gender } println("Person Constructor leave....") } class Student(name:String, age:Int, var major:String) extends Person(name, age) { println("Person Student enter....") //重写 override val school = "peking" override def toString: String = "Person: override def toString :" + school println("Person Student leave....") } 抽象类 abstract package org.train.scala.c4 object AbstractApp { def main(args: Array[String]): Unit = { new Stude2().speak; } } /** * 只有定义,没有实现 */ abstract class Person2{ //抽象方法 def speak //抽象的属性 val name:String } class Stude2 extends Person2{ override def speak: Unit = { println("sk") } override val name: String = _ } 伴生类和伴生对象 /** * 伴生类和伴生对象 * 如果有一个class,还有一个与class同名的object * 那么就称这个object是class的伴生对象,class是object的伴生类 */ //伴生类 (相对 object ApplyTest ) class ApplyTest{ } //伴生对象 (相对class ApplyTest 来说的 ) object ApplyTest{ } apply 方法 package com.imooc.scala.course04 object ApplyApp { def main(args: Array[String]): Unit = { // for(i <- 1 to 10) { // ApplyTest.incr // } // // println(ApplyTest.count) // 10 说明object本身就是一个单例对象 val b = ApplyTest() // ==> Object.apply println("~~~~~~~~~~~") val c = new ApplyTest() println(c) c() // 类名() ==> Object.apply // 对象() ==> Class.apply } } /** * 伴生类和伴生对象 * 如果有一个class,还有一个与class同名的object * 那么就称这个object是class的伴生对象,class是object的伴生类 */ class ApplyTest{ def apply() = { println("class ApplyTest apply....") } } object ApplyTest{ println("Object ApplyTest enter....") var count = 0 def incr = { count = count + 1 } // 最佳实践:在Object的apply方法中去new Class def apply():ApplyTest = { println("Object ApplyTest apply....") // 在object中的apply中new class new ApplyTest } println("Object ApplyTest leave....") } case class // 通常用在模式匹配 object CaseClassApp { def main(args: Array[String]): Unit = { println(Dog("wangcai").name) } } // case class不用new case class Dog(name:String) case class Dog2(name: String) trait 接口 Java/Scala OO 封装:属性、方法封装到类中 Person: private int id, String name, Date birthday..... getter/setter eat、sleep.... 继承:父类和子类之间的关系 User extends Person exam..... 多态:***** 父类引用指向子类对象 精髓所在 开发框架的基石 Person person = new Person(); User user = new User(); Person person = new User(); Trait xxx extends ATrait with BTrait class SparkConf(loadDefaults: Boolean) extends Cloneable with Logging with Serializable ... .... 集合 数组 package com.imooc.scala.course05 object ArrayApp extends App{ // val a = new Array[String](5) // a.length // a(1) = "hello" // // val b = Array("hadoop", "spark", "storm") // // val c = Array(2,3,4,5,6,7,8,9) // c.sum // c.min // c.max // // c.mkString(",") // 可变数组 val c = scala.collection.mutable.ArrayBuffer[Int]() c += 1 c += 2 c += (3,4,5) c ++= Array(6,7,8) c.insert(0,0) c.remove(1) c.remove(0,3) c.trimEnd(2) // for(i <- 0 until c.length) { // println(c(i)) // } // for(ele <- c) { // println(ele) // } for(i <- (0 until c.length).reverse) { println(c(i)) } // println(c.toArray.mkString) } List nil 就是 一个空的集合的意思 package com.imooc.scala.course05 object ListApp extends App{ // val l = List(1,2,3,4,5) // // // val l5 = scala.collection.mutable.ListBuffer[Int]()//可变list // l5 += 2 // l5 += (3,4,5) // l5 ++= List(6,7,8,9) // // // l5 -= 2 // l5 -= 3 // l5 -= (1, 4) // l5 --= List(5,6,7,8) // // println(l5) // // // l5.isEmpty // l5.head // l5.tail def sum(nums:Int*):Int = { if(nums.length == 0) { 0 } else { nums.head + sum(nums.tail:_*) // _* 类型自动转换 } } // val set = scala.collection.mutable.Set[Int]() // set += 1 // set += (1,1) println(sum()) println(sum(1,2,3,4)) } head 就是 第一个, 而 tail 就是 除了 第一个数据的 之后的所有数据 set 无序,不可重复的 list ,用法与 list 类似 // val set = scala.collection.mutable.Set[Int]() //可变set // set += 1 // set += (1,1) map package com.imooc.scala.course05 import scala.collection.mutable object MapApp extends App{ val a = Map("PK" -> 18, "zhangsan" -> 30) val b = Map("PK" -> 18, "zhangsan" -> 30) // val c = mutable.HashMap[String,Int]() // b.getOrElse("PK", 9) // for((key,value) <- b) { // println(key + " : " + value ) // } // for(key <- b.keySet) { // println(key + " : " + b.getOrElse(key, 9)) // } // for(value <- b.values) { // println(value) // } for((key,_) <- b) { println(key + " : " + b.getOrElse(key, 9) ) } } Option , Some, None object OptionApp extends App { val m = Map(1 -> 2) // println(m(1)) // println(m(2)) // println(m.get(1).get) println(m.getOrElse(2, "None")) } /** * case object None extends Option[Nothing] { def isEmpty = true def get = throw new NoSuchElementException("None.get") } final case class Some[+A](x: A) extends Option[A] { def isEmpty = false def get = x } */ Option 是一个抽象类 , 实现类是 Some ,None Some 代表存在, None 代表空 Tuple // 元组:(.......) object TupleApp extends App{ val a = (1,2,3,4,5) for(i <- 0 until(a.productArity)) { println(a.productElement(i)) } //下标从 1 开始,不是 0 val hostPort = ("localhost",8080) hostPort._1 hostPort._2 } 模式匹配 即 match case 类似 java 的 switch case 用法,但是比JAVA的强大很多 模式匹配 Java: 对一个值进行条件判断,返回针对不同的条件进行不同的处理 变量 match { case value1 => 代码1 case value2 => 代码2 ..... case _ => 代码N } 基本模式匹配 def grade(grade : String): Unit = { grade match{ case "A" => println("99。。。") case "B" => println("89。。。") case _ => println("00。。。") } } 条件匹配 def grade(grade : String): Unit = { grade match{ case "A" => println("99。。。") case "B" => println("89。。。") //条件匹配, 双重过滤 case _ if(grade=="C") => println("cc。。。") case _ => println("00。。。") } } Array 模式匹配 def greeting(array:Array[String]): Unit = { array match { case Array("zhangsan") => println("Hi:zhangsan") //数组必须是 zhangsan case Array(x,y) => println("Hi:" + x + " , " + y) // 数组可以有任意的2个 case Array("zhangsan", _*) => println("Hi:zhangsan and other friends...")//多个内容, case _ => println("Hi: everybody...") } } List 模式匹配 def greeting(list:List[String]): Unit = { list match { case "zhangsan"::Nil => println("Hi:zhangsan") //只有 zhangsan case x::y::Nil => println("Hi:" + x + " , " + y) //只有2个 任意元素 case "zhangsan"::tail => println("Hi: zhangsan and other friends...") // 多个 case _ => println("Hi:everybody....") } } 类型匹配 def matchType(obj:Any): Unit = { obj match { case x:Int => println("Int") case x:String => println("String") case m:Map[_,_] => m.foreach(println) // map , key value 任意 case _ => println("other type") } } 异常处理 //IO val file = "test.txt" try{ // open file // use file val i = 10/0 println(i) } catch { case e:ArithmeticException => println("除数不能为0..") case e:Exception => println(e.getMessage) } finally { // 释放资源,一定能执行: close file } case class 模式匹配 def caseclassMatch(person:Person): Unit = { person match { case CTO(name,floor) => println("CTO name is: " + name + " , floor is: " + floor) case Employee(name,floor) => println("Employee name is: " + name + " , floor is: " + floor) case _ => println("other") } } class Person case class CTO(name:String, floor:String) extends Person case class Employee(name:String, floor:String) extends Person case class Other(name:String) extends Person caseclassMatch(CTO("PK", "22")) caseclassMatch(Employee("zhangsan", "2")) caseclassMatch(Other("other")) Some None 模式匹配 val grades = Map("PK"->"A", "zhangsan"-> "C") def getGrade(name:String): Unit = { val grade = grades.get(name) grade match { case Some(grade) => println(name + ": your grade is :" + grade) case None => println("Sorry....") } } 高级函数 字符串高级操作 object StringApp extends App { val s = "Hello:" val name = "PK" // println(s + name) println(s"Hello:$name") val team = "AC Milan" // 插值 println(s"Hello:$name, Welcome to $team") val b = """ |这是一个多行字符串 |hello |world |PK """.stripMargin println(b) } 查值,就是 s + “ xx $属性名 ” , 必须是 s 开头的, 属性名 必须带上 $ 匿名函数 匿名参数传递给 对应函数 匿名函数传递给 add 函数 , 或者 匿名函数传递给变量都是可以的 currying 函数 // 将原来接收两个参数的一个函数,转换成2个 def sum(a:Int, b:Int) = a+b println(sum(2,3)) // 改成 currying 函数 def sum2(a:Int)(b:Int) = a + b println(sum2(2)(3)) 即将参数给 拆开 高阶函数 package com.imooc.scala.course07 /** * 匿名函数: 函数是可以命名的,也可以不命名 * (参数名:参数类型...) => 函数体 */ object FunctionApp extends App { // def sayHello(name:String): Unit = { // println("Hi: " + name) // } // // sayHello("PK") // // 将原来接收两个参数的一个函数,转换成2个 // def sum(a:Int, b:Int) = a+b // println(sum(2,3)) // // def sum2(a:Int)(b:Int) = a + b // println(sum2(2)(3)) val l = List(1, 2, 3, 4, 5, 6, 7, 8) //map: 逐个去操作集合中的每个元素 // l.map((x: Int) => x + 1) // l.map((x) => x * 2) // l.map(x => x * 2) // l.map(_ * 2).foreach(println) // l.map(_ * 2).filter(_ > 8).foreach(println) // 1+2 3+3 6+4 10+5 // l.reduce(_+_) // 即 求和 sum l.reduceLeft(_-_) l.reduceRight(_-_) l.fold(0)(_-_) println( l.fold(100)(_ + _)) // 从 100 开始 以此 求和 val f = List(List(1,2),List(3,4),List(5,6)) f.flatten // // 压扁 合成一个 List // flatMap f.map(_.map(_*2)) //每个元素 乘以2 f.flatMap(_.map(_*2)) // 并合成一个 List, 每个元素 乘以2 , val txt = scala.io.Source.fromFile("/Users/rocky/imooc/hello.txt").mkString // println(txt) val txts = List(txt) // 如何使用scala来完成wordcount统计 // 链式编程:Hibernate、Spark txts.flatMap(_.split(",")).map(x => (x,1)) //... .foreach(println) } 偏函数 /** * 偏函数:被包在花括号内没有match的一组case语句 */ object PartitalFunctionApp extends App { val names = Array("Akiho Yoshizawa", "YuiHatano", "Aoi Sola") val name = names(Random.nextInt(names.length)) name match { case "Akiho Yoshizawa" => println("吉老师...") case "YuiHatano" => println("波老师....") case _ => println("真不知道你们在说什么....") } // A 输入参数类型 B 输出参数类型 // A 输入参数类型 第一个参数, B 输出参数类型 第二个参数, def sayChinese:PartialFunction[String,String] = { case "Akiho Yoshizawa" => "吉老师..." case "YuiHatano" => "波老师...." case _ => "真不知道你们在说什么...." } println(sayChinese("Akiho Yoshizawa")) } 隐式转换 即类似 java 的 动态代理 AOP 需求:为一个已存在的类添加一个新的方法 Java:动态代理 Scala:隐式转换 双刃剑 Spark/Hive/MR.... 调优 import java.io.File //import ImplicitAspect._ object ImplicitApp { // 定义隐式转换函数即可 // implicit def man2superman(man:Man):Superman = new Superman(man.name) // val man = new Man("PK") // man.fly() // implicit def file2RichFile(file: File): RichFile = new RichFile(file) // val file = new File("/Users/rocky/imooc/hello.txt") // val txt = file.read() // println(txt) } //class Man(val name: String) { // def eat(): Unit = { // println(s"man[ $name ] eat ..... ") // } //} // //class Superman(val name: String) { // def fly(): Unit = { // println(s"superman[ $name ] fly ..... ") // } //} class RichFile(val file: File) { def read() = { scala.io.Source.fromFile(file.getPath).mkString } } 隐式参数 object ImplicatParam extends App { def testParam(implicit name:String): Unit ={ println(name) } // testParam("aa") // implicit val name2 ="bb" // testParam// 不会报错,默认使用 name2 // implicit val s1="s1" // implicit val s2="s2" // testParam // 多个 会报错,不知道用 s1 还是 s2 } 隐式类 object ImplicitClassApp extends App { implicit class Calcuotor(x:Int){ def add(a:Int) = a+x } println(1.add(3)) // 可以使用调用 Calcuotor 方法 } scala 操作外部数据 读取文件及网络数据 import scala.io.Source object FileApp { def main(args: Array[String]): Unit = { val file = Source.fromFile("/Users/rocky/imooc/hello.txt")(scala.io.Codec.ISO8859) def readLine(): Unit ={ for(line <- file.getLines()){ println(line) } } //readLine() def readChar(): Unit ={ for(ele <- file) { println(ele) } } // readChar() def readNet(): Unit ={ val file = Source.fromURL("http://www.baidu.com") for(line <- file.getLines()){ println(line) } } readNet() } } 读取mysql package org.train.scala.c9 import java.sql.{Connection, DriverManager} object MySQLApp { def main(args: Array[String]): Unit = { val url = "jdbc:mysql://localhost:3306/mysql" val username = "root" val password = "123456" var connection:Connection = null try{ // make the connection classOf[com.mysql.jdbc.Driver] connection = DriverManager.getConnection(url, username, password) // create the statement, and run the select query val statement = connection.createStatement() val resultSet = statement.executeQuery("select host,user from user") while(resultSet.next()){ val host = resultSet.getString("host") val user = resultSet.getString("user") println(s"$host, $user") } } catch { case e:Exception => e.printStackTrace() } finally { // free if(connection == null) { connection.close() } } } } 与java 写法一样的 操作xml
FIX4.2 Test
package org.train.scala.c9 import java.io.{FileInputStream, InputStreamReader} import scala.xml.XML object XMLApp { def main(args: Array[String]): Unit = { // loadXML() // readXMLAttr() updateXML() } def updateXML(): Unit ={ val xml = XML.load(this.getClass.getClassLoader.getResource("books.xml")) // println(xml) val bookMap = scala.collection.mutable.HashMap[String,String]() (xml \ "book").map(x => { val id = (x \ "@id").toString() val name = (x \ "name").text.toString bookMap(id) = name }) // for((key,value) <- bookMap) { // println(s"$key : $value") // } val newXml = {bookMap.map(updateXmlFile)} // println(newXml) XML.save("newBooks.xml", newXml) } def updateXmlFile(ele:(String,String)) = { val (id, oldName) = ele {oldName + " Programming"} } def readXMLAttr(): Unit = { val xml = XML.load(this.getClass.getClassLoader.getResource("pk.xml")) // println(xml) // header/field // val headerField = xml \ "header" \ "field" // println(headerField) // all field // val fields = xml \\ "field" // for (field <- fields) { // println(field) // } // header/field/name // val fieldAttributes = (xml \ "header" \ "field").map(_ \ "@name") // val fieldAttributes = (xml \ "header" \ "field" \\ "@name") // for (fieldAttribute <- fieldAttributes) { // println(fieldAttribute) // } //name="Logon" message // val filters = (xml \\ "message") // .filter(_.attribute("name").exists(_.text.equals("Logon"))) // val filters = (xml \\ "message") // .filter(x => ((x \ "@name").text).equals("Logon")) // for (filter <- filters) { // println(filter) // } // header/field content (xml \ "header" \ "field") .map(x => (x \ "@name", x.text, x \ "@required")) .foreach(println) } def loadXML(): Unit = { // val xml = XML.load(this.getClass.getClassLoader.getResource("test.xml")) // println(xml) // val xml = XML.load(new FileInputStream("/Users/rocky/source/scala-train/src/main/resources/test.xml")) // println(xml) val xml = XML.load( new InputStreamReader( new FileInputStream("/Users/rocky/source/scala-train/src/main/resources/test.xml") ) ) println(xml) } } scala 结合spring boot 参考 https://gitee.com/odilil/boot-scala?_from=gitee_search bean 注入 还可以: @Autowired val metaTableRepository : MetaTableRepository = null // 不能使用 _ 下划线 占位符来标识
来源:OSCHINA
发布时间:2020-06-03 11:37:00
特别要注意第二窗体对应的view的fxml文件路径写法(view和controller都放在各自的包下): @FXML protected void show2Action() throws IOException { Stage stage = new Stage(); stage.setTitle("第二个窗体实验"); AnchorPane pane = FXMLLoader.load(getClass().getResource("../javafxcontroller2.fxml"));//要注意fxml文件的类路径写法(与本JavaFXController.java的相对关系) Scene scene = new Scene(pane,300,400); stage.setScene(scene); stage.initModality(Modality.APPLICATION_MODAL);//模式窗体 stage.setOnCloseRequest(new EventHandler() { @Override public void handle(WindowEvent event) { System.out.println("window close event"); if(i++<2){ event.consume(); // i=0; } else i=0; } }); stage.show(); // stage.showAndWait(); //等待stage关闭之后才能继续运行 System.out.println("stage.showAndWait"); // stage.show(); }
来源:OSCHINA
发布时间:2020-06-03 11:29:00
1.触发条件: MinorGC:当Eden去满了就会触发。 FullGC: 1.调用System.gc()后,建议FullGC,但不一定会执行; 2.老年代空间满了; 3.方法区满了; 4.调用MinorGC后进入老年代的平均大小大于老年代可用内存;
来源:OSCHINA
发布时间:2020-05-31 23:35:00
一、ThreadLocal简介   多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。   ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示 二、ThreadLocal简单使用   下面的例子中,开启两个线程,在每个线程内部设置了本地变量的值,然后调用print方法打印当前本地变量的值。如果在打印之后调用本地变量的remove方法会删除本地内存中的变量,代码如下所示 1 package test; 2 3 public class ThreadLocalTest { 4 5 static ThreadLocal localVar = new ThreadLocal<> (); 6 7 static void print(String str) { 8 // 打印当前线程中本地内存中本地变量的值 9 System.out.println(str + " :" + localVar.get()); 10 // 清除本地内存中的本地变量 11 localVar.remove(); 12 } 13 14 public static void main(String[] args) { 15 Thread t1 = new Thread( new Runnable() { 16 @Override 17 public void run() { 18 // 设置线程1中本地变量的值 19 localVar.set("localVar1" ); 20 // 调用打印方法 21 print("thread1" ); 22 // 打印本地变量 23 System.out.println("after remove : " + localVar.get()); 24 } 25 }); 26 27 Thread t2 = new Thread( new Runnable() { 28 @Override 29 public void run() { 30 // 设置线程1中本地变量的值 31 localVar.set("localVar2" ); 32 // 调用打印方法 33 print("thread2" ); 34 // 打印本地变量 35 System.out.println("after remove : " + localVar.get()); 36 } 37 }); 38 39 t1.start(); 40 t2.start(); 41 } 42 } 下面是运行后的结果: 三、ThreadLocal的实现原理   下面是ThreadLocal的类图结构,从图中可知:Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null ,只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们(后面我们会查看这两个方法的源码)。除此之外,和我所想的不同的是,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面(前面也说过,该变量是Thread类的变量)。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量。下面我们通过查看ThreadLocal的set、get以及remove方法来查看ThreadLocal具体实怎样工作的   1、set方法源码 1 public void set(T value) { 2 //(1) 获取当前线程(调用者线程) 3 Thread t = Thread.currentThread(); 4 //(2) 以当前线程作为key值,去查找对应的线程变量,找到对应的map 5 ThreadLocalMap map = getMap(t); 6 //(3) 如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值 7 if (map != null ) 8 map.set( this , value); 9 //(4) 如果map为null,说明首次添加,需要首先创建出对应的map 10 else 11 createMap(t, value); 12 }   在上面的代码中,(2)处调用getMap方法获得当前线程对应的threadLocals(参照上面的图示和文字说明),该方法代码如下 ThreadLocalMap getMap(Thread t) { return t.threadLocals; // 获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上 }   如果调用getMap方法返回值不为null,就直接将value值设置到threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回null说明是第一次调用set方法(前面说到过,threadLocals默认值为null,只有调用set方法的时候才会创建map),这个时候就需要调用createMap方法创建threadLocals,该方法如下所示 1 void createMap(Thread t, T firstValue) { 2 t.threadLocals = new ThreadLocalMap( this , firstValue); 3 }   createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中。   2、get方法源码   在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。 1 public T get() { 2 // (1)获取当前线程 3 Thread t = Thread.currentThread(); 4 // (2)获取当前线程的threadLocals变量 5 ThreadLocalMap map = getMap(t); 6 // (3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值 7 if (map != null ) { 8 ThreadLocalMap.Entry e = map.getEntry( this ); 9 if (e != null ) { 10 @SuppressWarnings("unchecked" ) 11 T result = (T)e.value; 12 return result; 13 } 14 } 15 // (4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量 16 return setInitialValue(); 17 } 18 19 private T setInitialValue() { 20 // protected T initialValue() {return null;} 21 T value = initialValue(); 22 // 获取当前线程 23 Thread t = Thread.currentThread(); 24 // 以当前线程作为key值,去查找对应的线程变量,找到对应的map 25 ThreadLocalMap map = getMap(t); 26 // 如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值 27 if (map != null ) 28 map.set( this , value); 29 // 如果map为null,说明首次添加,需要首先创建出对应的map 30 else 31 createMap(t, value); 32 return value; 33 }   3、remove方法的实现   remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量 1 public void remove() { 2 // 获取当前线程绑定的threadLocals 3 ThreadLocalMap m = getMap(Thread.currentThread()); 4 // 如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量 5 if (m != null ) 6 m.remove( this ); 7 }   4、如下图所示:每个线程内部有一个名为threadLocals的成员变量,该变量的类型为ThreadLocal.ThreadLocalMap类型(类似于一个HashMap),其中的key为当前定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值。每个线程的本地变量存放在自己的本地内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。 四、ThreadLocal不支持继承性   同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的) 1 package test; 2 3 public class ThreadLocalTest2 { 4 5 // (1)创建ThreadLocal变量 6 public static ThreadLocal threadLocal = new ThreadLocal<> (); 7 8 public static void main(String[] args) { 9 // 在main线程中添加main线程的本地变量 10 threadLocal.set("mainVal" ); 11 // 新创建一个子线程 12 Thread thread = new Thread( new Runnable() { 13 @Override 14 public void run() { 15 System.out.println("子线程中的本地变量值:"+ threadLocal.get()); 16 } 17 }); 18 thread.start(); 19 // 输出main线程中的本地变量值 20 System.out.println("mainx线程中的本地变量值:"+ threadLocal.get()); 21 } 22 } 五、InheritableThreadLocal类   在上面说到的ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能,下面是该类的源码 1 public class InheritableThreadLocal extends ThreadLocal { 2 3 protected T childValue(T parentValue) { 4 return parentValue; 5 } 6 7 ThreadLocalMap getMap(Thread t) { 8 return t.inheritableThreadLocals; 9 } 10 11 void createMap(Thread t, T firstValue) { 12 t.inheritableThreadLocals = new ThreadLocalMap( this , firstValue); 13 } 14 }   从上面代码可以看出,InheritableThreadLocal类继承了ThreadLocal类,并重写了childValue、getMap、createMap三个方法。其中createMap方法在被调用(当前线程调用set方法时得到的map为null的时候需要调用该方法)的时候,创建的是inheritableThreadLocal而不是threadLocals。同理,getMap方法在当前调用者线程调用get方法的时候返回的也不是threadLocals而是inheritableThreadLocal。   下面我们看看重写的childValue方法在什么时候执行,怎样让子线程访问父线程的本地变量值。我们首先从Thread类开始说起 1 private void init(ThreadGroup g, Runnable target, String name, 2 long stackSize) { 3 init(g, target, name, stackSize, null , true ); 4 } 5 private void init(ThreadGroup g, Runnable target, String name, 6 long stackSize, AccessControlContext acc, 7 boolean inheritThreadLocals) { 8 // 判断名字的合法性 9 if (name == null ) { 10 throw new NullPointerException("name cannot be null" ); 11 } 12 13 this .name = name; 14 // (1)获取当前线程(父线程) 15 Thread parent = currentThread(); 16 // 安全校验 17 SecurityManager security = System.getSecurityManager(); 18 if (g == null ) { // g:当前线程组 19 if (security != null ) { 20 g = security.getThreadGroup(); 21 } 22 if (g == null ) { 23 g = parent.getThreadGroup(); 24 } 25 } 26 g.checkAccess(); 27 if (security != null ) { 28 if (isCCLOverridden(getClass())) { 29 security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); 30 } 31 } 32 33 g.addUnstarted(); 34 35 this .group = g; // 设置为当前线程组 36 this .daemon = parent.isDaemon(); // 守护线程与否(同父线程) 37 this .priority = parent.getPriority(); // 优先级同父线程 38 if (security == null || isCCLOverridden(parent.getClass())) 39 this .contextClassLoader = parent.getContextClassLoader(); 40 else 41 this .contextClassLoader = parent.contextClassLoader; 42 this .inheritedAccessControlContext = 43 acc != null ? acc : AccessController.getContext(); 44 this .target = target; 45 setPriority(priority); 46 //(2) 如果父线程的inheritableThreadLocal不为null 47 if (inheritThreadLocals && parent.inheritableThreadLocals != null ) 48 //(3) 设置子线程中的inheritableThreadLocals为父线程的inheritableThreadLocals 49 this .inheritableThreadLocals = 50 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 51 this .stackSize = stackSize; 52 53 tid = nextThreadID(); 54 }   在init方法中,首先(1)处获取了当前线程(父线程),然后(2)处判断当前父线程的inheritableThreadLocals是否为null,然后调用createInheritedMap将父线程的inheritableThreadLocals作为构造函数参数创建了一个新的ThreadLocalMap变量,然后赋值给子线程。下面是createInheritedMap方法和ThreadLocalMap的构造方法 1 static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { 2 return new ThreadLocalMap(parentMap); 3 } 4 5 private ThreadLocalMap(ThreadLocalMap parentMap) { 6 Entry[] parentTable = parentMap.table; 7 int len = parentTable.length; 8 setThreshold(len); 9 table = new Entry[len]; 10 11 for ( int j = 0; j < len; j++ ) { 12 Entry e = parentTable[j]; 13 if (e != null ) { 14 @SuppressWarnings("unchecked" ) 15 ThreadLocal key = (ThreadLocal ) e.get(); 16 if (key != null ) { 17 // 调用重写的方法 18 Object value = key.childValue(e.value); 19 Entry c = new Entry(key, value); 20 int h = key.threadLocalHashCode & (len - 1 ); 21 while (table[h] != null ) 22 h = nextIndex(h, len); 23 table[h] = c; 24 size++ ; 25 } 26 } 27 } 28 }   在构造函数中将父线程的inheritableThreadLocals成员变量的值赋值到新的ThreadLocalMap对象中。返回之后赋值给子线程的inheritableThreadLocals。总之,InheritableThreadLocals类通过重写getMap和createMap两个方法将本地变量保存到了具体线程的inheritableThreadLocals变量中,当线程通过InheritableThreadLocals实例的set或者get方法设置变量的时候,就会创建当前线程的inheritableThreadLocals变量。而父线程创建子线程的时候,ThreadLocalMap中的构造函数会将父线程的inheritableThreadLocals中的变量复制一份到子线程的inheritableThreadLocals变量中。 六、从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题 1、基础概念   首先我们先看看ThreadLocalMap的类图,在前面的介绍中,我们知道ThreadLocal只是一个工具类,他为用户提供get、set、remove接口操作实际存放本地变量的threadLocals(调用线程的成员变量),也知道threadLocals是一个ThreadLocalMap类型的变量,下面我们来看看ThreadLocalMap这个类。在此之前,我们回忆一下Java中的四种引用类型,相关GC只是参考前面系列的文章( JVM相关 ) ①强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。 ②软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中 ③弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null ④虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象) 2、分析ThreadLocalMap内部实现   上面我们知道ThreadLocalMap内部实际上是一个Entry数组 ,我们先看看Entry的这个内部类 1 /** 2 * 是继承自WeakReference的一个类,该类中实际存放的key是 3 * 指向ThreadLocal的弱引用和与之对应的value值(该value值 4 * 就是通过ThreadLocal的set方法传递过来的值) 5 * 由于是弱引用,当get方法返回null的时候意味着坑能引用 6 */ 7 static class Entry extends WeakReference> { 8 /** value就是和ThreadLocal绑定的 */ 9 Object value; 10 11 // k:ThreadLocal的引用,被传递给WeakReference的构造方法 12 Entry(ThreadLocal k, Object v) { 13 super (k); 14 value = v; 15 } 16 } 17 // WeakReference构造方法(public class WeakReference extends Reference ) 18 public WeakReference(T referent) { 19 super (referent); // referent:ThreadLocal的引用 20 } 21 22 // Reference构造方法 23 Reference(T referent) { 24 this (referent, null ); // referent:ThreadLocal的引用 25 } 26 27 Reference(T referent, ReferenceQueue queue) { 28 this .referent = referent; 29 this .queue = (queue == null ) ? ReferenceQueue.NULL : queue; 30 }   在上面的代码中,我们可以看出,当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。   考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。   总结:THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。
来源:OSCHINA
发布时间:2020-05-31 23:29:00
很多职场转行人员都会遇见这样那样的困惑与问题,今天小编针对同学们的问题作出了有效建议。 我是非计算机专业出身,可以学软件测试吗? 我年纪太大了,竞争不过年轻人,怎么办? 如果学完找不到工作,怎么办? 我是非计算机专业出身,可以学软件测试吗? 首先我想说下,目前国内基本上没有大学开设软件测试的专业,很多人都有误区,其实计算机专业并不属于软件测试专业。 目前这一行要求的主要还是核心技能,测试行业是属于入门容易,但想走远比较难。所以说在转行前一定要确定自己是否真的对这个行业感兴趣,这个可以让自己持续走下去。 我年纪太大了,竞争不过年轻人,怎么办? (想要超车走捷径可以加入我们Q群(718897738)一起齐头并进加油!) 年龄并不是一个决定因素,重要的还是能力。软件测试岗位更注重的是资历,你的实操经验越多,从业经历越丰富,也就越值钱,所以是“越老越吃香”。 并且软件测试行业的职业生涯是很长远的,入行越久,职位越高,工资越高。
来源:OSCHINA
发布时间:2020-05-31 22:29:06
引用与对象 每种编程语言都有自己操作内存中元素的方式,例如在 C 和 C++ 里是通过指针,而在 Java 中则是通过“引用”。 在 Java 中一切都被视为了对象,但是我们操作的标识符实际上是对象的一个引用(reference)。 //创建一个引用,引用可以独立存在,并不一定需要与一个对象关联 String s; 通过将这个叫“引用”的标识符指向某个对象,之后便可以通过这个引用来实现操作对象了。 String str = new String ( "abc" ); System. out .println(str.toString()); 在 JDK1.2 之前,Java中的定义很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表着一个引用。 Java 中的垃圾回收机制在判断是否回收某个对象的时候,都需要依据“引用”这个概念。 在不同垃圾回收算法中,对引用的判断方式有所不同: 引用计数法:为每个对象添加一个引用计数器,每当有一个引用指向它时,计数器就加1,当引用失效时,计数器就减1,当计数器为0时,则认为该对象可以被回收(目前在Java中已经弃用这种方式了)。 可达性分析算法:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。 JDK1.2 之前,一个对象只有“已被引用”和"未被引用"两种状态,这将无法描述某些特殊情况下的对象,比如,当内存充足时需要保留,而内存紧张时才需要被抛弃的一类对象。 四种引用类型 所以在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。 一,强引用 Java中默认声明的就是强引用,比如: Object obj = new Object (); //只要obj还指向Object对象,Object对象就不会被回收 obj = null ; //手动置null 只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了 二,软引用 软引用是用来描述一些非必需但仍有用的对象。 在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常 。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。 在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。 下面以一个例子来进一步说明强引用和软引用的区别: 在运行下面的Java代码之前,需要先配置参数 -Xms2M -Xmx3M,将 JVM 的初始内存设为2M,最大可用内存为 3M。 首先先来测试一下强引用,在限制了 JVM 内存的前提下,下面的代码运行正常 public class TestOOM { public static void main (String[] args) { testStrongReference(); } private static void testStrongReference () { // 当 new byte为 1M 时,程序运行正常 byte [] buff = new byte [ 1024 * 1024 * 1 ]; } } 但是如果我们将 byte [] buff = new byte [ 1024 * 1024 * 1 ]; 替换为创建一个大小为 2M 的字节数组 byte [] buff = new byte [ 1024 * 1024 * 2 ]; 则内存不够使用,程序直接报错,强引用并不会被回收 接着来看一下软引用会有什么不一样,在下面的示例中连续创建了 10 个大小为 1M 的字节数组,并赋值给了软引用,然后循环遍历将这些对象打印出来。 public class TestOOM { private static List list = new ArrayList<>(); public static void main ( String[] args ) { testSoftReference(); } private static void testSoftReference () { for ( int i = 0 ; i < 10 ; i++) { byte [] buff = new byte [ 1024 * 1024 ]; SoftReference< byte []> sr = new SoftReference<>(buff); list. add (sr); } System.gc(); //主动通知垃圾回收 for ( int i= 0 ; i < list.size(); i++){ Object obj = ((SoftReference) list. get (i)). get (); System. out .println(obj); } } } 打印结果: 我们发现无论循环创建多少个软引用对象,打印结果总是只有最后一个对象被保留,其他的obj全都被置空回收了。 这里就说明了在内存不足的情况下,软引用将会被自动回收。 值得注意的一点 , 即使有 byte[] buff 引用指向对象, 且 buff 是一个strong reference, 但是 SoftReference sr 指向的对象仍然被回收了,这是因为Java的编译器发现了在之后的代码中, buff 已经没有被使用了, 所以自动进行了优化。 如果我们将上面示例稍微修改一下: private static void testSoftReference () { byte [] buff = null ; for ( int i = 0 ; i < 10 ; i++) { buff = new byte [ 1024 * 1024 ]; SoftReference< byte []> sr = new SoftReference<>(buff); list. add (sr); } System.gc(); //主动通知垃圾回收 for ( int i= 0 ; i < list.size(); i++){ Object obj = ((SoftReference) list. get (i)). get (); System. out .println(obj); } System. out .println( "buff: " + buff.toString()); } 则 buff 会因为强引用的存在,而无法被垃圾回收,从而抛出OOM的错误。 如果一个对象惟一剩下的引用是软引用,那么该对象是软可及的(softly reachable)。垃圾收集器并不像其收集弱可及的对象一样尽量地收集软可及的对象,相反,它只在真正 “需要” 内存时才收集软可及的对象。 三,弱引用 弱引用的引用强度比软引用要更弱一些, 无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收 。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。 我们以与软引用同样的方式来测试一下弱引用: private static void testWeakReference () { for ( int i = 0 ; i < 10 ; i++) { byte [] buff = new byte [ 1024 * 1024 ]; WeakReference< byte []> sr = new WeakReference<>(buff); list. add (sr); } System.gc(); //主动通知垃圾回收 for ( int i= 0 ; i < list.size(); i++){ Object obj = ((WeakReference) list. get (i)). get (); System. out .println(obj); } } 打印结果: 可以发现所有被弱引用关联的对象都被垃圾回收了。 四,虚引用 虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。 public class PhantomReference extends Reference { /** * Returns this reference object's referent. Because the referent of a * phantom reference is always inaccessible, this method always returns * null. * * @return null */ public T get() { return null ; } public PhantomReference ( T referent, ReferenceQueue q) { super (referent, q); } } 那么传入它的构造方法中的 ReferenceQueue 又是如何使用的呢? 五,引用队列(ReferenceQueue) 引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。 与软引用、弱引用不同,虚引用必须和引用队列一起使用。
来源:OSCHINA
发布时间:2020-05-31 22:28:00
6.15日:这几天把for,while遍历,重要的函数lambda,map函数,函数的定义,等一些基础的东西学了..还有1个大节学完后,就可以学习应用的demo了..嘎嘎 6.9日:这几天学习了元组,字典,集合等数组的定义和增删查改等;下一步开始学习python的逻辑和流程了.. 6.3日:学习了python列表,其实和php的数组用法是一样的,不过有很多语法糖不一样,比如增加的append,expend,删除的pop和del,修改直接通过定位下标修改,查找有count,index,in三种,然后还有个反转数组的reverse. 6.2日:学习了变量的N种查找,填充居中,替换,转换,分割,大小写,判断,编码等等方法,有点乱,幸好我有跟着写...感觉python的很多语法糖和php还是有很大区别,当然共同的也有,比如python的split和php的explode函数就一毛一样;今天已经上完第二大节了,还有8个大节. 6.1日:学习了变量N种赋值方式,,还有关于数字的计算,成功在自己本地运行了这些demo,感觉自己回到了刚学php那种感觉,我爱学习!!!还有什么比学习更快乐呢! a=333 b=c=444 #序列解包 d,e,f=555,666,777 #查看变量指向的内存地址id,如果一个变量0指向,那么就会被进行内存垃圾回收机制 print(id(b),id(c)) #删除变量(指针) #del(b) print(a,b,c,d,e,f); #取整 print(3//2) #取幂 print(2**3) #help函数 help(abs) #name=input('请输入你的姓名:') #print(name) #print(type(name)) 5.31日:安装python成功,更新pip失败,我都会背下命令行了,python -m pip install --upgrade pip,命令行意思:加载pip模组,安装,更新pip.可惜一直失败,嘤嘤嘤,哭了,升级不到pip2.0,后来在百度的帮助下,解决了这个问题,执行下面两句命令行,--index-url大致意思应该是换了python的pip更新源头 pip3 install --index-url https://pypi.douban.com/simple requests python -m pip install --upgrade pip --index-url https://pypi.douban.com/simple requests
来源:OSCHINA
发布时间:2020-05-31 22:26:00
配置ip地址等信息在/etc/sysconfig/network-scripts/ifcfg-ens33文件里做如下配置: 命令: vi /etc/sysconfig/network-scripts/ifcfg-ens33 需要修改的地方 BOOTPROTO="static" # 手动分配ip ONBOOT="yes" # 该网卡是否随网络服务启动 IPADDR="192.168.220.101" # 该网卡ip地址就是你要配置的固定IP,如果你要用xshell等工具连接,220这个网段最好和你自己的电脑网段一致,否则有可能用xshell连接失败 GATEWAY="192.168.220.2" # 网关 NETMASK="255.255.255.0" # 子网掩码 DNS1="8.8.8.8" # DNS,8.8.8.8为Google提供的免费DNS服务器的IP地址 如果做了以上配置,还不能正常连接主机,不能正常连接外网请注意看下面 虚拟机上面的这个网关很重要,一定要与配置的网关一致。 注意IP地址的起始区间,保持在区间范围之内 如果还是不能正常连接外网,看下面的配置文件里面是否有内容 # 查看是否有DNS配置 cat /etc/resolv.conf # 添加DNS配置 vim /etc/resolv.conf # 填入DNS服务器 nameserver 8.8.8.8 nameserver 8.8.4.4
来源:OSCHINA
发布时间:2020-05-31 22:04:00
首先通过下面链接地址下载 Anaconda 的个人版本。 https://www.anaconda.com/products/individual 从上面下载的地址中,选择你需要的版本,目前 Windows 应该基本上都是 64 位的了。 在你下载的文件中双击运行。 欢迎界面 在弹出的界面中显示了欢迎界面。 许可证 你需要同意许可证,才能让安装继续。 选择用户 在这里你选择默认的用户就可以了。 选择安装目录 在这里将会显示默认的安装目录,Anaconda 的安装比较消耗空间,你需要确定你的磁盘有足够的空间可以按照。 设置一个路径 在安装的时候,不建议设置 PATH,因为可能因为设置 PATH 导致安装的时候出现问题,也有可能会导致 Windows 载入不同的 Python 的版本。 安装进程 你需要等待一些时间,让安装完成。 安装将会按照顺序进行。 安装完成后下一步继续 当安装完成后,可以单击下一步继续。 提示你整合 PyCharm 这一步你不需要做任何事情,下一步继续就可以了。 安装完成 最后将会提示你,安装已经完成了。 单击完成就可以了。 校验安装 在安装好以后,你可以校验安装。 在 Windows 中,你可以选择打开 Anaconda 的命令行,然后输入 conda info 命令。 输入 conda info 命令查看安装的 anaconda。 你也可以输入 python 来查看绑定的版本。 如果你能够看到所有的版本,则说明你的安装已经完成,可以开始使用了。 https://www.ossez.com/t/windows-10-anaconda-3/123
来源:OSCHINA
发布时间:2020-05-31 21:32:00
1.Parallel scavenge+old:年轻代采用复制算法,老年代采用标记-整理算法,是多线程的并行收集器。 它注重的是可控制的吞吐量,适合于后台运算而不需要与用户有太多的交互的时候。 吞吐量:是CPU用于运行用户线程的时间和总CPU消耗时间的比值。 2.ParNew:其它和Parallel差不多,其区别主要在于ParNew更关注 停顿时间 ,且 可以与CMS收集器组队 工作。是许多工作在Server端的首要选择。 3.CMS:采用标记-清除算法,为了 获取最短停顿时间 为目标的多线程并发的收集器。 有 初次标记(STW)-并发标记-重新标记(STW)-并发清除 四个过程: 初次标记:需要stop the world。仅仅是标记一下GC root能直接可达的对象。 并发标记:进行GC root tracing过程,标记所有可达的对象,但由于此时收集器线程和用户线程并发运行,可能会不断更新引用域,所以会对引用更新的地方做跟踪记录让重新标记阶段去处理。 重新标记:需要stop the world 。修正因用户线程运行而导致的标记变动的那一部分的标记记录。 并发清除:重新启动用户线程,同时对标记的区域做清除。 ps:上面只是参考大部分面试答案,具体的还是不太清除,比如看了一篇分析:重新标记是标记存活对象,并发清除只是将对象标为不可达,真正清除是用啥?,整个过程标记的对象是老年代对象和新生代引用的老年代对象?并发标记之后还要预清理和可控制的预清理啥的.... CMS垃圾收集器详解 缺点: 对CPU资源敏感:会占用一部分线程用于垃圾收集而使应用程序变慢,总吞吐量下降。 无法处理浮动垃圾:浮动垃圾是在标记过程之后产生的垃圾。由于用户线程运行不可避免会产生浮动垃圾,而CMS只能等到下一次收集才能去处理。 产生内存碎片:由于采用的标记-清除算法,必然会产生大量内存碎片,可能会出现老年代内存空间足够但没有连续内存无法存放而导致的full GC。 4.G1收集器:是一款面向服务器的处理器,主要针对配备了多核CPU以及大容量内存的机器。 它有以下特点 : 并行与并发:由于使用了多个CPU可以有效减少停顿时间,部分其他收集器需要定下用户线程再执行的GC操作,G1可以通过并发的方式去运行用户线程,提高吞吐量了。 空间整合:从整体上看G1采用的是标记整理算法,而局部上使用的是复制算法。这意味着G1运行期间不会产生内存碎片,收集完成后仍有规整的可用内存,避免了分配大对象时没有连续内存空间来存放而提前触发下一次GC。 可预测的停顿时间:G1相比CMS的一大优势就是他可以建立可预测的时间模型,它能够使使用者明确在M毫秒的时间片段内,G1收集的时间不超过N毫秒。 分代整合:尽管G1不需要其它收集器来分代收集,仍保留了分代的概念,会采用不同的方式收集新创建的对象和存活了一段时间的对象。 而使G1有以上特点的原因是: 1.G1相较于其他收集器有不同的内存布局:它将Java堆划分成多个相同大小的独立区域(region),尽管还是有新生代和老年代的概念,但两者之间不再是物理隔离的了,而是一部分可以不连续的region集合。 2.避免全堆扫描:G1为每个region维护了一个与之对应的记忆集(Remember Set)来记录新生代和老年代之间的引用关系从而避免了全堆扫描。 3.有优先级的区域。G1会跟踪记录每个region中的垃圾价值大小,在后台建立一个优先列表,根据允许的收集时间,优先回收价值最大的region。 这种使用了region划分内存空间以及有优先级的区域回收方式保证了G1能够在有限时间内获得尽可能高的收集效率 。 收集过程: 初次标记--并发标记--最终标记--筛选回收。 5.Serial +Serial old:新生代采用复制算法、老年代采用标记整理算法,是需要stw的,单线程的收集器,适合于单CPU下的Client模式。 参考: 深入理解JVM(3)——7种垃圾收集器 (深入解析) 一篇文章彻底搞定所有GC面试问题 (面试回答思路) 面试官问我G1回收器怎么知道你是什么时候的垃圾?
来源:OSCHINA
发布时间:2020-05-31 20:30:00
首先通过下面链接地址下载 Anaconda 的个人版本。 https://www.anaconda.com/products/individual 从上面下载的地址中,选择你需要的版本,目前 Windows 应该基本上都是 64 位的了。 在你下载的文件中双击运行。 欢迎界面 在弹出的界面中显示了欢迎界面。 许可证 你需要同意许可证,才能让安装继续。 选择用户 在这里你选择默认的用户就可以了。 选择安装目录 在这里将会显示默认的安装目录,Anaconda 的安装比较消耗空间,你需要确定你的磁盘有足够的空间可以按照。 设置一个路径 在安装的时候,不建议设置 PATH,因为可能因为设置 PATH 导致安装的时候出现问题,也有可能会导致 Windows 载入不同的 Python 的版本。 安装进程 你需要等待一些时间,让安装完成。 安装将会按照顺序进行。 安装完成后下一步继续 当安装完成后,可以单击下一步继续。 提示你整合 PyCharm 这一步你不需要做任何事情,下一步继续就可以了。 安装完成 最后将会提示你,安装已经完成了。 单击完成就可以了。 校验安装 在安装好以后,你可以校验安装。 在 Windows 中,你可以选择打开 Anaconda 的命令行,然后输入 conda info 命令。 输入 conda info 命令查看安装的 anaconda。 你也可以输入 python 来查看绑定的版本。 如果你能够看到所有的版本,则说明你的安装已经完成,可以开始使用了。
来源:OSCHINA
发布时间:2020-06-01 08:50:00
0.前言 前篇介绍了一些数据库的基本概念和以及一些常见的数据库,让我们对数据库有了一个初步的认识。这一篇我们将继续为C#数据操作的基础填上一个空白-SQL语句。 SQL(Structured Query Language,结构化查询语言)是一种特定的编程语言,用于管理数据库系统,操作数据甚至编写一些程序。 当然,一方面因为时间问题,一方面因为各大数据库的区别(当然了,还有就是个人对SQL研究并不是那么深)所以这一篇就从SQL的基本操作入手,带领大家一起看看SQL的世界。 1. SQL的分类 在SQL的世界里,被分割为两个部分:DML(Data Manipulation Language 数据操纵语言)、DDL(Database Definition Language 数据定义语言)。当然,也有很多其他的分法,这里参照了机械工业出版社出版的《计算机科学丛书- 数据库系统概念》。 1.1 DML 数据操纵语言,用户可以凭此来访问或者操纵那些被结构化存储起来的数据。DML提供了以下功能: 对存储在数据库的数据进行检索(select) 在数据库中添加新的数据(insert) 修改数据库中的数据(update) 删除数据库中的某些数据(delete) 简单的概括起来就是增删改查,对于开发而言这是一项枯燥乏味的工作,当然也是每个程序必不可少的工作。如果你见到这个词:crud,不要诧异,这是开发对增删改查的一种缩写(create,read,update,delete)。 在技术的演变过程中,为了更快更好的增删改查,有一些大牛开发出了一系列的ORM框架,比如C#里最出名的EntityFramework、与Hibernate同源的NHibernate等等。 1.2 DDL 数据定义语言,用户可以用来创建数据库、修改数据库属性、删除数据库,新建表、视图,修改表、视图,删除表、视图等。与DML不同的是,DDL操作的对象从数据转变成了承载数据的实体或者与操作数据的实体。 还有与DML不同的一点是,DDL更多的会使用 create、alter、drop等关键字(分别用来 创建、修改、销毁)。 1.3 方言 如今的城市人们来自五湖四海,有的人用普通话,有的人还是一口流利的家乡话。与之相同的就是在数据库这个江湖里,各大门派都在标准SQL里添加了自己的东西,让SQL成了一个操持着五湖四海的方言的大家族。比如说微软的Transcat-SQL和PL/SQL。 2. 一些简单操作 这里先简单介绍一下通用SQL下的操作: 2.1 创建数据库 create database test; 这是一个简单的创建数据库的SQL语句,这是标准SQL的一部分。效果就是创建一个名字为test的数据库,字符集等属性是系统的默认值。 当然,在SQL Server里可以通过以下方式指定字符集: create database 数据库名 DEFAULT CHARACTER SET gbk COLLATE gbk_chinese_ci; -- 使用gbk CREATE DATABASE 数据库名 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; -- 使用utf8 这是在开发过程中最常用的创建数据库方式。 2.2 创建表 数据表是数据库里最重要的一个实体,我们大概演示一下如何通过sql语句创建一个表: create table demo ( [key] int identity primary key , [name] varchar(20) ) go SQL 创建表的格式如下: create table <表名> ( [属性名] <类型> [params...配置] ) 如果有第二个属性,则在第一个之后添加一个逗号,然后继续按照格式声明。 其中 属性名和类型是必须的,配置则可有可无。 常见配置项: identity 表示该列是个自增列,一般是起始1,增长步长为1 primary key 表示该列是主键列,只能有一个主键 not null 表示该字段非空,如果是空值进来则会报错 unique 表示该字段的值不能出现重复 而数据类型则因为数据库不同会有一些细微的差别,所以这里就不错过多介绍了。 2.3 查询 一个简单的查询: select * from demo; 表示查询该表的所有数组。 然后更进一步,可以限制查询条件: select * from demo where <条件>; 注意一下这里的条件里的等值判断用的是一个等号,而不像开发语言里用的是双等号。 这时候发现我们用不了那么多的字段,然后筛选出要显示的字段: select <字段01>,<字段02>,<字段03> from [表名] where <条件> 这里简单介绍一下查询,当然还有很多没有一一介绍,在后续的章节会把这部分补齐的。 2.4 添加数据 在查询之前,我们得先保证数据表里有数据,所以我们看看如何插入数据吧。 插入单条记录: insert into [表名](<字段1>,<字段2>,<字段3>) values('值1','值2','值3') 在表名后面跟括号,括号内写入要插入值的字段,然后values关键字后面用括号包裹起来的一组值便是要插入的值。插入值要与字段名一一对应。 如果要插入多条记录呢? insert into [表名](<字段1>,<字段2>,<字段3>) values('值1','值2','值3'),('值1','值2','值3') 如果需要插入多条的话,将数据用括号包裹起来,然后依次跟在values后面。 2.5 修改数据 当我们发现插入的数据有问题的时候或者因为业务的进行,数据库表里的数据需要更新,这时候我们可以参照以下方式写自己的sql: update [表名] set <字段1> = <值1> 如果需要更新多个字段,可以在更新字段后面添加一个逗号,然后跟在后面,简单实例: update AdditionalService set storeid = 1 , name = '23' 目前所以的更新都是全表更新,当然我们一样可以使用 where来限制。 2.6 删除数据 删除数据的关键字是delete,所以删除的写法是: delete [表名] where <条件> 如果不设置where 条件,则删除的是全表数据。 2.7 删除表 删除表的操作: drop table [表名] 这个操作会把表结构和表里的数据都删除。 3.总结 这一篇大概介绍了SQL的基本用法,开发过程中的SQL基本够用了。后续会随着文章内容逐步填补未介绍的部分。 更多内容烦请关注 我的博客《高先生小屋》
来源:OSCHINA
发布时间:2020-06-01 08:20:00
昨天晚上在知乎上看到一个网友问题,我做了一个详细的回答,收到了许多测试人的喜欢与点赞,我把我的回答贴出来分享一下。 既然问题问的这么官方,那我来做一个科普?后面再来解答你的问题。 软件测试(Software Testing),描述一种用来促进鉴定软件的正确性、完整性、安全性和质量的过程。换句话说,软件测试是一种实际输出与预期输出之间的审核或者比较过程。软件测试的经典定义是:在规定的条件下对程序进行操作,以发现程序错误,衡量软件质量,并对其是否能满足设计要求进行评估的过程。 学习软件测试从哪里入手? 我认为分为初级、中级和高级三个阶段,不足之处欢迎朋友们指出,我会及时改正。 初级阶段 初级阶段需要掌握四个方面的内容: 一、软件测试的基础知识,编写测试用例的方法及测试流程 二、掌握禅道、SVN等必要工具,及缺陷定义和测试计划编写方法 三、web测试与app测试的方式方法与协议 四、接口测试postman工具的操作使用,前端基础知识H5及CSS 中级阶段 中级阶段需要掌握六个方面的内容,从中级开始就是涉及到一些工具的使用 一、QTP自动化工具的环境搭建 二、loadrunner性能工具的环境搭建 三、jmeter性能工具的环境搭建及接口压力测试 四、jmeter脚本增强,app/web性能测试 五、fiddler抓包工具的操作使用、Jenkins自动化部署工具 六、数据库MySQL、SQL语句 高级阶段 鉴于问题的角度,我的观点是实践出真知,当你到了中高级阶段,也许你比我的体会更加深刻,就留一个白。正好昨天在编辑一个软件测试全栈思维导图,有需要的可以私信我。贡献我的微薄之力,做好了也会发出来,记得关注我! 随着互联网IT产业的蓬勃发展,软件测试的行业也日趋火热,软件测试基础入门知识都是固定的那一些,要想成为一个优秀的软件测试工程师要学的并不少。回到你的问题,软件测试是不是很简单?什么人都可以学?我想你心里已经有了答案。 我说你只要学一个月就可以学成,入门到放弃啊!滑稽狗头 零基础容易从自学到放弃,太多个这样的例子了,一个人的精力有限,还会涉及到每个人自制力,白天上班下班了很多都是只想呆着不动葛优瘫一会,你需要鼓励需要有人给你加油打气!遇到一些自己解决不了的技术难题,容易诱发焦虑迷茫等不利于学习软件测试的情绪,我深有体会,因此建了一个软件测试交流群,有什么问题可以直接在群里问,(718897738)欢迎来学习交流,免费学习资料可供下载) 那么怎样成为一个好的测试工程师? 作为一名软件工程师,需要的能力并不多,但是要成为一名优秀的软件测试工程师,需要的能力就比较多了,自己整理出来8个方面,每个方面都会分成很多细小的方便并进行举例说明。 知乎是一个开放性的平台,有问题上知乎!那我就再为软件测试人或者在路上的测试人说一下优秀的软件测试工程师必备的8个能力。正所谓,做一行爱一行,既然选择了测试,就要把他做好! 《优秀的软件测试工程师必备的“8个能力”》 一、业务分析能力 1.分析整体业务流程 不了解整个公司的业务,根本就没办法进行测试 2.分析被测业务数据 了解整个业务里面所需的数据有哪些?哪些是需要用户提供的?哪些是自己提供的?有哪些可以是假数据?有哪些必须是真数据?添加数据的时候可以用哪个库? 明白了整个软件的数据库架构,才能知道哪一个数据是从哪一个表里头带出来的,它的逻辑是什么,有没有连带关系。 3.分析被测系统架构 用什么语言开发的?用的是什么服务器?测试它的话需要用什么样的环境进行测试?整体的测试环境是什么样的? 如果缺少了,需要进行环境搭建,架构搭建。一般去一家新公司之后,架构是搭建好的,了解它即可,熟悉之前的这些老员工们使用什么样的架构发表去做的。 4.分析被测业务模块 整个软件有哪些模块,比如说首页面、注册页面、登录页面、会员页面、商品详情页面、优惠券页面等等 明白有多少个模块需要测试,每个模块之间的连带关系,进而怎样进行人员分工 5.分析测试所需资源 我需要几台计算机,需要几部手机,手机需要什么样的系统,什么样的型号。 比如测一个网站的性能的时候,电脑的配置达不到测试并发5000人的标准,要么升级电脑的硬件配置,要么多机联合,多机联合时需要几台电脑,都需要提前筹划。 6.分析测试完成目标 我的性能目标是什么样的?我的功能目标是什么样的?我要上线达到的上线标准是什么样的?性能目标,比如我要达到并发5000人的时候,CPU占用率不能高于70%,内存占用率不能高于60%,响应时间不能超过5秒功能目标,比如整体的业务流程都跑通,所有的分支流程都没有问题,所有的接口都能够互相调用,整体的UI界面没有问题,兼容性没有问题等 把这些问题都弄清楚,测试的思路会非常的清晰 二、缺陷洞察能力 1.一般缺陷的发现能力 至少你要满足一般缺陷的发现能力,这个是最基本的,如果要连最简单的一般的缺陷都发现不了的话,别说优秀测试工程师了,你说你是测试我都不信 2.隐性问题的发现能力 在软件的测试过程当中有一些缺陷藏的比较深,有的是性能方面的问题,有的是功能方面的问题,它需要有一些设定特定的条件的情况下才会出现这样的问题。 比如说买双鞋必须选择的是什么品牌,必须选择是红颜色,必须选择44号,而且必须选择用特定的支付方式才会出现这样的bug的时候,那么这种就属于特别隐性的bug,对于这样的问题的发现能力一定要比别人更强,要找到一些别人可能发现不了的bug。 3.发现连带问题的能力 当发现了一个缺陷之后,能够想到通过这个缺陷可能会引发其他哪个地方出现问题,这就叫做连带的问题。而不是说发现这一个bug之后提了这一个就算完了,一定要有一个察觉,可能其他地方也存在这样的问题。 ^_^ 最后: 凌晨码字不容易哇~点个赞同是对我最大的支持!其实我也就晚上闲下来才有时间来知乎回答一些我认为有价值的问题。帮助到更多想入行或者已经在路上的测试人。软件测试学习群,欢迎来学习交流,免费学习资料可供下载。 晚安 测试人 如果对python自动化测试、web自动化、接口自动化、移动端自动化、面试经验交流等等感兴趣的测试人,可以关注我。加入我们免费获取更多软件测试进阶资料!
来源:OSCHINA
发布时间:2020-06-03 10:43:00
作者:ksfzhaohui https://my.oschina.net/OutOfMemory/blog/3117737 最近有个需求解析一个订单文件,并且说明文件可达到千万条数据,每条数据大概在20个字段左右,每个字段使用逗号分隔,需要尽量在半小时内入库。 思路 1.估算文件大小 因为告诉文件有千万条,同时每条记录大概在20个字段左右,所以可以大致估算一下整个订单文件的大小,方法也很简单使用 FileWriter 往文件中插入一千万条数据,查看文件大小,经测试大概在1.5G左右; 2.如何批量插入 由上可知文件比较大,一次性读取内存肯定不行,方法是每次从当前订单文件中截取一部分数据,然后进行批量插入,如何批次插入可以使用**insert(...)values(...),(...)**的方式,经测试这种方式效率还是挺高的; 怎么快速插入 100 条数据,用时最短 ,这篇看下。 3.数据的完整性 截取数据的时候需要注意,需要保证数据的完整性,每条记录最后都是一个换行符,需要根据这个标识保证每次截取都是整条数,不要出现半条数据这种情况; 4.数据库是否支持批次数据 因为需要进行批次数据的插入,数据库是否支持大量数据写入,比如这边使用的mysql,可以通过设置 max_allowed_packet 来保证批次提交的数据量; 5.中途出错的情况 因为是大文件解析,如果中途出现错误,比如数据刚好插入到900w的时候,数据库连接失败,这种情况不可能重新来插一遍,所有需要记录每次插入数据的位置,并且需要保证和批次插入的数据在同一个事务中,这样恢复之后可以从记录的位置开始继续插入。 实现 1.准备数据表 这里需要准备两张表分别是:订单状态位置信息表,订单表; CREATE TABLE `file_analysis` (   `id` bigint(20) NOT NULL AUTO_INCREMENT,   `file_type` varchar(255) NOT NULL COMMENT '文件类型 01:类型1,02:类型2',   `file_name` varchar(255) NOT NULL COMMENT '文件名称',   `file_path` varchar(255) NOT NULL COMMENT '文件路径',   `status` varchar(255) NOT NULL COMMENT '文件状态 0初始化;1成功;2失败:3处理中',   `position` bigint(20) NOT NULL COMMENT '上一次处理完成的位置',   `crt_time` datetime NOT NULL COMMENT '创建时间',   `upd_time` datetime NOT NULL COMMENT '更新时间',   PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 CREATE TABLE `file_order` (   `id` bigint(20) NOT NULL AUTO_INCREMENT,   `file_id` bigint(20) DEFAULT NULL,   `field1` varchar(255) DEFAULT NULL,   `field2` varchar(255) DEFAULT NULL,   `field3` varchar(255) DEFAULT NULL,   `field4` varchar(255) DEFAULT NULL,   `field5` varchar(255) DEFAULT NULL,   `field6` varchar(255) DEFAULT NULL,   `field7` varchar(255) DEFAULT NULL,   `field8` varchar(255) DEFAULT NULL,   `field9` varchar(255) DEFAULT NULL,   `field10` varchar(255) DEFAULT NULL,   `field11` varchar(255) DEFAULT NULL,   `field12` varchar(255) DEFAULT NULL,   `field13` varchar(255) DEFAULT NULL,   `field14` varchar(255) DEFAULT NULL,   `field15` varchar(255) DEFAULT NULL,   `field16` varchar(255) DEFAULT NULL,   `field17` varchar(255) DEFAULT NULL,   `field18` varchar(255) DEFAULT NULL,   `crt_time` datetime NOT NULL COMMENT '创建时间',   `upd_time` datetime NOT NULL COMMENT '更新时间',   PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10000024 DEFAULT CHARSET=utf8 2.配置数据库包大小 mysql> show VARIABLES like '%max_allowed_packet%'; +--------------------------+------------+ | Variable_name | Value | +--------------------------+------------+ | max_allowed_packet | 1048576 | | slave_max_allowed_packet | 1073741824 | +--------------------------+------------+ 2 rows in set mysql> set global max_allowed_packet = 1024*1024*10; Query OK, 0 rows affected 通过设置max_allowed_packet,保证数据库能够接收批次插入的数据包大小;不然会出现如下错误: Caused by: com.mysql.jdbc.PacketTooBigException: Packet for query is too large (4980577 > 1048576). You can change this value on the server by setting the max_allowed_packet' variable.     at com.mysql.jdbc.MysqlIO.send(MysqlIO.java:3915)     at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2598)     at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2778)     at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2834) 3.准备测试数据 public static void main(String[] args) throws IOException {   FileWriter out = new FileWriter(new File("D://xxxxxxx//orders.txt"));   for (int i = 0; i < 10000000; i++) {     out.write(         "vaule1,vaule2,vaule3,vaule4,vaule5,vaule6,vaule7,vaule8,vaule9,vaule10,vaule11,vaule12,vaule13,vaule14,vaule15,vaule16,vaule17,vaule18");     out.write(System.getProperty("line.separator"));   }   out.close(); } 使用FileWriter遍历往一个文件里插入1000w条数据即可,这个速度还是很快的,不要忘了在每条数据的后面添加 换行符(\n\r) ; 4.截取数据的完整性 除了需要设置每次读取文件的大小,同时还需要设置一个参数,用来每次获取一小部分数据,从这小部分数据中获取 换行符(\n\r) ,如果获取不到一直累加直接获取为止,这个值设置大小大致同每条数据的大小差不多合适,部分实现如下: ByteBuffer byteBuffer = ByteBuffer.allocate(buffSize); // 申请一个缓存区 long endPosition = batchFileSize + startPosition - buffSize;// 子文件结束位置 long startTime, endTime; for (int i = 0; i < count; i++) {     startTime = System.currentTimeMillis();     if (i + 1 != count) {         int read = inputChannel.read(byteBuffer, endPosition);// 读取数据         readW: while (read != -1) {             byteBuffer.flip();// 切换读模式             byte[] array = byteBuffer.array();             for (int j = 0; j < array.length; j++) {                 byte b = array[j];                 if (b == 10 || b == 13) { // 判断\n\r                     endPosition += j;                     break readW;                 }             }             endPosition += buffSize;             byteBuffer.clear(); // 重置缓存块指针             read = inputChannel.read(byteBuffer, endPosition);         }     } else {         endPosition = fileSize; // 最后一个文件直接指向文件末尾     }     ...省略,更多可以查看Github完整代码... } 如上代码所示开辟了一个缓冲区,根据每行数据大小来定大概在200字节左右,然后通过遍历查找 换行符(\n\r) ,找到以后将当前的位置加到之前的结束位置上,保证了数据的完整性; 5.批次插入数据 通过**insert(...)values(...),(...)**的方式批次插入数据,部分代码如下: // 保存订单和解析位置保证在一个事务中 SqlSession session = sqlSessionFactory.openSession(); try {   long startTime = System.currentTimeMillis();   FielAnalysisMapper fielAnalysisMapper = session.getMapper(FielAnalysisMapper.class);   FileOrderMapper fileOrderMapper = session.getMapper(FileOrderMapper.class);   fileOrderMapper.batchInsert(orderList);   // 更新上次解析到的位置,同时指定更新时间   fileAnalysis.setPosition(endPosition + 1);   fileAnalysis.setStatus("3");   fileAnalysis.setUpdTime(new Date());   fielAnalysisMapper.updateFileAnalysis(fileAnalysis);   session.commit();   long endTime = System.currentTimeMillis();   System.out.println("===插入数据花费:" + (endTime - startTime) + "ms==="); } catch (Exception e) {   session.rollback(); } finally {   session.close(); } ...省略,更多可以查看Github完整代码... 如上代码在一个事务中同时保存批次订单数据和文件解析位置信息,batchInsert通过使用mybatis的**< foreach >**标签来遍历订单列表,生成values数据; 总结 以上展示了部分代码,完整的代码可以查看 Github 地址中的batchInsert模块,本地设置每次截取的文件大小为2M。 经测试1000w条数据(大小1.5G左右)插入mysql数据库中,大概花费时间在20分钟左右,当然可以通过设置截取的文件大小,花费的时间也会相应的改变。 推荐去我的博客阅读更多: 1. Java JVM、集合、多线程、新特性系列教程 2. Spring MVC、Spring Boot、Spring Cloud 系列教程 3. Maven、Git、Eclipse、Intellij IDEA 系列工具教程 4. Java、后端、架构、阿里巴巴等大厂最新面试题 觉得不错,别忘了点赞+转发哦!
来源:OSCHINA
发布时间:2020-06-03 09:49:00
[TOC] mybatis运行分为两部分,第一部分读取配置文件缓存到Configuration对象中。用以创建SqlSessionFactory,第二部分是SqlSession的执行过程。 Mybatis基本认识 动态代理 之前我们知道Mapper仅仅是一个接口,而不是一个逻辑实现类。但是在Java中接口是无法执行逻辑的。这里Mybatis就是通过动态代理实现的。关于动态代理我们常用的有Jdk动态代理和cglib动态代理。两种却别这里不做赘述。关于CGLIB代理在框架中使用的比较多。 关于动态代理就是所有的请求有一个入口,由这个入口进行分发。在开发领域的一个用途就是【负载均衡】 关于Mybatis的动态代理是使用了两种的结合。 下面看看JDK和cglib两种实现 JDK实现 首先我们需要提供一个接口 , 这个接口是对我们程序员的一个抽象。 拥有编码和改BUG的本领 public interface Developer { /** * 编码 */ void code(); /** * 解决问题 */ void debug(); } 关于这两种本领每个人处理方式不同。这里我们需要一个具体的实例对象 public class JavaDeveloper implements Developer { @Override public void code() { System.out.println("java code"); } @Override public void debug() { System.out.println("java debug"); } } 我们传统的调用方式是通过java提供的new 机制创造一个JavaDeveloper对象出来。而通过动态代理是通过 java.lang.reflect.Proxy 对象创建对象调用实际方法的。 通过 newProxyInstance 方法获取接口对象的。而这个方法需要三个参数 ClassLoader loader : 通过实际接口实例对象获取ClassLoader Class[] interfaces : 我们抽象的接口 InvocationHandler h : 对我们接口对象方法的调用。在调用节点我们可以进行我们的业务拦截 JavaDeveloper jDeveloper = new JavaDeveloper(); Developer developer = (Developer) Proxy.newProxyInstance(jDeveloper.getClass().getClassLoader(), jDeveloper.getClass().getInterfaces(), (proxy, method, params) -> { if (method.getName().equals("code")) { System.out.println("我是一个特殊的人,code之前先分析问题"); return method.invoke(jDeveloper, params); } if (method.getName().equals("debug")) { System.out.println("我没有bug"); } return null; }); developer.code(); developer.debug(); CGLIB动态代理 cglib动态代理优点在于他不需要我们提前准备接口。他代理的实际的对象。这对于我们开发来说就很方便了。 public class HelloService { public HelloService() { System.out.println("HelloService构造"); } final public String sayHello(String name) { System.out.println("HelloService:sayOthers>>"+name); return null; } public void sayHello() { System.out.println("HelloService:sayHello"); } } 下面我们只需要实现cglib提供的MethodInterceptor接口,在初始化设置cglib的时候加载这个实例化对象就可以了 public class MyMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("======插入前置通知======"); Object object = methodProxy.invokeSuper(o, objects); System.out.println("======插入后者通知======"); return object; } } 下面我们就来初始化设置cglib public static void main(String[] args) { //代理类class文件存入本地磁盘方便我们反编译查看源代码 System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/root/code"); //通过CGLIB动态代理获取代理对象过程 Enhancer enhancer = new Enhancer(); //设置enhancer对象的父类 enhancer.setSuperclass(HelloService.class); // 设置enhancer的回调对象 enhancer.setCallback(new MyMethodInterceptor()); //创建代理对象 HelloService helloService = (HelloService) enhancer.create(); //通过代理对象调用目标方法 helloService.sayHello(); } 仔细看看cglib和spring的aop特别像。针对切点进行切面拦截控制。 总结 通过对比两种动态代理我们很容易发现,mybatis就是通过JDK代理实现Mapper调用的。我们Mapper接口实现通过代理到xml中对应的sql执行逻辑 反射 相信有一定经验的Java工程师都对反射或多或少有一定了解。其实从思想上看不惯哪种语言都是有反射的机制的。 通过反射我们就摆脱了对象的限制我们调用方法不再需要通过对象调用了。可以通过Class对象获取方法对象。从而通过invoke方法进行方法的调用了。 Configuration对象作用 Configuration对象存储了所有Mybatis的配置。主要初始化一下参数 properties settings typeAliases typeHandler ObjectFactory plugins environment DatabaseIdProvider Mapper映射器 映射器结构 BoundSql提供三个主要的属性 parameterMappings 、parameterObject、sql parameterObject参数本身。我们可以传递java基本类型、POJO、Map或者@Param标注的参数。 当我们传递的是java基本类型mybatis会转换成对应的包装对象 int -> Integer 如果我们传递POJO、Map。就是对象本身 我们传递多个参数且没有@Param指定变量名则parameterObject 类似 {"1":p1,"2":p2,"param1":p1,"param2":p2} 我们传递多个参数且@Param指定变量名 则parameterObject类似 {"key1":p1,"key2":p2,"param1":p1,"param2":p2} parameterMapping 是记录属性、名称、表达式、javaType,jdbcType、typeHandler这些信息 sql 属性就是我们映射器中的一条sql. 正常我们在常见中对sql进行校验。正常不需要修改sql。 sqlsession执行流程(源码跟踪) 首先我们看看我们平时开发的Mapper接口是如何动态代理的。这就需要提到 MapperProxyFactory 这个类了。该类中的 newInstance 方法 protected T newInstance(MapperProxy mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } 通过上满代码及上述对jdk动态代理的表述。我们可以知道mapperProxy是我们代理的重点。 MapperProxy是InvocationHandler的实现类。他重写的invoke方法就是代理对象执行的方法入口。 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } private boolean isDefaultMethod(Method method) { return (method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC && method.getDeclaringClass().isInterface(); } 通过源码发现。invoke内部首先判断对象是否是类 。 通过打断点发现最终会走到cacheMapperMethod这个方法去创建MapperMethod对象。 继续查看MapperMethod中execute方法我们可以了解到内部实现其实是一个命令行模式开发。通过判断命令从而执行不同的语句。判断到具体执行语句然后将参数传递给sqlsession进行sql调用并获取结果。到了sqlsession就和正常jdbc开发sql进行关联了。sqlsession中 Executor 、 StatementHandler 、 ParameterHandler 、 Resulthandler 四大天王 Executor 顾名思义他就是一个执行器。将java提供的sql提交到数据库。Mybatis提供了三种执行器。 Configuration.class 中 newExecutor 源码 根据uml我们不难看出mybatis中提供了三类执行器分别SimpleExecutor、ReuseExecutor、BatchExecutor public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); } private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { // 得到configuration 中的environment final Environment environment = configuration.getEnvironment(); // 得到configuration 中的事务工厂 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 获取执行器 final Executor executor = configuration.newExecutor(tx, execType); // 返回默认的SqlSession return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } 通过上述源码我们知道在sqlsession获取一个数据库session对象时我们或根据我们的settings配置加载一个Executor对象。在settings中配置也很简单 我们也可以通过java代码设置 factory.openSession(ExecutorType.BATCH); StatementHandler 顾名思义,StatementHandler就是专门处理数据库回话的。这个对象的创建还是在Configuration中管理的。 public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } 很明显Mybatis中StatementHandler使用的是RoutingStatementHandler这个class 关于StatementHandler和RoutingStatementHandler之间的关系我们通过源码可以看出这里和Executor一样都是适配器模式。采用这种模式的好处是方便我们对这些对象进行代理。这里读者可以猜测一下是使用了哪种动态代理。给点提示 这里使用了接口哦 在查看BaseStatementHandler结构我们会发现和Executor一模一样。同样的Mybatis在构造RoutingStatementHandler的时候会根据setting中配置来加载不同的具体子类。这些子类都是继承了BaseStatementHandler. 前一节我们跟踪了Executor。 我们知道Mybatis默认的是SimpleExecutor。 StatementHandler我们跟踪了Mybaits默认的是PrePareStatementHandler。在SimpleExecutor执行查询的源码如下 我们发现在executor查询钱会先让statementHandler构建一个Statement对象。最终就是StatementHandler中prepare方法。这个方法在抽象类BaseStatmentHandler中已经封装好了。 这个方法的逻辑是初始化statement和设置连接超时等一些辅助作用 然后就是设置一些参数等设置。最后就走到了执行器executor的doquery PrepareStatement在我们jdbc开发时是常见的一个类 。 这个方法执行execute前我们需要设置sql语句,设置参数进行编译。这一系列步骤就是刚才我们说的流程也是PrepareStatementHandler.prepareStatement帮我们做的事情。那么剩下的我们也很容易想到就是我们对数据结果的封装。正如代码所示下马就是resultSetHandler帮我们做事情了。 结果处理器(ResultSetHandler) @Override public List handleResultSets(Statement stmt) throws SQLException { ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); final List multipleResults = new ArrayList<>(); int resultSetCount = 0; ResultSetWrapper rsw = getFirstResultSet(stmt); List resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount); while (rsw != null && resultMapCount > resultSetCount) { ResultMap resultMap = resultMaps.get(resultSetCount); handleResultSet(rsw, resultMap, multipleResults, null); rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } String[] resultSets = mappedStatement.getResultSets(); if (resultSets != null) { while (rsw != null && resultSetCount < resultSets.length) { ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); if (parentMapping != null) { String nestedResultMapId = parentMapping.getNestedResultMapId(); ResultMap resultMap = configuration.getResultMap(nestedResultMapId); handleResultSet(rsw, resultMap, null, parentMapping); } rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } } return collapseSingleResultList(multipleResults); } 这个方法我们可以导出来是结果xml中标签配置对结果的一个封装。 总结 SqlSession在一个查询开启的时候会先通过CacheExecutor查询缓存。击穿缓存后会通过BaseExector子类的SimpleExecutor创建StatementHandler。PrepareStatementHandler会基于PrepareStament执行数据库操作。并针对返回结果通过ResultSetHandler返回结果数据 主题
来源:OSCHINA
发布时间:2020-06-03 09:30:06
六月福利 2020年6月公众号码农小胖哥原创文章转发第一名将送全新《 Spring Boot实战 》实体书一本,该书是学习热门框架 Spring Boot 的经典之作。 你不再需要依靠运气,而是勤奋 。截止统计日期2020年6月30日,统计数据以官方公众号工具为准,运营人员不参加活动,本次活动图书由掘金社区赞助。 1. 前言 前几天讲了 设计模式中的命令模式 ,今天来看看另一个模式。移动支付目前在国内已经是非常普及了,连楼下早餐摊的七十多岁大妈也使用支付宝和微信支付卖鸡蛋饼。如果让你做一个App你肯定要考虑多个渠道支付,以保证获客渠道。如果让你来接入多种支付渠道你会怎么设计? 2. 通常写法 一般下面这种写法很容易被创造出来: public boolean pay(BigDecimal amount){ boolean ret =false; if (alipay){ //todo 支付宝的逻辑 }else if (wechatpay){ //todo 微信支付的逻辑 }else if (ooxx){ // …… } return ret; } 如果集成了四五种支付,这个代码就没法看了少说几千行,而且改动某个支付的逻辑很容易改了其它支付的逻辑。因此需要合理的设计来避免这种风险。 3. 策略模式 大部分的支付可以简化为这个流程: 中间的 发起支付前逻辑 和 支付后处理逻辑 是客户端的自定义业务逻辑,向支付服务器发送的请求只会携带对应支付服务器特定要求的参数调用不同的支付 SDK 。所以我们分别建立对应支付方式的策略来隔离区分它们,降低它们的耦合度。当准备支付时我们只需要选择对应的策略就可以了。 这就用到了设计模式中的策略模式: 结合上面的类图,我们就来结合着需求来聊聊策略模式中的主要几个角色。 Strategy 接口。这个接口用来声明每一种方式的独立执行策略,用来封装具体策略的特有算法逻辑。 ConcreteStrategy 是 Strategy 的实现类。实现了不同策略的算法逻辑。比如每种支付的调用细节等等。 Context 上下文。它通过策略接口来引用了具体的策略并使用具体的策略来执行逻辑,同时所有策略的共性也可以在该类中进行统一处理。在聚合支付需求中我们传入一个策略,先执行支付前的逻辑,然后使用策略,策略执行完毕后,再执行后置的共性逻辑。 Client 客户端。创建策略对象并传递给上下文 Context ,然后由上下文运行具体的策略。 结合业务逻辑是这样的: 请求到达客户端,客户端根据请求中包含的支付渠道来构建对应的策略对象并把它交给上下文对象去执行支付流程。 然后我们就可以分别为支付宝、微信支付、银联支付构造三个策略对象 AliPayStrategy 、 WechatPayStrategy 、 UnionPayStrategy ,我们来模拟一下执行策略: public class Client { public static void main(String[] args) { // 获取请求中的支付渠道标识 String code = "p01"; PayStrategy payStrategy = null; PayRequest request = null; if (PayType.ALI.getCode().equals(code)) { //组装为支付宝支付策略 payStrategy = new AliPayStrategy(); // 构造支付宝请求参数 request = new AliPayRequest(); } if (PayType.WECHAT.getCode().equals(code)) { //组装为微信支付策略 payStrategy = new AliPayStrategy(); // 构造微信支付请求参数 request = new WechatPayRequest(); } if (PayType.UNION.getCode().equals(code)) { //组装为银联支付策略 payStrategy = new UnionPayStrategy(); // 构造银联支付请求参数 request = new UnionPayRequest(); } if (Objects.nonNull(payStrategy)) { PayContext payContext = new PayContext(); payContext.setPayStrategy(payStrategy); payContext.pay(request); } } } 我们拿到请求中的支付标识,然后初始化对应的支付策略,封装指定的请求参数,交给上下文对象 PayContext 来执行请求。如果你再增加什么 ApplePay 之类的去实现 ApplePayStrategy 就行了。 4. 优缺点 策略模式并不都带来正面的作用。 4.1 优点 我们将算法的实现和算法的使用进行了隔离,算法实现只关心算法逻辑,使用算法只关心什么条件下使用什么算法。 开闭原则,无需修改上下文对象,想扩展只需要引入新的策略。 运行时根据条件动态的去切换算法。 适应个性的同时也可以兼容共性。 4.2 缺点 客户端必须明确知道激活策略的条件。 引入太多的策略类。 可被一些函数式接口所代替。伪代码 Pay.request(data).strategy(data->{ }) 。 5. 总结 策略模式也是很常见而且有着广泛使用场景的设计模式。今天我们从聚合支付来学习了策略模式,对它的优缺点也进行了一个分析。随着 函数式编程 的普及,策略模式开始被逐渐的代替,但是它依然值得我们去学习。感谢你的阅读,多多关注: 码农小胖哥 ,更多编程干货奉上。 关注公众号:Felordcn获取更多资讯 个人博客:https://felord.cn
来源:OSCHINA
发布时间:2020-06-03 08:56:00
JavaFX初探(菜单) 本节我们介绍如何创建菜单、菜单栏、增加菜单项、为菜单分类,创建子菜单、设置菜单上下文。你可以使用下面的类来创建菜单。 MenuBar MenuItem Menu CheckMenuItem RadioMenuItem CustomMenuItem SeparatorMenuItem ContextMenu 下图是一个典型的菜单的使用: 在应用中构建菜单 一个菜单就是一系列可操作的项目,可以根据用户的需要来表现。当一个菜单可见的时候,用户可以在某一时刻选中其中一个,在用户选中某一项时,这个菜单变成隐藏模式。通过使用菜单,我们可以节省用户界面的空间,因为有一些功能某些时间并不是总要现实出来的。 菜单在菜单栏中被分组,你需要使用下面的菜单项类,当你构建一个菜单的时候。 MenuItem 创建可选项 Menu 创建子菜单 RadioButtonItem 创建一个单选项 CheckMenuItem 这个菜单项可以在选择被无选择之间转换。 为了给菜单分类,可以使用SeparatorMenuItem 类。 菜单通常在窗口的顶部,并且这些菜单是隐藏的,我们可以通过鼠标点击上下文来打开菜单。 创建菜单栏 尽管菜单栏可以放在用户界面的任何地方,但是一般情况我们放到窗口的顶部。并且菜单栏可已自动的改变自己的大小。默认情况下,每一个菜单栏中的菜单像一个按钮一样呈现出来。 想想一个这样的一个应用,他显示植物的名称,图片,以及简单的描述信息。我们创建3个菜单项,:File,Edit,View.并给这三项添加菜单项。代码如下所示: import java.util.AbstractMap.SimpleEntry; import java.util.Map.Entry; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.effect.DropShadow; import javafx.scene.effect.Effect; import javafx.scene.effect.Glow; import javafx.scene.effect.SepiaTone; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.VBox; import javafx.stage.Stage; public class MenuSample extends Application { final PageData[] pages = new PageData[] { new PageData( "Apple" , "The apple is the pomaceous fruit of the apple tree, species Malus " + "domestica in the rose family (Rosaceae). It is one of the most " + "widely cultivated tree fruits, and the most widely known of " + "the many members of genus Malus that are used by humans. " + "The tree originated in Western Asia, where its wild ancestor, " + "the Alma, is still found today." , "Malus domestica" ), new PageData( "Hawthorn" , "The hawthorn is a large genus of shrubs and trees in the rose " + "family, Rosaceae, native to temperate regions of the Northern " + "Hemisphere in Europe, Asia and North America. " + "The name hawthorn was " + "originally applied to the species native to northern Europe, " + "especially the Common Hawthorn C. monogyna, and the unmodified " + "name is often so used in Britain and Ireland." , "Crataegus monogyna" ), new PageData( "Ivy" , "The ivy is a flowering plant in the grape family (Vitaceae) native to" + " eastern Asia in Japan, Korea, and northern and eastern China. " + "It is a deciduous woody vine growing to 30 m tall or more given " + "suitable support, attaching itself by means of numerous small " + "branched tendrils tipped with sticky disks." , "Parthenocissus tricuspidata" ), new PageData( "Quince" , "The quince is the sole member of the genus Cydonia and is native to " + "warm-temperate southwest Asia in the Caucasus region. The " + "immature fruit is green with dense grey-white pubescence, most " + "of which rubs off before maturity in late autumn when the fruit " + "changes color to yellow with hard, strongly perfumed flesh." , "Cydonia oblonga" ) }; final String[] viewOptions = new String[] { "Title" , "Binomial name" , "Picture" , "Description" }; final Entry[] effects = new Entry[] { new SimpleEntry<>( "Sepia Tone" , new SepiaTone()), new SimpleEntry<>( "Glow" , new Glow()), new SimpleEntry<>( "Shadow" , new DropShadow()) }; final ImageView pic = new ImageView(); final Label name = new Label(); final Label binName = new Label(); final Label description = new Label(); public static void main (String[] args) { launch(args); } @Override public void start (Stage stage) { stage.setTitle( "Menu Sample" ); Scene scene = new Scene( new VBox(), 400 , 350 ); MenuBar menuBar = new MenuBar(); // --- Menu File Menu menuFile = new Menu( "File" ); // --- Menu Edit Menu menuEdit = new Menu( "Edit" ); // --- Menu View Menu menuView = new Menu( "View" ); menuBar.getMenus().addAll(menuFile, menuEdit, menuView); ((VBox) scene.getRoot()).getChildren().addAll(menuBar); stage.setScene(scene); stage.show(); } private class PageData { public String name; public String description; public String binNames; public Image image; public PageData (String name, String description, String binNames) { this .name = name; this .description = description; this .binNames = binNames; image = new Image(getClass().getResourceAsStream(name + ".jpg" )); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 和其他的UI控件不同,Menu类和其他的扩展MenuItem的类都不是扩展自Node类。所以他们不能直接添加到场景中,需要先添加到MenuBar中然后在添加到场景中。运行如下图所示: 你可以使用键盘的方向键来浏览菜单,然而当你选中一个菜单的时候,什么都没有发生,那是因为我们还没有指定行为。 添加菜单项 为文件菜单添加功能。 Shuffle 加载植物的参考信息 Clear 删除参考信息并清空场景 Separator 分离菜单项 Exit 退出应用 使用MenuItem创建了一个Shuffle菜单,并添加了一个图片组件。我们可以为MenuItem指定文本和图片。我们可以通过setAction方法来指定该菜单被点击时候的事件,这个Button是一样的。代码如下: import java.util.AbstractMap.SimpleEntry; import java.util.Map.Entry; import javafx.application.Application; import javafx.event.ActionEvent; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.effect.DropShadow; import javafx.scene.effect.Effect; import javafx.scene.effect.Glow; import javafx.scene.effect.SepiaTone; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.text.Font; import javafx.scene.text.TextAlignment; import javafx.stage.Stage; public class MenuSample extends Application { final PageData[] pages = new PageData[] { new PageData( "Apple" , "The apple is the pomaceous fruit of the apple tree, species Malus " + "domestica in the rose family (Rosaceae). It is one of the most " + "widely cultivated tree fruits, and the most widely known of " + "the many members of genus Malus that are used by humans. " + "The tree originated in Western Asia, where its wild ancestor, " + "the Alma, is still found today." , "Malus domestica" ), new PageData( "Hawthorn" , "The hawthorn is a large genus of shrubs and trees in the rose " + "family, Rosaceae, native to temperate regions of the Northern " + "Hemisphere in Europe, Asia and North America. " + "The name hawthorn was " + "originally applied to the species native to northern Europe, " + "especially the Common Hawthorn C. monogyna, and the unmodified " + "name is often so used in Britain and Ireland." , "Crataegus monogyna" ), new PageData( "Ivy" , "The ivy is a flowering plant in the grape family (Vitaceae) native" + " to eastern Asia in Japan, Korea, and northern and eastern China." + " It is a deciduous woody vine growing to 30 m tall or more given " + "suitable support, attaching itself by means of numerous small " + "branched tendrils tipped with sticky disks." , "Parthenocissus tricuspidata" ), new PageData( "Quince" , "The quince is the sole member of the genus Cydonia and is native" + " to warm-temperate southwest Asia in the Caucasus region. The " + "immature fruit is green with dense grey-white pubescence, most " + "of which rubs off before maturity in late autumn when the fruit " + "changes color to yellow with hard, strongly perfumed flesh." , "Cydonia oblonga" ) }; final String[] viewOptions = new String[] { "Title" , "Binomial name" , "Picture" , "Description" }; final Entry[] effects = new Entry[] { new SimpleEntry<>( "Sepia Tone" , new SepiaTone()), new SimpleEntry<>( "Glow" , new Glow()), new SimpleEntry<>( "Shadow" , new DropShadow()) }; final ImageView pic = new ImageView(); final Label name = new Label(); final Label binName = new Label(); final Label description = new Label(); private int currentIndex = - 1 ; public static void main (String[] args) { launch(args); } @Override public void start (Stage stage) { stage.setTitle( "Menu Sample" ); Scene scene = new Scene( new VBox(), 400 , 350 ); scene.setFill(Color.OLDLACE); name.setFont( new Font( "Verdana Bold" , 22 )); binName.setFont( new Font( "Arial Italic" , 10 )); pic.setFitHeight( 150 ); pic.setPreserveRatio( true ); description.setWrapText( true ); description.setTextAlignment(TextAlignment.JUSTIFY); shuffle(); MenuBar menuBar = new MenuBar(); final VBox vbox = new VBox(); vbox.setAlignment(Pos.CENTER); vbox.setSpacing( 10 ); vbox.setPadding( new Insets( 0 , 10 , 0 , 10 )); vbox.getChildren().addAll(name, binName, pic, description); // --- Menu File Menu menuFile = new Menu( "File" ); MenuItem add = new MenuItem( "Shuffle" , new ImageView( new Image( "menusample/new.png" ))); add.setOnAction((ActionEvent t) -> { shuffle(); vbox.setVisible( true ); }); menuFile.getItems().addAll(add); // --- Menu Edit Menu menuEdit = new Menu( "Edit" ); // --- Menu View Menu menuView = new Menu( "View" ); menuBar.getMenus().addAll(menuFile, menuEdit, menuView); ((VBox) scene.getRoot()).getChildren().addAll(menuBar, vbox); stage.setScene(scene); stage.show(); } private void shuffle () { int i = currentIndex; while (i == currentIndex) { i = ( int ) (Math.random() * pages.length); } pic.setImage(pages[i].image); name.setText(pages[i].name); binName.setText( "(" + pages[i].binNames + ")" ); description.setText(pages[i].description); currentIndex = i; } private class PageData { public String name; public String description; public String binNames; public Image image; public PageData (String name, String description, String binNames) { this .name = name; this .description = description; this .binNames = binNames; image = new Image(getClass().getResourceAsStream(name + ".jpg" )); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 当用户选择Shuffle菜单的时候,会就调用我们指定的setOnAction方法,显示出植物的名字、图片和简单描述。 Clear菜单用来抹除场景中的内容。我们可以使用下面的方法: MenuItem clear = new MenuItem( "Clear" ); clear.setAccelerator(KeyCombination.keyCombination( "Ctrl+X" )); clear.setOnAction ((ActionEvent t) -> { vbox.setVisible( false ); }) ; 1 2 3 4 5 6 Exit菜单实现如下: MenuItem exit = new MenuItem( "Exit" ); exit.setOnAction ((ActionEvent t) -> { System.exit( 0 ); }) ; 1 2 3 4 5 我们使用getItems方法来添加这些菜单项,如下: menuFile .getItems () .addAll ( add , clear, new SeparatorMenuItem(), exit) ; 1 2 编译运行应用,如下图所示: View菜单可以来用指定,显示植物的部分信息, // --- Creating four check menu items within the start method CheckMenuItem titleView = createMenuItem ( "Title" , name); CheckMenuItem binNameView = createMenuItem ( "Binomial name" , binName); CheckMenuItem picView = createMenuItem ( "Picture" , pic); CheckMenuItem descriptionView = createMenuItem ( "Description" , description); menuView.getItems().addAll(titleView, binNameView, picView, descriptionView); ... // The createMenuItem method private static CheckMenuItem createMenuItem (String title, final Node node){ CheckMenuItem cmi = new CheckMenuItem(title); cmi.setSelected(true); cmi.selectedProperty().addListener( (ObservableValue ov, Boolean old_val, Boolean new_val) -> { node.setVisible(new_val); }); return cmi; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 CheckMenuItem 是MenuItem类的扩展,他可以选中和非选中,如果被选中了,会显示出一个标记。编译运行如下图所示: 创建子菜单 Edit菜单中定义了两个菜单项:图片效果和无效果。图片效果菜单项有子菜单项。 使用RadioMenuItem 来创建子菜单,代码如下: // Picture Effect menu Menu menuEffect = new Menu( "Picture Effect" ); final ToggleGroup groupEffect = new ToggleGroup(); for (Entry effect : effects) { RadioMenuItem itemEffect = new RadioMenuItem(effect.getKey()); itemEffect.setUserData(effect.getValue()); itemEffect.setToggleGroup(groupEffect); menuEffect.getItems().add(itemEffect); } // No Effects menu final MenuItem noEffects = new MenuItem( "No Effects" ); noEffects.setOnAction ((ActionEvent t) -> { pic.setEffect( null ); groupEffect.getSelectedToggle().setSelected( false ); }) ; // Processing menu item selection groupEffect . selectedToggleProperty () . addListener ( new ChangeListener() { public void changed(ObservableValue ov, Toggle old_toggle, Toggle new_toggle) { if (groupEffect.getSelectedToggle() != null ) { Effect effect = (Effect) groupEffect.getSelectedToggle().getUserData(); pic.setEffect(effect); } } }) ; groupEffect . selectedToggleProperty () . addListener ( (ObservableValue ov, Toggle old_toggle, Toggle new_toggle) -> { if (groupEffect.getSelectedToggle() != null ) { Effect effect = (Effect) groupEffect.getSelectedToggle().getUserData(); pic.setEffect(effect); } }) ; // Adding items to the Edit menu menuEdit . getItems () . addAll (menuEffect, noEffects) ; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 如下所示: 我们可以使用setDisable来禁用某一个菜单。 Menu menuEffect = new Menu( "Picture Effect" ); final ToggleGroup groupEffect = new ToggleGroup(); for (Entry effect : effects) { RadioMenuItem itemEffect = new RadioMenuItem(effect.getKey()); itemEffect.setUserData(effect.getValue()); itemEffect.setToggleGroup(groupEffect); menuEffect.getItems().add(itemEffect); } final MenuItem noEffects = new MenuItem( "No Effects" ); noEffects.setDisable( true ); noEffects.setOnAction ((ActionEvent t) -> { pic.setEffect( null ); groupEffect.getSelectedToggle().setSelected( false ); noEffects.setDisable( true ); }) ; groupEffect . selectedToggleProperty () . addListener ( (ObservableValue ov, Toggle old_toggle, Toggle new_toggle) -> { if (groupEffect.getSelectedToggle() != null ) { Effect effect = (Effect) groupEffect.getSelectedToggle().getUserData(); pic.setEffect(effect); noEffects.setDisable( false ); } else { noEffects.setDisable( true ); } }) ; menuEdit . getItems () . addAll (menuEffect, noEffects) ; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 添加上下文菜单 如果你的界面中没有为菜单的控件,那么你可以使用上下文菜单。当点击鼠标的时候,弹出上下文菜单。 final ContextMenu cm = new ContextMenu(); MenuItem cmItem1 = new MenuItem( "Copy Image" ); cmItem1.setOnAction ((ActionEvent e) -> { Clipboard clipboard = Clipboard.getSystemClipboard(); ClipboardContent content = new ClipboardContent(); content.putImage(pic.getImage()); clipboard.setContent(content); }) ; cm . getItems () . add (cmItem1) ; pic . addEventHandler (MouseEvent.MOUSE_CLICKED, (MouseEvent e) -> { if (e.getButton() == MouseButton.SECONDARY) cm.show(pic, e.getScreenX(), e.getScreenY()); }) ; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
来源:OSCHINA
发布时间:2020-06-03 08:19:00
Hi,大家好,我是明哥。 在自己学习 Golang 的这段时间里,我写了详细的学习笔记放在我的个人微信公众号 《Go编程时光》,对于 Go 语言,我也算是个初学者,因此写的东西应该会比较适合刚接触的同学,如果你也是刚学习 Go 语言,不防关注一下,一起学习,一起成长。 我的在线博客: http://golang.iswbm.com 我的 Github:github.com/iswbm/GolangCodingTime 刚接触 Go 语言的信道的时候,经常会遇到死锁的错误,而导致这个错误的原因有很多种,这里整理了几种常见的。 fatal error: all goroutines are asleep - deadlock! 错误示例一 看下面这段代码 package main import "fmt" func main() { pipline := make(chan string) pipline <- "hello world" fmt.Println(<-pipline) } 运行会抛出错误,如下 fatal error: all goroutines are asleep - deadlock! 看起来好像没有什么问题?先往信道中存入数据,再从信道中读取数据。 回顾前面的基础,我们知道使用 make 创建信道的时候,若不传递第二个参数,则你定义的是无缓冲信道,而对于无缓冲信道,在接收者未准备好之前,发送操作是阻塞的. 因此,对于解决此问题有两种方法: 使接收者代码在发送者之前执行 使用缓冲信道,而不使用无缓冲信道 第一种方法 : 若要程序正常执行,需要保证接收者程序在发送数据到信道前就进行阻塞状态,修改代码如下 package main import "fmt" func main() { pipline := make(chan string) fmt.Println(<-pipline) pipline <- "hello world" } 运行的时候还是报同样的错误。问题出在哪里呢? 原来我们将发送者和接收者写在了同一协程中,虽然保证了接收者代码在发送者之前执行,但是由于前面接收者一直在等待数据 而处于阻塞状态,所以无法执行到后面的发送数据。还是一样造成了死锁。 有了前面的经验,我们将接收者代码写在另一个协程里,并保证在发送者之前执行,就像这样的代码 package main func hello(pipline chan string) { <-pipline } func main() { pipline := make(chan string) go hello(pipline) pipline <- "hello world" } 运行之后 ,一切正常。 第二种方法 : 接收者代码必须在发送者代码之前 执行,这是针对无缓冲信道才有的约束。 既然这样,我们改使用可缓冲信道不就OK了吗? package main import "fmt" func main() { pipline := make(chan string, 1) pipline <- "hello world" fmt.Println(<-pipline) } 运行之后,一切正常。 错误示例二 每个缓冲信道,都有容量,当信道里的数据量等于信道的容量后,此时再往信道里发送数据,就失造成阻塞,必须等到有人从信道中消费数据后,程序才会往下进行。 比如这段代码,信道容量为 1,但是往信道中写入两条数据,对于一个协程来说就会造成死锁。 package main import "fmt" func main() { ch1 := make(chan string, 1) ch1 <- "hello world" ch1 <- "hello China" fmt.Println(<-ch1) } 错误示例三 当程序一直在等待从信道里读取数据,而此时并没有人会往信道中写入数据。此时程序就会陷入死循环,造成死锁。 比如这段代码,for 循环接收了两次消息("hello world"和“hello China”)后,再也没有人发送数据了,接收者就会处于一个等待永远接收不到数据的囧境。陷入死循环,造成死锁。 package main import "fmt" func main() { pipline := make(chan string) go func() { pipline <- "hello world" pipline <- "hello China" // close(pipline) }() for data := range pipline{ fmt.Println(data) } } 包子铺里的包子已经卖完了,可还有人在排队等着买,如果不再做包子,就要告诉排队的人:不用等了,今天的包子已经卖完了,明日请早呀。 不能让人家死等呀,不跟客人说明一下,人家还以为你们店后面还在蒸包子呢。 所以这个问题,解决方法很简单,只要在发送完数据后,手动关闭信道,告诉 range 信道已经关闭,无需等待就行。 package main import "fmt" func main() { pipline := make(chan string) go func() { pipline <- "hello world" pipline <- "hello China" close(pipline) }() for data := range pipline{ fmt.Println(data) } } 系列导读 01. 开发环境的搭建(Goland & VS Code) 02. 学习五种变量创建的方法 **03. 详解数据类型:** 整形与浮点型 04. 详解数据类型:byte、rune与string 05. 详解数据类型:数组与切片 06. 详解数据类型:字典与布尔类型 07. 详解数据类型:指针 08. 面向对象编程:结构体与继承 09. 一篇文章理解 Go 里的函数 10. Go语言流程控制:if-else 条件语句 11. Go语言流程控制:switch-case 选择语句 12. Go语言流程控制:for 循环语句 13. Go语言流程控制:goto 无条件跳转 14. Go语言流程控制:defer 延迟调用 15. 面向对象编程:接口与多态 16. 关键字:make 和 new 的区别? 17. 一篇文章理解 Go 里的语句块与作用域 18. 学习 Go 协程:goroutine 19. 学习 Go 协程:详解信道/通道 20. 几个信道死锁经典错误案例详解 21. 学习 Go 协程:WaitGroup 22. 学习 Go 协程:互斥锁和读写锁 23. Go 里的异常处理:panic 和 recover 24. 超详细解读 Go Modules 前世今生及入门使用 25. Go 语言中关于包导入必学的 8 个知识点 26. 如何开源自己写的模块给别人用? 27. 说说 Go 语言中的类型断言? 28. 这五点带你理解Go语言的select用法
来源:OSCHINA
发布时间:2020-06-03 08:10:00
1.首先导入使用Maven导入jar包 org.springframework.boot spring-boot-starter-data-redis com.alibaba fastjson 1.2.62 2.在application.properties配置信息 # Redis数据库索引(默认为0) spring.redis.database=0 # Redis服务器地址 spring.redis.host=localhost # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password=123456 # 连接池最大连接数(使用负值表示没有限制) spring.redis.pool.max-active=200 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.pool.max-wait=-1 # 连接池中的最大空闲连接 spring.redis.pool.max-idle=10 # 连接池中的最小空闲连接 spring.redis.pool.min-idle=0 # 连接超时时间(毫秒) spring.redis.timeout=1000ms 3.编写Redis工具类 @Configuration @ConditionalOnClass(RedisOperations.class) //系统中有RedisOperations类时 @EnableConfigurationProperties(RedisProperties.class) //启动RedisProperties这个类 @EnableCaching // www.1b23.com public class RedisConfig extends CachingConfigurerSupport { @Autowired RedisTemplate redisTemplate; // 配置缓存管理器 @Bean public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { LettuceConnectionFactory jedisConnectionFactory = (LettuceConnectionFactory) redisTemplate.getConnectionFactory(); jedisConnectionFactory.setDatabase(2); //指定dbindex redisTemplate.setConnectionFactory(jedisConnectionFactory); jedisConnectionFactory.resetConnection(); RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(60*20)) // 20分钟缓存失效 // 设置key的序列化方式 // .entryTtl(Duration.ofSeconds(10)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // 设置value的序列化方式 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new FastJsonRedisSerializer(Object.class))) // 不缓存null值 .disableCachingNullValues(); RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .transactionAware() .build(); return redisCacheManager; } } package com.FireService.config; import java.nio.charset.Charset; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; import com.alibaba.fastjson.serializer.SerializerFeature; public class FastJsonRedisSerializer implements RedisSerializer { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); private Class clazz; static { ParserConfig.getGlobalInstance().addAccept("com.FireService"); } public FastJsonRedisSerializer(Class clazz) { super(); this.clazz = clazz; } @Override public byte[] serialize(T t) throws SerializationException { if (null == t) { return new byte[0]; } return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } @Override public T deserialize(byte[] bytes) throws SerializationException { if (null == bytes || bytes.length <= 0) { return null; } String str = new String(bytes, DEFAULT_CHARSET); return (T) JSON.parseObject(str, clazz); } } 4.SpringBoot有关缓存的几个注解 @Cacheable:查询 可选属性: cacheNames/value:指定缓存组件的名字; key:缓存数据使用的key,可以用来指定。默认即使用方法参数的值 keyGenerator:key的生成器,可以自己指定key的生成器的组件id //自定义配置类配置keyGenerator @Configuration public class MyCacheConfig { @Bean("myKeyGenerator") public KeyGenerator keyGenerator(){ return new KeyGenerator() { @Override public Object generate(Object target, Method method, Object... params) { return method.getName()+"["+ Arrays.asList(params).toString() +"]"; } }; } } cacheManager:指定缓存管理器;或者cacheResolver获取指定解析器 condition:指定符合条件的情况下才缓存;如condition="#id>0" unless:否定缓存,当unless指定的条件为true,方法的返回值不会被缓存,可以获取到结果进行判断;如unless="#result==null"; sync:是否使用异步模式 例如: @Cacheable(value = "RedisInfo", key = "#root.methodName+'['+#account+']'") @ResponseBody @RequestMapping("/RedisTest") public Result findUserOrder(String account) throws Exception{ if(account!=null) { List> list=orderFindGoods.findUserOrder(account); return Results.successWithData(list, BaseEnums.SUCCESS.code(), BaseEnums.SUCCESS.desc()); }else { return Results.failure(); } } 运行项目查看结果 1.第一次访问 查看Druid连接信息
来源:OSCHINA
发布时间:2020-06-03 07:45:00
简介 你知道序列化可以使用代理吗?你知道序列化的安全性吗?每个java程序员都听说过序列化,要存储对象需要序列化,要在网络上传输对象要序列化,看起来很简单的序列化其实里面还隐藏着很多小秘密,今天本文将会为大家一一揭秘。 更多精彩内容且看: 区块链从入门到放弃系列教程-涵盖密码学,超级账本,以太坊,Libra,比特币等持续更新 Spring Boot 2.X系列教程:七天从无到有掌握Spring Boot-持续更新 Spring 5.X系列教程:满足你对Spring5的一切想象-持续更新 java程序员从小工到专家成神之路(2020版)-持续更新中,附详细文章教程 更多内容请访问 www.flydean.com 什么是序列化 序列化就是将java对象按照一定的顺序组织起来,用于在网络上传输或者写入存储中。而反序列化就是从网络中或者存储中读取存储的对象,将其转换成为真正的java对象。 所以序列化的目的就是为了传输对象,对于一些复杂的对象,我们可以使用第三方的优秀框架,比如Thrift,Protocol Buffer等,使用起来非常的方便。 JDK本身也提供了序列化的功能。要让一个对象可序列化,则可以实现java.io.Serializable接口。 java.io.Serializable是从JDK1.1开始就有的接口,它实际上是一个marker interface,因为java.io.Serializable并没有需要实现的接口。继承java.io.Serializable就表明这个class对象是可以被序列化的。 [@Data](https://my.oschina.net/difrik) @AllArgsConstructor public class CustUser implements java.io.Serializable{ private static final long serialVersionUID = -178469307574906636L; private String name; private String address; } 上面我们定义了一个CustUser可序列化对象。这个对象有两个属性:name和address。 接下看下怎么序列化和反序列化: public void testCusUser() throws IOException, ClassNotFoundException { CustUser custUserA=new CustUser("jack","www.flydean.com"); CustUser custUserB=new CustUser("mark","www.flydean.com"); try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(custUserA); objectOutputStream.writeObject(custUserB); } try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); CustUser custUser1 = (CustUser) objectInputStream.readObject(); CustUser custUser2 = (CustUser) objectInputStream.readObject(); log.info("{}",custUser1); log.info("{}",custUser2); } } 上面的例子中,我们实例化了两个CustUser对象,并使用objectOutputStream将对象写入文件中,最后使用ObjectInputStream从文件中读取对象。 上面是最基本的使用。需要注意的是CustUser class中有一个serialVersionUID字段。 serialVersionUID是序列化对象的唯一标记,如果class中定义的serialVersionUID和序列化存储中的serialVersionUID一致,则表明这两个对象是一个对象,我们可以将存储的对象反序列化。 如果我们没有显示的定义serialVersionUID,则JVM会自动根据class中的字段,方法等信息生成。很多时候我在看代码的时候,发现很多人都将serialVersionUID设置为1L,这样做是不对的,因为他们没有理解serialVersionUID的真正含义。 重构序列化对象 假如我们有一个序列化的对象正在使用了,但是突然我们发现这个对象好像少了一个字段,要把他加上去,可不可以加呢?加上去之后原序列化过的对象能不能转换成这个新的对象呢? 答案是肯定的,前提是两个版本的serialVersionUID必须一样。新加的字段在反序列化之后是空值。 序列化不是加密 有很多同学在使用序列化的过程中可能会这样想,序列化已经将对象变成了二进制文件,是不是说该对象已经被加密了呢? 这其实是序列化的一个误区,序列化并不是加密,因为即使你序列化了,还是能从序列化之后的数据中知道你的类的结构。比如在RMI远程调用的环境中,即使是class中的private字段也是可以从stream流中解析出来的。 如果我们想在序列化的时候对某些字段进行加密操作该怎么办呢? 这时候可以考虑在序列化对象中添加writeObject和readObject方法: private String name; private String address; private int age; private void writeObject(ObjectOutputStream stream) throws IOException { //给age加密 age = age + 2; log.info("age is {}", age); stream.defaultWriteObject(); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); log.info("age is {}", age); //给age解密 age = age - 2; } 上面的例子中,我们为CustUser添加了一个age对象,并在writeObject中对age进行了加密(加2),在readObject中对age进行了解密(减2)。 注意,writeObject和readObject都是private void的方法。他们的调用是通过反射来实现的。 使用真正的加密 上面的例子, 我们只是对age字段进行了加密,如果我们想对整个对象进行加密有没有什么好的处理办法呢? JDK为我们提供了javax.crypto.SealedObject 和java.security.SignedObject来作为对序列化对象的封装。从而将整个序列化对象进行了加密。 还是举个例子: public void testCusUserSealed() throws IOException, ClassNotFoundException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException { CustUser custUserA=new CustUser("jack","www.flydean.com"); Cipher enCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); Cipher deCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKey secretKey = new SecretKeySpec("saltkey111111111".getBytes(), "AES"); IvParameterSpec iv = new IvParameterSpec("vectorKey1111111".getBytes()); enCipher.init(Cipher.ENCRYPT_MODE, secretKey, iv); deCipher.init(Cipher.DECRYPT_MODE,secretKey,iv); SealedObject sealedObject= new SealedObject(custUserA, enCipher); try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(sealedObject); } try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); SealedObject custUser1 = (SealedObject) objectInputStream.readObject(); CustUser custUserV2= (CustUser) custUser1.getObject(deCipher); log.info("{}",custUserV2); } } 上面的例子中,我们构建了一个SealedObject对象和相应的加密解密算法。 SealedObject就像是一个代理,我们写入和读取的都是这个代理的加密对象。从而保证了在数据传输过程中的安全性。 使用代理 上面的SealedObject实际上就是一种代理,考虑这样一种情况,如果class中的字段比较多,而这些字段都可以从其中的某一个字段中自动生成,那么我们其实并不需要序列化所有的字段,我们只把那一个字段序列化就可以了,其他的字段可以从该字段衍生得到。 在这个案例中,我们就需要用到序列化对象的代理功能。 首先,序列化对象需要实现writeReplace方法,表示替换成真正想要写入的对象: public class CustUserV3 implements java.io.Serializable{ private String name; private String address; private Object writeReplace() throws java.io.ObjectStreamException { log.info("writeReplace {}",this); return new CustUserV3Proxy(this); } } 然后在Proxy对象中,需要实现readResolve方法,用于从系列化过的数据中重构序列化对象。如下所示: public class CustUserV3Proxy implements java.io.Serializable{ private String data; public CustUserV3Proxy(CustUserV3 custUserV3){ data =custUserV3.getName()+ "," + custUserV3.getAddress(); } private Object readResolve() throws java.io.ObjectStreamException { String[] pieces = data.split(","); CustUserV3 result = new CustUserV3(pieces[0], pieces[1]); log.info("readResolve {}",result); return result; } } 我们看下怎么使用: public void testCusUserV3() throws IOException, ClassNotFoundException { CustUserV3 custUserA=new CustUserV3("jack","www.flydean.com"); try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(custUserA); } try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); CustUserV3 custUser1 = (CustUserV3) objectInputStream.readObject(); log.info("{}",custUser1); } } 注意,我们写入和读出的都是CustUserV3对象。 Serializable和Externalizable的区别 最后我们讲下Externalizable和Serializable的区别。Externalizable继承自Serializable,它需要实现两个方法: void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; 什么时候需要用到writeExternal和readExternal呢? 使用Serializable,Java会自动为类的对象和字段进行对象序列化,可能会占用更多空间。而Externalizable则完全需要我们自己来控制如何写/读,比较麻烦,但是如果考虑性能的话,则可以使用Externalizable。 另外Serializable进行反序列化不需要执行构造函数。而Externalizable需要执行构造函数构造出对象,然后调用readExternal方法来填充对象。所以Externalizable的对象需要一个无参的构造函数。 总结 本文详细分析了序列化对象在多种情况下的使用,并讲解了Serializable和Externalizable的区别,希望大家能够喜欢。 本文作者:flydean程序那些事 本文链接: http://www.flydean.com/java-serialization/ 本文来源:flydean的博客 欢迎关注我的公众号:程序那些事,更多精彩等着您!
来源:OSCHINA
发布时间:2020-06-03 06:19:00
$event代表当前事件对象,这个事件对象是默认传的,可以不传也行。 附上事件默认行为 百度一下 阻止事件冒泡
将一个html属性值变成表达式,可以用强制绑定,在属性前面加一个冒号即可 :src="imageUrl"

1. 双大括号表达式

{{ content }}

{{ content . toUpperCase ()}}

2. 指令一 : 强制数据绑定

访问指定站点
访问指定站点 2
访问指定站点 2

3. 指令二 : 绑定事件监听

Vue实例的方法以$符号开头,比如下面的写法 vm.$swtich(){} 停止事件冒泡,当事件外层还有事件可,可以停止外层事件 @Click.stop 阻止事件的默认行为 @Click.prevent 键盘按键修饰符 @Click.enter 表单数据的自动收集,使用v-model 生命周期常用的两个方法 mounted(),初始化阶段,这个方法只执行一次,比如改善Ajax方法,执行定时期 更新阶段的方法执行N次,可以是0次 BeforDestory(),死亡阶段只执行一次 自定义过滤器,比如可以对页面上的时间日期进行特定的格式化 比如2020-02-02 // 定义过滤器 Vue . filter ( 'dateString' , function (value , format= 'YYYY-MM-DD HH:mm:ss' ) { return moment(value). format (format) ; }) 这个网址是日期格式化的API,比较好用 https://www.bootcdn.cn/moment.js/ 常用内置指令 v:text : 更新元素的 textContent v-html : 更新元素的 innerHTML v-if : 如果为 true, 当前标签才会输出到页面 v-else: 如果为 false, 当前标签才会输出到页面 v-show : 通过控制 display 样式来控制显示 / 隐藏 v-for : 遍历数组 / 对象 v-on : 绑定事件监听 , 一般简写为 @ v-bind : 强制绑定解析表达式 , 可以省略 v-bind v-model : 双向数据绑定 ref : 为某个元素注册一个唯一标识 , vue 对象通过 $refs 属性访问这个元素对象 v-cloak : 使用它防止闪现表达式 , 与 css 配合 : [v-cloak] { display: none } 1. 注册全局指令 Vue.directive('my-directive', function(el, binding){ el.innerHTML = binding.value.toupperCase() }) 2. 注册局部指令 directives : { 'my-directive' : { bind (el, binding) { el.innerHTML = binding.value.toupperCase() } } } 3. 使用指令 v-my-directive='xxx' -->
来源:OSCHINA
发布时间:2020-06-03 00:22:00
以前使用的,查看函数运行时间的方法: #encoding:utf-8 import time def trainModel(): print("training...") time.sleep(2) def testModel(): print("testing...") time.sleep(1) if __name__ == "__main__": start = time.time() trainModel() #训练模型 end = time.time() print("cost time:",end-start) start = time.time() testModel() #测试模型 end = time.time() print("cost time:",end-start) """ training... cost time: 2.002230644226074 testing... cost time: 1.0011751651763916 """ 或者是 #encoding:utf-8 import time def trainModel(): start = time.time() #写入函数内部 print("training...") end = time.time() print("cost time:",end-start) def testModel(): start = time.time() print("testing...") end = time.time() print("cost time:",end-start) if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.1682510375976562e-05 testing... cost time: 2.1457672119140625e-06 """ 使用装饰器后发现真好用!!! 装饰器(装饰器为函数,被装饰的也是函数) #encoding:utf-8 import time def costTime(func): def wrapper(*args, **kwargs): start = time.time() f = func(*args, **kwargs) end = time.time() print("cost time:",end-start) return f return wrapper #相当于 costTime(trainModel()) #将trainModel拿到costTime中的wrapper中运行 #即可在trainModel()前后添加装饰代码 @costTime def trainModel(): print("training...") @costTime def testModel(): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.1682510375976562e-05 testing... cost time: 2.1457672119140625e-06 """ #装饰器(装饰器为函数,被装饰的也是函数,传参数) #encoding:utf-8 import time def costTime(flag): def wrapper(func): def inner_wrapper(*args, **kwargs): start = time.time() f = func(*args, **kwargs) end = time.time() if flag == "True": print("cost time:",end-start) return f return inner_wrapper return wrapper @costTime(flag = "True") def trainModel(): print("training...") @costTime(flag = "False") def testModel(): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.2159347534179688e-05 testing... """ #装饰器(装饰器为函数,被装饰的是类) #encoding:utf-8 import time def costTime(func): def wrapper(*args, **kwargs): start = time.time() f = func(*args, **kwargs) end = time.time() print("cost time:",end-start) return f return wrapper @costTime class trainModel: def __init__(self): print("training...") @costTime class testModel: def __init__(self): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.2159347534179688e-05 testing... cost time: 2.86102294921875e-06 """ #装饰器(装饰器为函数,被装饰的是类,传参数) #encoding:utf-8 import time def costTime(flag): def wrapper(func): def inner_wrapper(*args, **kwargs): start = time.time() f = func(*args, **kwargs) end = time.time() if flag == "True": print("cost time:",end-start) return f return inner_wrapper return wrapper @costTime(flag = "True") class trainModel: def __init__(self): print("training...") @costTime(flag = "False") class testModel: def __init__(self): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.1920928955078125e-05 testing... """ #装饰器(装饰器为类,被装饰的是函数) #encoding:utf-8 import time class costTime(): def __init__(self,func): self.fun = func #重载__call__方法是就需要接受一个函数并返回一个函数 def __call__(self,*args, **kwargs): start = time.time() f = self.fun(*args, **kwargs) end = time.time() print("cost time:",end-start) return f @costTime def trainModel(): print("training...") @costTime def testModel(): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.1682510375976562e-05 testing... cost time: 2.1457672119140625e-06 """ #装饰器(装饰器为类,被装饰的也是函数,传参数) #encoding:utf-8 import time class costTime: def __init__(self,flag): self.flag = flag def __call__(self,func): def wrapper(*args, **kwargs): start = time.time() f = func(*args, **kwargs) end = time.time() if self.flag == "True": print("cost time:",end-start) return f return wrapper @costTime(flag = "True") def trainModel(): print("training...") @costTime(flag = "False") def testModel(): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.2874603271484375e-05 testing... """ #装饰器(装饰器为类,被装饰的也是类) #encoding:utf-8 import time class costTime(): def __init__(self,func): self.fun = func #重载__call__方法是就需要接受一个函数并返回一个函数 def __call__(self,*args, **kwargs): start = time.time() f = self.fun(*args, **kwargs) end = time.time() print("cost time:",end-start) return f @costTime class trainModel: def __init__(self): print("training...") @costTime class testModel: def __init__(self): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.2159347534179688e-05 testing... cost time: 2.86102294921875e-06 """ #装饰器(装饰器为函数,被装饰的是类,传参数) #encoding:utf-8 import time class costTime: def __init__(self,flag): self.flag = flag def __call__(self,func): def wrapper(*args, **kwargs): start = time.time() f = func(*args, **kwargs) end = time.time() if self.flag == "True": print("cost time:",end-start) return f return wrapper @costTime(flag = "True") class trainModel: def __init__(self): print("training...") @costTime(flag = "False") class testModel: def __init__(self): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.1920928955078125e-05 testing... """
来源:OSCHINA
发布时间:2020-06-02 23:40:00
v-for遍历数组
  • 用这个v-for时,后面最好跟一个 :key 这是标准,当然,不跟也行,只是不标准。 数组更新检测, 变更方法 Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括: push() pop() shift() unshift() splice() sort() reverse() 理解这些这些方法,这是VUE对JavaScript原生的数组方法进行了重写,重写的目的是为了达到监视数组,以及数组元素的变化。从而达到当数组元素数据变化的时候,页面也相应的发生改变。说白了就是想达到监视数组变化,实现数据绑定的效果。 这些重写的方法内部执行过程,先调用原生JavaScript数组,再更新页面 其中seplice()方法比较强大,可以做增加,删除,修改三个操作。
  • 来源:OSCHINA
    发布时间:2020-06-02 23:38:00
    Spring boot 版本: 2.2.1.RELEASE Spring cloud 版本 : Hoxton.RC1 项目中同时使用了 spring-cloud-starter-zipkin spring-boot-starter-data-redis org.springframework.cloud spring-cloud-starter-zipkin org.springframework.boot spring-boot-starter-data-redis //www.1b23.com 在编码中有多个Service同时注入了RedisTemplate redisTemplate导致在Tomcat启动时报错,导致无法运行。 错误如下 [ main] o.a.c.loader.WebappClassLoaderBase : The web application [ROOT] appears to have started a thread named [lettuce-eventExecutorLoop-1-1] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread: sun.misc.Unsafe.park(Native Method)` … Caused by: org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisException: Cannot retrieve initial cluster partitions from initial URIs [RedisURI [host=‘172.31.214.234’, port=6380], RedisURI [host=‘172.31.214.234’, port=6381], RedisURI [host=‘172.31.214.235’, port=6380], RedisURI [host=‘172.31.214.235’, port=6381], RedisURI [host=‘172.31.214.236’, port=6380], RedisURI [host=‘172.31.214.236’, port=6381] 最终原因是Redis 客户端lettuce无法连接上Redis导致该报错。 解决 移除spring-boot-starter-data-redis中对lettuce的引用,加入jedis的依赖 。springcloud框架www.1b23.com org.springframework.boot spring-boot-starter-data-redis io.lettuce lettuce-core redis.clients jedis //www.1b23.com 当我移除spring-cloud-starter-zipkin 之后再次启动项目发现启动成功,spring-boot-starter-data-redis默认使用lettuce作为Redis的客户端,因此推断为spring-cloud-starter-zipkin 可能和lettuce有兼容性问题。 因此尝试把lettuce换为jedis,问题解决。
    来源:OSCHINA
    发布时间:2020-06-17 13:07:00
    >作者 谢恩铭,公众号「程序员联盟」(微信号:coderhub)。 转载请注明出处。 原文: https://www.jianshu.com/p/6cbf452666bd > 《C语言探索之旅》 全系列 内容简介 前言 题目规定 优化建议 第二部分第十课预告 1. 前言 第二部分的理论知识基本讲完了。上一课我们经历了很有意思的 C语言探索之旅 | 第二部分第八课:动态分配 。 这一课我们来实战一下,要实现的游戏叫“悬挂小人”。 >这个“小人”,不是“君子和小人”的小人。是 little man(小小的人)的意思。 读者:“你有必要这么强调吗?简直无聊嘛...” 好的,话休絮烦... 俗语说得好:“实践是必要的!” 对于大家来说这又尤为重要,因为我们刚刚结束了一轮 C语言的高级技术的“猛烈进攻”,需要好好复习一下,消化消化。 不论你多厉害,在编程领域,不实践是永远不行的。尽管你可能读懂了之前的所有课程,但是如果不配合一定的实践,是不能深刻理解的。 以前我大学里入门编程以前看 C语言的书,觉得看懂了,但是一上手要写程序,就像挤牙膏一样费劲。 这次的实战练习,我们一起来实现一个小游戏:“悬挂小人”,或叫 “上吊游戏”。英语叫 HangMan ,是挺著名的一个休闲益智游戏。 虽说是游戏,但是比较可惜的是还不能有图形界面 (不过课程后面会说怎么实现在控制台绘制小人,其实也可以实现简陋的“图形化”): 因为 C语言本身不具备绘制 GUI(Graphical User Interface 的缩写,表示“图形用户接口”)的能力,需要引入第三方的库。 悬挂小人游戏是一个经典的字母游戏,在规定步数内一个字母一个字母地猜单词,直到猜出整个单词。 所以我们的游戏暂时还是以控制台的形式(黑框框)与大家见面,当然如果你会图形编程,也可以把这个游戏扩展成图形界面的。 相信不少读者应该见过这个游戏的图形界面版本,就是每猜错一个字母画一笔,直到用完规定次数,小人被“吊死”。 这个实战的目的是让我们可以复习之前学过的所有 C语言知识:指针,字符串,文件读写,结构体,数组,等等,都是好家伙! 2. 题目规定 既然是出题目的实战,那么就需要委屈大家按照我的题目要求来编写这个游戏啦。 好,就来公布我们的题目要求: 游戏每一轮有 7 次(次数可以设置,不一定是 7 次)猜测的机会,用完则此轮失败。 每轮会从字典中随机抽取一个单词供玩家猜,初始时单词是以若干个星号( * )的方式来表示。说明所有字母都还隐藏着。 字典的所有单词储存在一个文本文件中(在 Windows 下通常是 txt 文件,在 Unix/Linux/macOS 下一般可以是任意后缀名的文件)。 每猜错一个字母就扣掉一次机会,猜对一个字母不扣除机会数。猜对的字母会显示在屏幕上的单词中,替换掉星号。 一个回合的运作机制 假设要猜的单词是 OSCAR。 假设我们给程序输入一个字母 B(猜的第一个字母),程序会验证字母是否在这个单词里。 有两种情况: 所猜的字母在单词中,此时程序会显示这个单词,不是全部显示,而是显示猜到的那些字母,其他的还未猜到的字母用 * 表示。 所猜的字母不在单词中(目前的情况,因为字母 B 不在单词 OSCAR 中),此时程序会告诉玩家“你猜错了”,剩余的机会数会被扣除一个。如果剩余机会数变为 0,游戏结束。 在图形化的“悬挂小人”(Hangman)游戏中,每猜一次会有一个小人被画出来。我们的游戏,虽然还不能真正实现图形化,但是如果优化一下,也可以在控制台实现类似这样的效果: 假设玩家输入一个 C,因为 C 在单词 OSCAR 中,那么程序不会扣除玩家的剩余机会数,而且会显示已猜到的字母,如下: 单词:**C** 如果玩家继续输入,这回输入的是 O,那么程序会显示如下: 单词:O*C** 多个相同字母的情况 有一些单词中,同一个字母会出现多次。比如在 APPLE(表示“苹果”)中,P 这个字母就出现了 2 次;在 ELEGANCE(表示“优雅”)中,E 这个字母出现了 3 次。 Hangman 游戏对此的规则很简单:只要猜出一个字母,其他重复的字母会同时显示。 假如要猜的单词是 ELEGANCE,用户输入了一个 E,那么会如下显示: 单词:E*E****E 一个回合的例子 欢迎来到悬挂小人游戏! 您还剩 7 次机会 神秘单词是什么呢?***** 输入一个字母:E 您还剩 6 次机会 神秘单词是什么呢?***** 输入一个字母:S 您还剩 6 次机会 神秘单词是什么呢?*S*** 输入一个字母:R 您还剩 6 次机会 神秘单词是什么呢?*S**R 输入一个字母: 游戏就会这样进行下去,直到玩家在 7 个机会用完前猜到单词,或者用完 7 个机会还没猜到单词,游戏结束。 例如: 您还剩 2 次机会 神秘单词是什么呢?OS*AR 输入一个字母:C 胜利了!神秘单词是:OSCAR 在控制台输入一个字母 在控制台中让程序读入一个字母,看起来简单,但其实暗藏玄机。不信我们来试一下。 要输入一个字母,一般大家会认为是这样做: scanf("%c", &myLetter); 确实是不错的,因为 %c 标明了等待用户输入一个字符。输入的字符会储存在 myLetter 这个变量(类型是 char)中。 如果我们只写一个 scanf,那是没问题的。但是假如有好几个 scanf,会怎么样呢?我们来测试一下: int main(int argc, char* argv[]) { char myLetter = 0; scanf("%c", &myLetter); printf("%c", myLetter); scanf("%c", &myLetter); printf("%c", myLetter); return 0; } 照我们的设想,上述程序应该会请求用户输入一个字符,再打印出来: 进行两次。 测试一下,实际情况是怎么样的呢?你输入了一个字符,没错,然后呢... 程序为你打印出来了你输入的那个字符,假如你输入的是 a,那么程序输出 a 然后程序就退出了,没有下文了。为什么不提示我输入第二个字符了呢?就好像它忽略了第二个 scanf 一样。到底发生了什么呢? 事实上,当你在控制台(console)里面输入时,你输入的内容都被记录到内存的某处,当然也包括按下 Enter 键(回车键)时产生的输入: \n 因此,你先输入了一个字符(例如 a),然后你按了一下回车键: 字符 a 就被第一个 scanf 取走了,第二个 scanf 则把你的回车键( \n )取走了。 为了避免这个问题,我们写一个函数 readCharacter() 来处理: char readCharacter() { char character = 0; character = getchar(); // 读取输入的第一个字母 character = toupper(character); // 把这个字母转成大写 // 读取其他的字符,直到 \n (为了忽略它们) while (getchar() != '\n') ; return character; // 返回读到的第一个字母 } 可以看到,以上程序中,我们使用了 getchar 函数,这个函数是在标准库的 stdio.h 中,用于读取一个用户输入的字符,效果相当于 scanf("%c", &letter); 然后,我们又用到了一个在本课程中还没学习过的函数:toupper。 根据字面意思 to + upper 是英语“转换为大写”的意思,所以这个函数就是用于把一个字母转成大写字母。 看到了吧,如果函数名起得好,几乎就不需要注释,看名字就知道大致是干什么的(论编程命名的重要性)。 借着 toupper 这个函数,玩家就可以输入小写字母或者大写字母了,因为在“悬挂小人”游戏中,我们显示的单词中的字母都是大写的。 toupper 这个函数定义在 ctype.h 这个标准库的头文件中,所以需要 #include 继续看我们的函数,可以看到其中最关键的地方是: while (getchar() != '\n') ; 这一小段代码使得我们可以清除第一个输入的字母外的其他字符,直到遇见 \n (回车符)。 函数返回的就是第一个输入的字母,这样可以保证不再受回车符的影响了。 我们用了一个 while 循环,而循环体部分只有一个分号( ; ),很简洁吧。 也许你会问,之前的课程中 while 循环的循环体不是由大括号围起来的么,怎么这里只有一个分号呢? 事实上,这个分号就相当于 { } 就是空循环体,什么都不做,所以其实以上的代码相当于: while (getchar() != '\n') { } 但是分号比大括号写起来更简单么,不要忘了程序员是懂得如何偷懒的一群人! 此 while 循环一直执行,直到用户输入回车符,其他的字符都被从内存中清除了,我们称其为 “清空缓冲区”。 因此:  为了在我们的程序中每次读取用户输入的一个字母,我们不要使用 scanf("%c", &myLetter); 而须要借助我们写的函数: myLetter = readCharacter(); 于是,我们的测试程序变成这样: #include #include char readCharacter() { char character = 0; character = getchar(); // 读取一个字母 character = toupper(character); // 把这个字母转成大写 // 读取其他的字符,直到 \n (为了忽略它) while (getchar() != '\n') ; return character; // 返回读到的第一个字母 } int main(int argc, char* argv[]) { char myLetter = 0; myLetter = readCharacter(); printf("%c\n", myLetter); myLetter = readCharacter(); printf("%c\n", myLetter); return 0; } 运行,输出类似如下(假如用户输入 o,回车;输入 k,回车): o O k K 字典 / 词库 因为我们的游戏是一步步写成的,所以一开始,肯定先写简单的,再逐步完善游戏。 因此,猜测的单词一开始我们只用一个。所以,我们一开始会这么写: char secretWord[] = "BOTTLE"; 你会说:“这样不是很无聊嘛,猜测的单词总是这一个”。 是的,但之后我们肯定会扩展。一开始这样做是为了不把问题复杂化,一次做一件事情,慢慢来么。 之后如果猜测一个单词的代码可以运行了,我们再用一个文件来储存所有可能的单词,这个文件可以起名为 dictionary(表示“字典”)。 那什么是字典或词库呢? 在我们的游戏里,就是一个文件,文件中的每一行存放了一个单词,之后我们的程序会随机从此文件中抽取一个单词来作为每一轮的猜测单词。 词库是类似这样的: YOU MOTHER LOVE PANDA BOTTLE FUNNY HONEY LIKE JAZZ MUSIC BREAD APPLE WATER PEOPLE 至于这个文件里有多少单词,因为我们的词库是可扩展的(之后肯定可以添加新的单词),所以其实只要统计回车符( \n )的数目就可以,因为是每行一个单词。 好了,游戏的基本点我们介绍到这里,其实有了前面所有课程的基础,你已经有能力来完成这个看似有点复杂的游戏了,不过要组织得好还是不那么容易的,你可以用多个函数来实现不同的功能。 加油,坚持不懈就是胜利,期待你的成果! 3. 优化建议 >如果你是在 Windows 下用 CodeBlocks 等 IDE 来编译的,那么请将字典文件 dictionary 改成 dictionary.txt。 因为 Windows 的文件储存形式和 Linux/Unix/macOS 有些不一样。 改进游戏 目前来说,我们只让玩家玩一轮,如果能加一个循环,使得游戏每次询问玩家是否要再玩一次,那“真真是极好的”。 目前还是单机模式,可以创建一个二人模式,就是一个玩家输入一个单词,第二个玩家来猜。 为什么不用 printf 函数来打印(绘制)一个悬挂小人呢?在每次我们猜错的时候,就把它画出来,每错一个,多画一笔,这样可以增加乐趣,可以用如下的代码: if (猜错1个字母) { printf(" _____\n"); printf(" | |\n"); printf(" | O\n"); printf(" |\n"); printf(" |\n"); printf(" |\n"); printf(" |\n"); printf("_|__\n"); } else if (猜错2个字母) { printf(" _____\n"); printf(" | |\n"); printf(" | O\n"); printf(" | |\n"); printf(" |\n"); printf(" |\n"); printf(" |\n"); printf("_|__\n"); } else if (猜错3个字母) { printf(" _____\n"); printf(" | |\n"); printf(" | O\n"); printf(" | \\|\n"); printf(" |\n"); printf(" |\n"); printf(" |\n"); printf("_|__\n"); } else if (猜错4个字母) { printf(" _____\n"); printf(" | |\n"); printf(" | O\n"); printf(" | \\|/\n"); printf(" |\n"); printf(" |\n"); printf(" |\n"); printf("_|__\n"); } else if (猜错5个字母) { printf(" _____\n"); printf(" | |\n"); printf(" | O\n"); printf(" | \\|/\n"); printf(" | |\n"); printf(" |\n"); printf(" |\n"); printf("_|__\n"); } else if (猜错6个字母) { printf(" _____\n"); printf(" | |\n"); printf(" | O\n"); printf(" | \\|/\n"); printf(" | |\n"); printf(" | /\n"); printf(" |\n"); printf("_|__\n"); } else if (猜错7个字母) { printf(" _____\n"); printf(" | |\n"); printf(" | O\n"); printf(" | \\|/\n"); printf(" | |\n"); printf(" | / \\\n"); printf(" |\n"); printf("_|__\n"); } 上面代码中的空格也许不同平台的显示不一样,可能需要大家自行调整。 如果 7 次机会全部用完,则小人挂掉,游戏结束。 请大家花点时间,好好理解这个游戏,并且尽可能地改进它。如果你可以不看我们的答案,而自己完成游戏和改进,那么你会收获很多的! 4. 第二部分第十课预告 今天的课就到这里,一起加油吧! 下一课我们就会公布悬挂小人游戏的解题思路和答案咯。 下一课: C语言探索之旅 | 第二部分第十课: 实战"悬挂小人"游戏 答案 >我是 谢恩铭 ,公众号「程序员联盟」(微信号:coderhub)运营者,慕课网精英讲师 Oscar 老师 ,终生学习者。 热爱生活,喜欢游泳,略懂烹饪。 人生格言:「向着标杆直跑」
    来源:OSCHINA
    发布时间:2020-06-17 12:15:00
    (点击图片进入关卡) 食人魔再次来袭,只有更多参数能够救你! 简介 你的函数可以定义不止一个参数。 def maybeBuildTrap(x, y): # 当如下函数被调用时, # x将是43,y将是50 maybeBuildTrap(43, 50) 默认代码 # 函数maybeBuildTrap定义了两个参数! def maybeBuildTrap(x, y): # 使用x和y作为移动的坐标。 hero.moveXY(x, y) enemy = hero.findNearestEnemy() if enemy: pass # 使用 buildXY 在特定 x 和 y 处建造 "fire-trap" while True: # 这会调用maybeBuildTrap,使用下方入口的坐标。 maybeBuildTrap((38, 20) # 下面在右侧入口使用maybeBuildTrap! # 现在在上方入口处使用maybeBuildTrap! ! 概览 就像 moveXY 接收两个参数那样,你创建的函数也可以定义多个参数! def maybeBuildTrap(x, y): # 当函数被调用时, # x 会是 43,y 会是 50 maybeBuildTrap(43, 50) 形参 vs. 实参 为啥有时我们叫它形参,有时叫它实参? 形参 (parameter) 是在函数定义里的参数。 实参 (argument) 是函数被调用时传进的实际参数值! 返回荆棘农场 B 解法 # 函数maybeBuildTrap定义了两个参数! def maybeBuildTrap(x, y): # 使用x和y作为移动的坐标。 hero.moveXY(x, y) enemy = hero.findNearestEnemy() if enemy: # 使用 buildXY 在特定 x 和 y 处建造 "fire-trap" hero.buildXY("fire-trap", x, y) while True: # 这会调用maybeBuildTrap,使用下方入口的坐标。 maybeBuildTrap(38, 20) # 下面在右侧入口使用maybeBuildTrap! maybeBuildTrap(56, 34) # 现在在上方入口处使用maybeBuildTrap! maybeBuildTrap(38, 48) 本攻略发于极客战记官方教学栏目,原文地址为: https://codecombat.163.com/news/jikezhanji-fanhuijingjinongchangb 极客战记——学编程,用玩的
    来源:OSCHINA
    发布时间:2020-06-17 11:26:00
    (点击图片进入关卡) 食人魔再次来袭,只有更多参数能够救你! 简介 你的函数可以定义不止一个参数。 def maybeBuildTrap(x, y): # 当如下函数被调用时, # x将是43,y将是50 maybeBuildTrap(43, 50) 默认代码 # 函数maybeBuildTrap定义了两个参数! def maybeBuildTrap(x, y): # 使用x和y作为移动的坐标。 hero.moveXY(x, y) enemy = hero.findNearestEnemy() if enemy: # 使用 buildXY 在特定 x 和 y 处建造 "fire-trap" pass while True: # 这会调用 maybeBuildTrap,并使用上方入口的坐标。 maybeBuildTrap(43, 50) # 下面在下方入口处使用maybeBuildTrap! # 下面在右侧入口使用maybeBuildTrap! ! 概览 就像 moveXY 接收两个参数那样,你创建的函数也可以定义多个参数! def maybeBuildTrap(x, y): # 当函数被调用时, # x 会是 43,y 会是 50 maybeBuildTrap(43, 50) 形参 vs. 实参 为啥有时我们叫它形参,有时叫它实参? 形参 (parameter) 是在函数定义里的参数。 实参 (argument) 是函数被调用时传进的实际参数值! 返回荆棘农场 A 解法 # 函数maybeBuildTrap定义了两个参数! def maybeBuildTrap(x, y): # 使用x和y作为移动的坐标。 hero.moveXY(x, y) enemy = hero.findNearestEnemy() if enemy: # 使用 buildXY 在特定 x 和 y 处建造 "fire-trap" hero.buildXY("fire-trap", x, y) while True: # 这会调用maybeBuildTrap,使用左侧入口的坐标。 maybeBuildTrap(20, 34) # 下面在下方入口处使用maybeBuildTrap! maybeBuildTrap(38, 20) # 下面在右侧入口使用maybeBuildTrap! ! maybeBuildTrap(56, 34) 本攻略发于极客战记官方教学栏目,原文地址为: https://codecombat.163.com/news/jikezhanji-fanhuijingjinongchanga 极客战记——学编程,用玩的
    来源:OSCHINA
    发布时间:2020-06-17 11:24:00
    本文来自: PerfMa技术社区 PerfMa(笨马网络)官网 最近2周开始接手apache flink全链路监控数据的作业,包括指标统计,业务规则匹配等逻辑,计算结果实时写入elasticsearch. 昨天遇到生产环境有作业无法正常重启的问题,我负责对这个问题进行排查跟进。 第一步,基础排查 首先拿到jobmanager和taskmanager的日志,我从taskmanager日志中很快发现2个基础类型的报错,一个是npe,一个是索引找不到的异常 elasticsearch sinker在执行写入数据的前后提供回调接口让作业开发人员对异常或者成功写入进行处理,如果在处理异常过程中有异常抛出,那么框架会让该task失败,导致作业重启。 npe很容易修复,索引找不到是创建索引的服务中的一个小bug,这些都是小问题。 重点是在日志中我看到另一个错误: java.lang.OutOfMemoryError: unable to create new native thread at java.lang.Thread.start0(Native Method) at java.lang.Thread.start(Unknown Source) at org.apache.flink.runtime.io.network.api.writer.RecordWriter.(RecordWriter.java:122) at org.apache.flink.runtime.io.network.api.writer.RecordWriter.createRecordWriter(RecordWriter.java:321) at org.apache.flink.streaming.runtime.tasks.StreamTask.createRecordWriter(StreamTask.java:1202) at org.apache.flink.streaming.runtime.tasks.StreamTask.createRecordWriters(StreamTask.java:1170) at org.apache.flink.streaming.runtime.tasks.StreamTask.(StreamTask.java:212) at org.apache.flink.streaming.runtime.tasks.StreamTask.(StreamTask.java:190) at org.apache.flink.streaming.runtime.tasks.OneInputStreamTask.(OneInputStreamTask.java:52) at sun.reflect.GeneratedConstructorAccessor4.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source) at java.lang.reflect.Constructor.newInstance(Unknown Source) at org.apache.flink.runtime.taskmanager.Task.loadAndInstantiateInvokable(Task.java:1405) at org.apache.flink.runtime.taskmanager.Task.run(Task.java:689) at java.lang.Thread.run(Unknown Source) 这种异常,一般是nproc设置太小导致的,或者物理内存耗尽,检查完ulimit和内存,发现都很正常,这就比较奇怪了。 第二步、分析jstack和jmap perfma有一个产品叫xland,我也是第一次使用,不得不说,确实牛逼,好用! 首先把出问题的taskmanager的线程栈信息和内存dump出来,具体命令: jstatck pid > 生成的文件名 jmap -dump:format=b,file=生成的文件名 进程号 接着把这两个文件导入xland,xland可以直接看到线程总数,可以方便搜索统计线程数、实例个数等等 最先发现的问题是这个taskmanager 线程总数竟然有17000+,这个数字显然有点大,这个时候我想看一下,哪一种类型的线程比较大,xland可以很方便的搜索,统计,这时候我注意到有一种类型的线程非常多,总数15520 更上层的调用信息看不到了,只看到来自apache http client,根据作业流程,首先想到的就是es sinker的RestHighLevelClient用到这个东西 那么我们在xland中统计RestHighLevelClient对象个数,发现有几百个,很显然这里有问题 第三步、定位具体问题 有了前面xland的帮助,我们很容易定位到是esclient出了问题 在我们的作业里面有2个地方用到了es client,一个是es sinker,es sinker使用的就是RestHighLevelClient,另一个是我们同学自己写的一个es client,同样是使用RestHighLevelClient,在es sinker的ElasticsearchSinkFunction中单独构造,用于在写入es前,先搜索一些东西拿来合并,还做了cache 1、怀疑RestHighLevelClient bug 我们通过一个测试,来验证是不是RestHighLevelClient的问题 启动一个单纯使用es sinker的job,调整并发度,观察前面出现较多的 I/O dispatcher线程的个数,最后发现单个es sinker也会有240+个 I/O dispatcher线程,通过调整并发,所有taskmanager的 I/O dispatcher线程总数基本和并发成正向比例 停掉写es作业,此时所有taskmanager是不存在I/O dispatcher线程的 看起来I/O dispatcher那种线程数量大,似乎是“正常的” 2、杀掉作业,观察线程是否被正常回收 杀掉作业,I/O dispatcher线程变成0了,看起来es sinker使用是正常的 这时候基本上可以判断是我们自己写的es client的问题。到底是什么问题呢? 我们再做一个测试进一步确认 3、启动问题作业,杀死job后,观察I/O dispatcher线程个数 重启flink的所有taskmanager,给一个“纯净”的环境,发现杀死作业后,还有I/O dispatcher线程。 这个测试可以判断是我们的es client存在线程泄漏 四、背后的原理 es sinker本质上是一个RichSinkFunction,RichSinkFunction带了open 和close 方法,在close方法中,es sinker正确关闭了http client @Override public void close() throws Exception { if (bulkProcessor != null) { bulkProcessor.close(); bulkProcessor = null; } if (client != null) { client.close(); client = null; } callBridge.cleanup(); // make sure any errors from callbacks are rethrown checkErrorAndRethrow(); } 而我们的es client是没有被正确关闭的。 具体原理应该是是这样的,当es sinker出现npe或者写es rejected等异常时,job会被flink重启,es sinker这种RichSinkFunction类型的算子会被flink 调用close关闭释放掉一些资源,而我们写在ElasticsearchSinkFunction中es client,是不会被框架关照到的,而这种写法我们自己也无法预先定义重启后关闭client的逻辑. 如果在构造时使用单例,理论上应该是可以避免作业反复重启时es client不断被构造导致线程泄漏和内存泄漏的,但是编写单例写法有问题,虽然有double check,但是没加volatile,同时锁的是this, 而不是类。 五、小结 1、xland确实好用,排查问题帮助很大。 2、flink作业用到的外部客户端不要单独构造,要使用类似RichFunction这种方式,提供open,close方法,确保让资源能够被flink正确释放掉。 3、用到的对象,创建的线程,线程池等等最好都起一个名字,方便使用xland事后排查问题,如果有经验的话,应该一开始就统计下用于构造es client的那个包装类对象个数。 一起来学习吧 : PerfMa KO 系列课之 JVM 参数【Memory篇】 JCU之 FutureTask 源码与工作原理分析
    来源:OSCHINA
    发布时间:2020-06-17 10:46:00