Java异常处理和最佳实践(含案例分析)
一、概述最近在代码CR的时候发现一些值得注意的问题,特别是在对Java异常处理的时候,比如有的同学对每个方法都进行 try-catch,在进行 IO 操作时忘记在 finally 块中关闭连接资源等等问题。回想自己对 java 的异常处理也不是特别清楚,看了一些异常处理的规范,并没有进行系统的学习,所以为了对 Java 异常处理机制有更深入的了解,我查阅了一些资料将自己的学习内容记录下来,希望对有同样困惑的同学提供一些帮助。在Java中处理异常并不是一个简单的事情,不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。在写本文之前,通过查阅相关资料了解如何处理Java异常,首先查看了阿里巴巴Java开发规范,其中有15条关于异常处理的说明,这些说明告诉了我们应该怎么做,但是并没有详细说明为什么这样做,比如为什么推荐使用 try-with-resources 关闭资源 ,为什么 finally 块中不能有 return 语句,这些问题当我们从字节码层面分析时,就可以非常深刻的理解它的本质。通过本文的的学习,你将有如下收获:了解Java异常的分类,什么是检查异常,什么是非检查异常从字节码层面理解Java的异常处理机制,为什么finally块中的代码总是会执行了解Java异常处理的不规范案例了解Java异常处理的最佳实践了解项目中的异常处理,什么时候抛出异常,什么时候捕获异常二、java 异常处理机制1、java 异常分类总结:Thorwable类(表示可抛出)是所有异常和错误的超类,两个直接子类为Error和Exception,分别表示错误和异常。其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常, 这两种异常有很大的区别,也称之为非检查异常(Unchecked Exception)和检查异常(Checked Exception),其中Error类及其子类也是非检查异常。检查异常和非检查异常检查异常:也称为“编译时异常”,编译器在编译期间检查的那些异常。由于编译器“检查”这些异常以确保它们得到处理,因此称为“检查异常”。如果抛出检查异常,那么编译器会报错,需要开发人员手动处理该异常,要么捕获,要么重新抛出。除了RuntimeException之外,所有直接继承 Exception 的异常都是检查异常。非检查异常:也称为“运行时异常”,编译器不会检查运行时异常,在抛出运行时异常时编译器不会报错,当运行程序的时候才可能抛出该异常。Error及其子类和RuntimeException 及其子类都是非检查异常。说明:检查异常和非检查异常是针对编译器而言的,是编译器来检查该异常是否强制开发人员处理该异常检查异常导致异常在方法调用链上显式传递,而且一旦底层接口的检查异常声明发生变化,会导致整个调用链代码更改。使用非检查异常不会影响方法签名,而且调用方可以自由决定何时何地捕获和处理异常建议使用非检查异常让代码更加简洁,而且更容易保持接口的稳定性检查异常举例在代码中使用 throw 关键字手动抛出一个检查异常,编译器提示错误,如下图所示:通过编译器提示,有两种方式处理检查异常,要么将异常添加到方法签名上,要么捕获异常:方式一:将异常添加到方法签名上,通过 throws 关键字抛出异常,由调用该方法的方法处理该异常:方式二:使用 try-catch 捕获异常,在 catch 代码块中处理该异常,下面的代码是将检查异常包装在非检查异常中重新抛出,这样编译器就不会提示错误了,关于如何处理异常后面会详细介绍:非检查异常举例所有继承 RuntimeException 的异常都是非检查异常,直接抛出非检查异常编译器不会提示错误:自定义检查异常自定义检查异常只需要继承 Exception 即可,如下代码所示:自定义检查异常的处理方式前面已经介绍,这里不再赘述。自定义非检查异常自定义非检查异常只需要继承 RuntimeException 即可,如下代码所示:2、从字节码层面分析异常处理前面已经简单介绍了一下Java 的异常体系,以及如何自定义异常,下面我将从字节码层面分析异常处理机制,通过字节码的分析你将对 try-catch-finally 有更加深入的认识。try-catch-finally的本质首先查阅 jvm 官方文档,有如下的描述说明:从官方文档的描述我们可以知道,图片中的字节码是在 JDK 1.6 (class 文件的版本号为50,表示java编译器的版本为jdk 1.6)及之前的编译器生成的,因为有 jsr 和 ret 指令可以使用。然而在 idea 中通过 jclasslib 插件查看 try-catch-finally 的字节码文件并没有 jsr/ret 指令,通过查阅资料,有如下说明:jsr / ret 机制最初用于实现finally块,但是他们认为节省代码大小并不值得额外的复杂性,因此逐渐被淘汰了。Sun JDK 1.6之后的javac就不生成jsr/ret指令了,那finally块要如何实现?javac采用的办法是把finally块的内容复制到原本每个jsr指令所在的地方,这样就不需要jsr/ret了,代价则是字节码大小会膨胀,但是降低了字节码的复杂性,因为减少了两个字节码指令(jsr/ret)。案例一:try-catch 字节码分析在 JDK 1.8 中 try-catch 的字节码如下所示:这里需要说明一下 athrow 指令的作用:异常表athrow指令:在Java程序中显示抛出异常的操作(throw语句)都是由 athrow指令来实现的,athrow 指令抛出的Objectref 必须是类型引用,并且必须作为 Throwable 类或 Throwable 子类的实例对象。它从操作数堆栈中弹出,然后通过在当前方法的异常表中搜索与 objectref 类匹配的第一个异常处理程序:如果在异常表中找到与 objectref 匹配的异常处理程序,PC 寄存器被重置到用于处理此异常的代码的位置,然后会清除当前帧的操作数堆栈,objectref 被推回操作数堆栈,执行继续。如果在当前框架中没有找到匹配的异常处理程序,则弹出该栈帧,该异常会重新抛给上层调用的方法。如果当前帧表示同步方法的调用,那么在调用该方法时输入或重新输入的监视器将退出,就好像执行了监视退出指令(monitorexit)一样。如果在所有栈帧弹出前仍然没有找到合适的异常处理程序,这个线程将终止。异常表:异常表中用来记录程序计数器的位置和异常类型。如上图所示,表示的意思是:如果在 8 到 16 (不包括16)之间的指令抛出的异常匹配 MyCheckedException 类型的异常,那么程序跳转到16 的位置继续执行。分析上图中的字节码:第一个 athrow 指令抛出 MyCheckedException 异常到操作数栈顶,然后去到异常表中查找是否有对应的类型,异常表中有 MyCheckedException ,然后跳转到 16 继续执行代码。第二个 athrow 指令抛出 RuntimeException 异常,然后在异常表中没有找到匹配的类型,当前方法强制结束并弹出当前栈帧,该异常重新抛给调用者,任然没有找到匹配的处理器,该线程被终止。案例二:try-catch-finally 字节码分析在刚刚的代码基础之上添加 finally 代码块,然后分析字节码如下:异常表的信息如下:添加 finally 代码块后,在异常表中新增了一条记录,捕获类型为 any,这里解释一下这条记录的含义:在 8 到 27(不包括27) 之间的指令执行过程中,抛出或者返回任何类型的结果都会跳转到 26 继续执行。从上图的字节码中可以看到,字节码索引为 26 后到结束的指令都是 finally 块中的代码,再解释一下finally块的字节码指令的含义,从 25 开始介绍,finally 块的代码是从 26 开始的:25 athrow // 匹配到异常表中的异常 any,清空操作数栈,将 RuntimeExcepion 的引用添加到操作数栈顶,然后跳转到 26 继续执行26 astore_2 // 将栈顶的引用保存到局部变量表索引为 2 的位置27 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;> // 获取类的静态字段引用放在操作数栈顶30 ldc #9 <执行finally 代码>//将字符串的放在操作数栈顶32 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>// 调用方法35 aload_2// 将局部变量表索引为 2 到引用放到操作数栈顶,这里就是前面抛出的RuntimeExcepion 的引用36 athrow// 在异常表中没有找到对应的异常处理程序,弹出该栈帧,该异常会重新抛给上层调用的方法案例三:finally 块中的代码为什么总是会执行简单分析一下上面代码的字节码指令:字节码指令 2 到 8 会抛出 ArithmeticException 异常,该异常是 Exception 的子类,正好匹配异常表中的第一行记录,然后跳转到 13 继续执行,也就是执行 catch 块中的代码,然后执行 finally 块中的代码,最后通过 goto 31 跳转到 finally 块之外执行后续的代码。如果 try 块中没有抛出异常,则执行完 try 块中的代码然后继续执行 finally 块中的代码,因为编译器在编译的时候将 finally 块中的代码添加到了 try 块代码后面,执行完 finally 的代码后通过 goto 31 跳转到 finally 块之外执行后续的代码 。编译器会将 finally 块中的代码放在 try 块和 catch 块的末尾,所以 finally 块中的代码总是会执行。通过上面的分析,你应该可以知道 finally 块的代码为什么总是会执行了,如果还是有不明白的地方欢迎留言讨论。案例四:finally 块中使用 return 字节码分析public int getInt() {
int i = 0;
try {
i = 1;
return i;
} finally {
i = 2;
return i;
}
}
public int getInt2() {
int i = 0;
try {
i = 1;
return i;
} finally {
i = 2;
}
}先分析一下 getInt() 方法的字节码:局部变量表:异常表:总结:从上面的字节码中我们可以看出,如果finally 块中有 return 关键字,那么 try 块以及 catch 块中的 return 都将会失效,所以在开发的过程中不应该在 finally 块中写 return 语句。先分析一下 getInt2() 方法的字节码:异常表:从上图字节码的分析,我们可以知道,虽然执行了finally块中的代码,但是返回的值还是 1,这是因为在执行finally代码块之前,将原来局部变量表索引为 1 的值 1 保存到了局部变量表索引为 2 的位置,最后返回到是局部变量表索引为 2 的值,也就是原来的 1。总结:如果在 finally 块中没有 return 语句,那么无论在 finally 代码块中是否修改返回值,返回值都不会改变,仍然是执行 finally 代码块之前的值。try-with-resources 的本质下面通过一个打包文件的代码来演示说明一下 try-with-resources 的本质:/**
* 打包多个文件为 zip 格式
*
* @param fileList 文件列表
*/
public static void zipFile(List<File> fileList) {
// 文件的压缩包路径
String zipPath = OUT + "/打包附件.zip";
// 获取文件压缩包输出流
try (OutputStream outputStream = new FileOutputStream(zipPath);
CheckedOutputStream checkedOutputStream = new CheckedOutputStream(outputStream, new Adler32());
ZipOutputStream zipOut = new ZipOutputStream(checkedOutputStream)) {
for (File file : fileList) {
// 获取文件输入流
InputStream fileIn = new FileInputStream(file);
// 使用 common.io中的IOUtils获取文件字节数组
byte[] bytes = IOUtils.toByteArray(fileIn);
// 写入数据并刷新
zipOut.putNextEntry(new ZipEntry(file.getName()));
zipOut.write(bytes, 0, bytes.length);
zipOut.flush();
}
} catch (FileNotFoundException e) {
System.out.println("文件未找到");
} catch (IOException e) {
System.out.println("读取文件异常");
}
}可以看到在 try() 的括号中定义需要关闭的资源,实际上这是Java的一种语法糖,查看编译后的代码就知道编译器为我们做了什么,下面是反编译后的代码:public static void zipFile(List<File> fileList) {
String zipPath = "./打包附件.zip";
try {
OutputStream outputStream = new FileOutputStream(zipPath);
Throwable var3 = null;
try {
CheckedOutputStream checkedOutputStream = new CheckedOutputStream(outputStream, new Adler32());
Throwable var5 = null;
try {
ZipOutputStream zipOut = new ZipOutputStream(checkedOutputStream);
Throwable var7 = null;
try {
Iterator var8 = fileList.iterator();
while(var8.hasNext()) {
File file = (File)var8.next();
InputStream fileIn = new FileInputStream(file);
byte[] bytes = IOUtils.toByteArray(fileIn);
zipOut.putNextEntry(new ZipEntry(file.getName()));
zipOut.write(bytes, 0, bytes.length);
zipOut.flush();
}
} catch (Throwable var60) {
var7 = var60;
throw var60;
} finally {
if (zipOut != null) {
if (var7 != null) {
try {
zipOut.close();
} catch (Throwable var59) {
var7.addSuppressed(var59);
}
} else {
zipOut.close();
}
}
}
} catch (Throwable var62) {
var5 = var62;
throw var62;
} finally {
if (checkedOutputStream != null) {
if (var5 != null) {
try {
checkedOutputStream.close();
} catch (Throwable var58) {
var5.addSuppressed(var58);
}
} else {
checkedOutputStream.close();
}
}
}
} catch (Throwable var64) {
var3 = var64;
throw var64;
} finally {
if (outputStream != null) {
if (var3 != null) {
try {
outputStream.close();
} catch (Throwable var57) {
var3.addSuppressed(var57);
}
} else {
outputStream.close();
}
}
}
} catch (FileNotFoundException var66) {
System.out.println("文件未找到");
} catch (IOException var67) {
System.out.println("读取文件异常");
}
}JDK1.7开始,java引入了 try-with-resources 声明,将 try-catch-finally 简化为 try-catch,在编译时会进行转化为 try-catch-finally 语句,我们就不需要在 finally 块中手动关闭资源。try-with-resources 声明包含三部分:try(声明需要关闭的资源)、try 块、catch 块。它要求在 try-with-resources 声明中定义的变量实现了 AutoCloseable 接口,这样在系统可以自动调用它们的close方法,从而替代了finally中关闭资源的功能,编译器为我们生成的异常处理过程如下:try 块没有发生异常时,自动调用 close 方法,try 块发生异常,然后自动调用 close 方法,如果 close 也发生异常,catch 块只会捕捉 try 块抛出的异常,close 方法的异常会在catch 中通过调用 Throwable.addSuppressed 来压制异常,但是你可以在catch块中,用 Throwable.getSuppressed 方法来获取到压制异常的数组。三、java 异常处理不规范案例异常处理分为三个阶段:捕获->传递->处理。try……catch的作用是捕获异常,throw的作用将异常传递给合适的处理程序。捕获、传递、处理,三个阶段,任何一个阶段处理不当,都会影响到整个系统。下面分别介绍一下常见的异常处理不规范案例。捕获捕获异常的时候不区分异常类型捕获异常不完全,比如该捕获的异常类型没有捕获到try{
……
} catch (Exception e){ // 不应对所有类型的异常统一捕获,应该抽象出业务异常和系统异常,分别捕获
……
}传递异常信息丢失异常信息转译错误,比如在抛出异常的时候将业务异常包装成了系统异常吃掉异常不必要的异常包装检查异常传递过程中不适用非检查检异常包装,造成代码被throws污染try{
……
} catch (BIZException e){
throw new BIZException(e); // 重复包装同样类型的异常信息
} catch (Biz1Exception e){
throw new BIZException(e.getMessage()); // 没有抛出异常栈信息,正确的做法是throw new BIZException(e);
} catch (Biz2Exception e){
throw new Exception(e); // 不能使用低抽象级别的异常去包装高抽象级别的异常,这样在传递过程中丢失了异常类型信息
} catch (Biz3Exception e){
throw new Exception(……); // 异常转译错误,将业务异常直接转译成了系统异常
} catch (Biz4Exception e){
…… // 不抛出也不记Log,直接吃掉异常
} catch (Exception e){
throw e;
}处理重复处理处理方式不统一处理位置分散try{
try{
try{
……
} catch (Biz1Exception e){
log.error(e); // 重复的LOG记录
throw new e;
}
try{
……
} catch (Biz2Exception e){
…… // 同样是业务异常,既在内层处理,又在外层处理
}
} catch (BizException e){
log.error(e); // 重复的LOG记录
throw e;
}
} catch (Exception e){
// 通吃所有类型的异常
log.error(e.getMessage(),e);
}四、java 异常处理规范案例1、阿里巴巴Java异常处理规约阿里巴巴Java开发规范中有15条异常处理的规约,其中下面两条使用的时候是比较困惑的,因为并没有告诉我们应该如何定义异常,如何抛出异常,如何处理异常:【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。【推荐】定义时区分unchecked / checked 异常,避免直接使用RuntimeException抛出,更不允许抛出Exception或者Throwable,应使用有业务含义的自定义异常。后面的章节我将根据自己的思考,说明如何定义异常,如何抛出异常,如何处理异常,接着往下看😀。2、异常处理最佳实践1、使用 try-with-resource 关闭资源。2、抛出具体的异常而不是 Exception,并在注释中使用 @throw 进行说明。3、捕获异常后使用描述性语言记录错误信息,如果是调用外部服务最好是包括入参和出参。 logger.error("说明信息,异常信息:{}", e.getMessage(), e)4、优先捕获具体异常。5、不要捕获 Throwable 异常,除非特殊情况。6、不要忽略异常,异常捕获一定需要处理。7、不要同时记录和抛出异常,因为异常会打印多次,正确的处理方式要么抛出异常要么记录异常,如果抛出异常,不要原封不动的抛出,可以自定义异常抛出。8、自定义异常不要丢弃原有异常,应该将原始异常传入自定义异常中。throw MyException("my exception", e);9、自定义异常尽量不要使用检查异常。10、尽可能晚的捕获异常,如非必要,建议所有的异常都不要在下层捕获,而应该由最上层捕获并统一处理这些异常。。11、为了避免重复输出异常日志,建议所有的异常日志都统一交由最上层输出。就算下层捕获到了某个异常,如非特殊情况,也不要将异常信息输出,应该交给最上层统一输出日志。五、项目中的异常处理实践1、如何自定义异常在介绍如何自定义异常之前,有必要说明一下使用异常的好处,参考Java异常的官方文档,总结有如下好处:能够将错误代码和正常代码分离能够在调用堆栈上传递异常能够将异常分组和区分在Java异常体系中定义了很多的异常,这些异常通常都是技术层面的异常,对于应用程序来说更多出现的是业务相关的异常,比如用户输入了一些不合法的参数,用户没有登录等,我们可以通过异常来对不同的业务问题进行分类,以便我们排查问题,所以需要自定义异常。那我们如何自定义异常呢?前面已经说了,在应用程序中尽量不要定义检查异常,应该定义非检查异常(运行时异常)。在我看来,应用程序中定义的异常应该分为两类:业务异常:用户能够看懂并且能够处理的异常,比如用户没有登录,提示用户登录即可。系统异常:用户看不懂需要程序员处理的异常,比如网络连接超时,需要程序员排查相关问题。下面是我设想的对于应用程序中的异常体系分类:在真实项目中,我们通常在遇到不符合预期的情况下,通过抛出异常来阻止程序继续运行,在抛出对应的异常时,需要在异常对象中描述抛出该异常的原因以及异常堆栈信息,以便提示用户和开发人员如何处理该异常。一般来说,异常的定义我们可以参考Java的其他异常定义就可以了,比如异常中有哪些构造方法,方法中有哪些构造参数,但是这样的自定义异常只是通过异常的类名对异常进行了一个分类,对于异常的描述信息还是不够完善,因为异常的描述信息只是一个字符串。我觉得异常的描述信息还应该包含一个错误码(code),异常中包含错误码的好处是什么呢?我能想到的就是和http请求中的状态码的优点差不多,还有一点就是能够方便提供翻译功能,对于不同的语言环境能够通过错误码找到对应语言的错误提示信息而不需要修改代码。基于上述的说明,我认为应该这样来定义异常类,需要定义一个描述异常信息的枚举类,对于一些通用的异常信息可以在枚举中定义,如下所示:/**
* 异常信息枚举类
*
*/
public enum ErrorCode {
/**
* 系统异常
*/
SYSTEM_ERROR("A000", "系统异常"),
/**
* 业务异常
*/
BIZ_ERROR("B000", "业务异常"),
/**
* 没有权限
*/
NO_PERMISSION("B001", "没有权限"),
;
/**
* 错误码
*/
private String code;
/**
* 错误信息
*/
private String message;
ErrorCode(String code, String message) {
this.code = code;
this.message = message;
}
/**
* 获取错误码
*
* @return 错误码
*/
public String getCode() {
return code;
}
/**
* 获取错误信息
*
* @return 错误信息
*/
public String getMessage() {
return message;
}
/**
* 设置错误码
*
* @param code 错误码
* @return 返回当前枚举
*/
public ErrorCode setCode(String code) {
this.code = code;
return this;
}
/**
* 设置错误信息
*
* @param message 错误信息
* @return 返回当前枚举
*/
public ErrorCode setMessage(String message) {
this.message = message;
return this;
}
}自定义系统异常类,其他类型的异常类似,只是异常的类名不同,如下代码所示:/**
* 系统异常类
*
*/
public class SystemException extends RuntimeException {
private static final long serialVersionUID = 8312907182931723379L;
/**
* 错误码
*/
private String code;
/**
* 构造一个没有错误信息的 <code>SystemException</code>
*/
public SystemException() {
super();
}
/**
* 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 SystemException
*
* @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息
*/
public SystemException(Throwable cause) {
super(cause);
}
/**
* 使用错误信息 message 构造 SystemException
*
* @param message 错误信息
*/
public SystemException(String message) {
super(message);
}
/**
* 使用错误码和错误信息构造 SystemException
*
* @param code 错误码
* @param message 错误信息
*/
public SystemException(String code, String message) {
super(message);
this.code = code;
}
/**
* 使用错误信息和 Throwable 构造 SystemException
*
* @param message 错误信息
* @param cause 错误原因
*/
public SystemException(String message, Throwable cause) {
super(message, cause);
}
/**
* @param code 错误码
* @param message 错误信息
* @param cause 错误原因
*/
public SystemException(String code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
/**
* @param errorCode ErrorCode
*/
public SystemException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
/**
* @param errorCode ErrorCode
* @param cause 错误原因
*/
public SystemException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.code = errorCode.getCode();
}
/**
* 获取错误码
*
* @return 错误码
*/
public String getCode() {
return code;
}
}上面定义的 SystemException 类中定义了很多的构造方法,我这里只是给出一个示例,所以保留了不传入错误码的构造方法,保留不使用错误码的构造方法,可以提高代码的灵活性,因为错误码的规范也是一个值得讨论的问题,关于如何定义错误码在阿里巴巴开发规范手册中有介绍,这里不再详细说明。2、如何使用异常前面介绍了如何自定义异常,接下来介绍一下如何使用异常,也就是什么时候抛出异常。异常其实可以看作方法的返回结果,当出现非预期的情况时,就可以通过抛出异常来阻止程序继续执行。比如期望用户有管理员权限才能删除某条记录,如果用户没有管理员权限,那么就可以抛出没有权限的异常阻止程序继续执行并提示用户需要管理员权限才能操作。抛出异常使用 throw 关键字,如下所示:throw new BizException(ErrorCode.NO_PERMISSION);什么时候抛出业务异常,什么时候抛出系统异常?业务异常(bizException/bussessException): 用户操作业务时,提示出来的异常信息,这些信息能直接让用户可以继续下一步操作,或者换一个正确操作方式去使用,换句话就是用户可以自己能解决的。比如:“用户没有登录”,“没有权限操作”。系统异常(SystemException): 用户操作业务时,提示系统程序的异常信息,这类的异常信息时用户看不懂的,需要告警通知程序员排查对应的问题,如 NullPointerException,IndexOfException。另一个情况就是接口对接时,参数的校验时提示出来的信息,如:缺少ID,缺少必须的参数等,这类的信息对于客户来说也是看不懂的,也是解决不了的,所以我把这两类的错误应当统一归类于系统异常关于应该抛出业务异常还是系统异常,一句话总结就是:该异常用户能否处理,如果用户能处理则抛出业务异常,如果用户不能处理需要程序员处理则抛出系统异常。在调用第三方的 rpc 接口时,我们应该如何处理异常呢?首先我们需要知道 rpc 接口抛出异常还是返回的包含错误码的 Result 对象,关于 rpc 应该返回异常还是错误码有很多的讨论,关于这方面的内容可以查看相关文档,这个不是本文的重点,通过实际观察知道 rpc 的返回基本都是包含错误码的 Result 对象,所以这里以返回错误码的情况进行说明。首先需要明确 rpc 调用失败应该返回系统异常,所以我们可以定义一个继承 SystemException 的 rpc 异常 RpcException,代码如下所示:/**
* rpc 异常类
*/
public class RpcException extends SystemException {
private static final long serialVersionUID = -9152774952913597366L;
/**
* 构造一个没有错误信息的 <code>RpcException</code>
*/
public RpcException() {
super();
}
/**
* 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 RpcException
*
* @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息
*/
public RpcException(Throwable cause) {
super(cause);
}
/**
* 使用错误信息 message 构造 RpcException
*
* @param message 错误信息
*/
public RpcException(String message) {
super(message);
}
/**
* 使用错误码和错误信息构造 RpcException
*
* @param code 错误码
* @param message 错误信息
*/
public RpcException(String code, String message) {
super(code, message);
}
/**
* 使用错误信息和 Throwable 构造 RpcException
*
* @param message 错误信息
* @param cause 错误原因
*/
public RpcException(String message, Throwable cause) {
super(message, cause);
}
/**
* @param code 错误码
* @param message 错误信息
* @param cause 错误原因
*/
public RpcException(String code, String message, Throwable cause) {
super(code, message, cause);
}
/**
* @param errorCode ErrorCode
*/
public RpcException(ErrorCode errorCode) {
super(errorCode);
}
/**
* @param errorCode ErrorCode
* @param cause 错误原因
*/
public RpcException(ErrorCode errorCode, Throwable cause) {
super(errorCode, cause);
}
}这个 RpcException 所有的构造方法都是调用的父类 SystemExcepion 的方法,所以这里不再赘述。定义好了异常后接下来是处理 rpc 调用的异常处理逻辑,调用 rpc 服务可能会发生 ConnectException 等网络异常,我们并不需要在调用的时候捕获异常,而是应该在最上层捕获并处理异常,调用 rpc 的处理demo代码如下:private Object callRpc() {
Result<Object> rpc = rpcDemo.rpc();
log.info("调用第三方rpc返回结果为:{}", rpc);
if (Objects.isNull(rpc)) {
return null;
}
if (!rpc.getSuccess()) {
throw new RpcException(ErrorCode.RPC_ERROR.setMessage(rpc.getMessage()));
}
return rpc.getData();
}3、如何处理异常我们应该尽可能晚的捕获异常,如非必要,建议所有的异常都不要在下层捕获,而应该由最上层捕获并统一处理这些异常。前面的已经简单说明了一下如何处理异常,接下来将通过代码的方式讲解如何处理异常。rpc 接口全局异常处理对于 rpc 接口,我们这里将 rpc 接口的返回结果封装到包含错误码的 Result 对象中,所以可以定义一个 aop 叫做 RpcGlobalExceptionAop,在 rpc 接口执行前后捕获异常,并将捕获的异常信息封装到 Result 对象中返回给调用者。Result 对象的定义如下:/**
* Result 结果类
*
*/
public class Result<T> implements Serializable {
private static final long serialVersionUID = -1525914055479353120L;
/**
* 错误码
*/
private final String code;
/**
* 提示信息
*/
private final String message;
/**
* 返回数据
*/
private final T data;
/**
* 是否成功
*/
private final Boolean success;
/**
* 构造方法
*
* @param code 错误码
* @param message 提示信息
* @param data 返回的数据
* @param success 是否成功
*/
public Result(String code, String message, T data, Boolean success) {
this.code = code;
this.message = message;
this.data = data;
this.success = success;
}
/**
* 创建 Result 对象
*
* @param code 错误码
* @param message 提示信息
* @param data 返回的数据
* @param success 是否成功
*/
public static <T> Result<T> of(String code, String message, T data, Boolean success) {
return new Result<>(code, message, data, success);
}
/**
* 成功,没有返回数据
*
* @param <T> 范型参数
* @return Result
*/
public static <T> Result<T> success() {
return of("00000", "成功", null, true);
}
/**
* 成功,有返回数据
*
* @param data 返回数据
* @param <T> 范型参数
* @return Result
*/
public static <T> Result<T> success(T data) {
return of("00000", "成功", data, true);
}
/**
* 失败,有错误信息
*
* @param message 错误信息
* @param <T> 范型参数
* @return Result
*/
public static <T> Result<T> fail(String message) {
return of("10000", message, null, false);
}
/**
* 失败,有错误码和错误信息
*
* @param code 错误码
* @param message 错误信息
* @param <T> 范型参数
* @return Result
*/
public static <T> Result<T> fail(String code, String message) {
return of(code, message, null, false);
}
/**
* 获取错误码
*
* @return 错误码
*/
public String getCode() {
return code;
}
/**
* 获取提示信息
*
* @return 提示信息
*/
public String getMessage() {
return message;
}
/**
* 获取数据
*
* @return 返回的数据
*/
public T getData() {
return data;
}
/**
* 获取是否成功
*
* @return 是否成功
*/
public Boolean getSuccess() {
return success;
}
}在编写 aop 代码之前需要先导入 spring-boot-starter-aop 依赖:<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>RpcGlobalExceptionAop 代码如下:/**
* rpc 调用全局异常处理 aop 类
*
*/
@Slf4j
@Aspect
@Component
public class RpcGlobalExceptionAop {
/**
* execution(* com.xyz.service ..*.*(..)):表示 rpc 接口实现类包中的所有方法
*/
@Pointcut("execution(* com.xyz.service ..*.*(..))")
public void pointcut() {}
@Around(value = "pointcut()")
public Object handleException(ProceedingJoinPoint joinPoint) {
try {
//如果对传入对参数有修改,那么需要调用joinPoint.proceed(Object[] args)
//这里没有修改参数,则调用joinPoint.proceed()方法即可
return joinPoint.proceed();
} catch (BizException e) {
// 对于业务异常,应该记录 warn 日志即可,避免无效告警
log.warn("全局捕获业务异常", e);
return Result.fail(e.getCode(), e.getMessage());
} catch (RpcException e) {
log.error("全局捕获第三方rpc调用异常", e);
return Result.fail(e.getCode(), e.getMessage());
} catch (SystemException e) {
log.error("全局捕获系统异常", e);
return Result.fail(e.getCode(), e.getMessage());
} catch (Throwable e) {
log.error("全局捕获未知异常", e);
return Result.fail(e.getMessage());
}
}
}aop 中 @Pointcut 的 execution 表达式配置说明:execution(public * *(..)) 定义任意公共方法的执行
execution(* set*(..)) 定义任何一个以"set"开始的方法的执行
execution(* com.xyz.service.AccountService.*(..)) 定义AccountService 接口的任意方法的执行
execution(* com.xyz.service.*.*(..)) 定义在service包里的任意方法的执行
execution(* com.xyz.service ..*.*(..)) 定义在service包和所有子包里的任意类的任意方法的执行
execution(* com.test.spring.aop.pointcutexp…JoinPointObjP2.*(…)) 定义在pointcutexp包和所有子包里的JoinPointObjP2类的任意方法的执行http 接口全局异常处理如果是 springboot 项目,http 接口的异常处理主要分为三类:基于请求转发的方式处理异常;基于异常处理器的方式处理异常;基于过滤器的方式处理异常。基于请求转发的方式:真正的全局异常处理。实现方式有:BasicExceptionController基于异常处理器的方式:不是真正的全局异常处理,因为它处理不了过滤器等抛出的异常。实现方式有:@ExceptionHandler@ControllerAdvice+@ExceptionHandlerSimpleMappingExceptionResolverHandlerExceptionResolver基于过滤器的方式:近似全局异常处理。它能处理过滤器及之后的环节抛出的异常。实现方式有:Filter关于 http 接口的全局异常处理,这里重点介绍基于异常处理器的方式,其余的方式建议查阅相关文档学习。在使用基于异常处理器的方式之前需要导入 spring-boot-starter-web 依赖即可,如下所示:<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>通过 @ControllerAdvice+@ExceptionHandler 实现基于异常处理器的http接口全局异常处理:/**
* http 接口异常处理类
*/
@Slf4j
@RestControllerAdvice("org.example.controller")
public class HttpExceptionHandler {
/**
* 处理业务异常
* @param request 请求参数
* @param e 异常
* @return Result
*/
@ExceptionHandler(value = BizException.class)
public Object bizExceptionHandler(HttpServletRequest request, BizException e) {
log.warn("业务异常:" + e.getMessage() , e);
return Result.fail(e.getCode(), e.getMessage());
}
/**
* 处理系统异常
* @param request 请求参数
* @param e 异常
* @return Result
*/
@ExceptionHandler(value = SystemException.class)
public Object systemExceptionHandler(HttpServletRequest request, SystemException e) {
log.error("系统异常:" + e.getMessage() , e);
return Result.fail(e.getCode(), e.getMessage());
}
/**
* 处理未知异常
* @param request 请求参数
* @param e 异常
* @return Result
*/
@ExceptionHandler(value = Throwable.class)
public Object unknownExceptionHandler(HttpServletRequest request, Throwable e) {
log.error("未知异常:" + e.getMessage() , e);
return Result.fail(e.getMessage());
}
}在 HttpExceptionHandler 类中,@RestControllerAdvice = @ControllerAdvice + @ResponseBody ,如果有其他的异常需要处理,只需要定义@ExceptionHandler注解的方法处理即可。六、总结读完本文应该了解Java异常处理机制,当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法帧)。如果在所有帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。最后对本文的内容做一个简单的总结,Java语言的异常处理方式有两种,一种是 try-catch 捕获异常,另一种是通过 throw 抛出异常。在程序中可以抛出两种类型的异常,一种是检查异常,另一种是非检查异常,应该尽量抛出非检查异常,遇到检查异常应该捕获进行处理不要抛给上层。在异常处理的时候应该尽可能晚的处理异常,最好是定义一个全局异常处理器,在全局异常处理器中处理所有抛出的异常,并将异常信息封装到 Result 对象中返回给调用者。参考文档:http://javainsimpleway.com/exception-handling-best-practices/https://www.infoq.com/presentations/effective-api-design/https://docs.oracle.com/javase/tutorial/essential/exceptions/advantages.htmljava 官方文档
Nexmark Benchmark
What is NexmarkNexmark is a benchmark suite for queries over continuous data streams. This project is inspired by the NEXMark research paper and Apache Beam Nexmark.Nexmark Benchmark SuiteSchemasThese are multiple queries over a three entities model representing on online auction system:Person represents a person submitting an item for auction and/or making a bid on an auction.Auction represents an item under auction.Bid represents a bid for an item under auction.QueriesQueryNameSummaryFlinkq0Pass ThroughMeasures the monitoring overhead including the source generator.✅q1Currency ConversionConvert each bid value from dollars to euros.✅q2SelectionFind bids with specific auction ids and show their bid price.✅q3Local Item SuggestionWho is selling in OR, ID or CA in category 10, and for what auction ids?✅q4Average Price for a CategorySelect the average of the wining bid prices for all auctions in each category.✅q5Hot ItemsWhich auctions have seen the most bids in the last period?✅q6Average Selling Price by SellerWhat is the average selling price per seller for their last 10 closed auctions.FLINK-19059q7Highest BidSelect the bids with the highest bid price in the last period.✅q8Monitor New UsersSelect people who have entered the system and created auctions in the last period.✅q9Winning BidsFind the winning bid for each auction.✅q10Log to File SystemLog all events to file system. Illustrates windows streaming data into partitioned file system.✅q11User SessionsHow many bids did a user make in each session they were active? Illustrates session windows.✅q12Processing Time WindowsHow many bids does a user make within a fixed processing time limit? Illustrates working in processing time window.✅q13Bounded Side Input JoinJoins a stream to a bounded side input, modeling basic stream enrichment.✅q14CalculationConvert bid timestamp into types and find bids with specific price. Illustrates more complex projection and filter.✅q15Bidding Statistics ReportHow many distinct users join the bidding for different level of price? Illustrates multiple distinct aggregations with filters.✅q16Channel Statistics ReportHow many distinct users join the bidding for different level of price for a channel? Illustrates multiple distinct aggregations with filters for multiple keys.✅q17Auction Statistics ReportHow many bids on an auction made a day and what is the price? Illustrates an unbounded group aggregation.✅q18Find last bidWhat's a's last bid for bidder to auction? Illustrates a Deduplicate query.✅q19Auction TOP-10 PriceWhat's the top price 10 bids of an auction? Illustrates a TOP-N query.✅q20Expand bid with auctionGet bids with the corresponding auction information where category is 10. Illustrates a filter join.✅q21Add channel idAdd a channel_id column to the bid table. Illustrates a 'CASE WHEN' + 'REGEXP_EXTRACT' SQL.✅q22Get URL DirectoriesWhat is the directory structure of the URL? Illustrates a SPLIT_INDEX SQL.✅Note: q1 ~ q8 are from original NEXMark queries, q0 and q9 ~ q13 are from Apache Beam, others are extended to cover more scenarios.MetricsFor evaluating the performance, there are two performance measurement terms used in Nexmark that are cores and time.Cores is the CPU usage used by the stream processing system. Usually CPU allows preemption, not like memory can be limited. Therefore, how the stream processing system effectively use CPU resources, how much throughput is contributed per core, they are important aspect for streaming performance benchmark. For Flink, we deploy a CPU usage collector on every worker node and send the usage metric to the benchmark runner for summarizing. We don't use the Status.JVM.CPU.Load metric provided by Flink, because it is not accurate.Time is the cost time for specified number of events executed by the stream processing system. With Cores * Time, we can know how many resources the stream processing system uses to process specified number of events.Nexmark Benchmark GuidelineRequirementsThe Nexmark benchmark framework runs Flink queries on standalone cluster, see the Flink documentation for more detailed requirements and how to setup it.Software RequirementsThe cluster should consist of one master node and one or more worker nodes. All of them should be Linux environment (the CPU monitor script requries to run on Linux). Please make sure you have the following software installed on each node:JDK 1.8.x or higher (Nexmark scripts uses some tools of JDK),ssh (sshd must be running to use the Flink and Nexmark scripts that manage remote components)If your cluster does not fulfill these software requirements you will need to install/upgrade it.Having passwordless SSH and the same directory structure on all your cluster nodes will allow you to use our scripts to control everything.Environment VariablesThe following environment variable should be set on every node for the Flink and Nexmark scripts.JAVA_HOME: point to the directory of your JDK installation.FLINK_HOME: point to the directory of your Flink installation.Build NexmarkBefore start to run the benchmark, you should build the Nexmark benchmark first to have a benchmark package. Please make sure you have installed maven in your build machine. And run the ./build.sh command under nexmark-flink directoy. Then you will get the nexmark-flink.tgz archive under the directory.Setup ClusterStep 1: Download the latest Flink package from the download page. Say flink-<version>-bin-scala_2.11.tgz.Step2: Copy the archives (flink-<version>-bin-scala_2.11.tgz, nexmark-flink.tgz) to your master node and extract it.tar xzf flink-<version>-bin-scala_2.11.tgz; tar xzf nexmark-flink.tgz
mv flink-<version> flink; mv nexmark-flink nexmarkStep3: Copy the jars under nexmark/lib to flink/lib which contains the Nexmark source generator.Step4: Configure Flink.Edit flink/conf/workers and enters the IP address of each worker node. Recommand to set 8 entries.Replace flink/conf/sql-client-defaults.yaml by nexmark/conf/sql-client-defaults.yamlReplaceflink/conf/flink-conf.yamlbynexmark/conf/flink-conf.yaml. Remember to update the following configurations:Set jobmanager.rpc.address to you master IP addressSet state.checkpoints.dir to your local file path (recommend to use SSD), e.g. file:///home/username/checkpoint.Set state.backend.rocksdb.localdir to your local file path (recommend to use SSD), e.g. /home/username/rocksdb.Step5: Configure Nexmark benchmark.Set nexmark.metric.reporter.host to your master IP address.Step6: Copy flink and nexmark to your worker nodes using scp.Step7: Start Flink Cluster by running flink/bin/start-cluster.sh on the master node.Step8: Setup the benchmark cluster by running nexmark/bin/setup_cluster.sh on the master node.(If you want to use kafka source instead of datagen source) Step9: Prepare Kafka(Please make sure Flink Kafka Jar is ready in flink/lib/ download page)Start your kafka cluster. (recommend to use SSD)Create kafka topic: bin/kafka-topics.sh --create --topic nexmark --bootstrap-server localhost:9092 --partitions 8.Edit nexmark/conf/nexmark.yaml, set kafka.bootstrap.servers.Prepare source data: nexmark/bin/run_query.sh insert_kafka.NOTE: Kafka source is endless, only supports tps mode (unlimited events.num) now.Run NexmarkYou can run the Nexmark benchmark by running nexmark/bin/run_query.sh all on the master node. It will run all the queries one by one, and collect benchmark metrics automatically. It will take 50 minutes to finish the benchmark by default. At last, it will print the benchmark summary result (Cores * Time(s) for each query) on the console.You can also run specific queries by running nexmark/bin/run_query.sh q1,q2.You can also tune the workload of the queries by editing nexmark/conf/nexmark.yaml with the nexmark.workload.* prefix options.Nexmark Benchmark ResultMachinesMinimum requirements:3 worker nodeEach machine has 8 cores and 32 GB RAM800 GB SSD local diskFlink ConfigurationUse the default configuration file flink-conf.yaml and sql-client-defaults.yaml defined in nexmark-flink/src/main/resources/conf/.Some notable configurations including:8 TaskManagers, each has only 1 slot8 GB for each TaskManager and JobManagerJob parallelism: 8Checkpoint enabled with exactly once mode and 3 minutes intervalUse RocksDB state backend with incremental checkpoint enabledMiniBatch optimization enabled with 2 seconds interval and 5000 rowsSplitting distinct aggregation optimization is enabledFlink version: 1.13.WorkloadsSource total events number is 100 million. Source generates 10M records per seconds. The percentage of 3 stream is Bid: 92%, Auction: 6%, Person: 2%.Benchmark Results+-------------------+-------------------+-------------------+-------------------+-------------------+-------------------+
| Nexmark Query | Events Num | Cores | Time(s) | Cores * Time(s) | Throughput/Cores |
+-------------------+-------------------+-------------------+-------------------+-------------------+-------------------+
|q0 |100,000,000 |8.45 |76.323 |645.087 |155.02 K/s |
|q1 |100,000,000 |8.26 |76.643 |633.165 |157.94 K/s |
|q2 |100,000,000 |8.23 |69.309 |570.736 |175.21 K/s |
|q3 |100,000,000 |8.59 |76.531 |657.384 |152.12 K/s |
|q4 |100,000,000 |12.85 |226.605 |2912.841 |34.33 K/s |
|q5 |100,000,000 |10.8 |418.242 |4516.930 |22.14 K/s |
|q7 |100,000,000 |14.21 |570.983 |8112.884 |12.33 K/s |
|q8 |100,000,000 |9.42 |72.673 |684.288 |146.14 K/s |
|q9 |100,000,000 |16.11 |435.882 |7022.197 |14.24 K/s |
|q10 |100,000,000 |8.09 |213.795 |1729.775 |57.81 K/s |
|q11 |100,000,000 |10.6 |237.599 |2518.946 |39.7 K/s |
|q12 |100,000,000 |13.69 |96.559 |1321.536 |75.67 K/s |
|q13 |100,000,000 |8.24 |92.839 |764.952 |130.73 K/s |
|q14 |100,000,000 |8.28 |74.861 |620.220 |161.23 K/s |
|q15 |100,000,000 |8.73 |158.224 |1380.927 |72.42 K/s |
|q16 |100,000,000 |11.51 |466.008 |5362.602 |18.65 K/s |
|q17 |100,000,000 |9.24 |92.666 |856.162 |116.8 K/s |
|q18 |100,000,000 |12.49 |149.076 |1862.171 |53.7 K/s |
|q19 |100,000,000 |21.38 |106.190 |2270.551 |44.04 K/s |
|q20 |100,000,000 |17.27 |305.099 |5267.805 |18.98 K/s |
|q21 |100,000,000 |8.33 |121.845 |1015.293 |98.49 K/s |
|q22 |100,000,000 |8.25 |93.244 |769.471 |129.96 K/s |
|Total |2,200,000,000 |243.029 |4231.196 |51495.920 |1.89 M/s |
+-------------------+-------------------+-------------------+-------------------+-------------------+-------------------+RoadmapRun Nexmark benchmark for more stream processing systems, such as Spark, KSQL. However, they don't have complete streaming SQL features. Therefore, not all of the queries can be ran in these systems. But we can implement the queries in programing way using Spark Streaming, Kafka Streams.Support Latency metric for the benchmark. Latency measures the required time from a record entering the system to some results produced after some actions performed on the record. However, this is not easy to support for SQL queries unless we modify the queries.ReferencesPete Tucker, Kristin Tufte, Vassilis Papadimos, David Maier. NEXMark – A Benchmark for Queries over Data Streams. June 2010.
如何高效搭建资产管理平台?众安科技告诉你答案是图技术
本⽂整理⾃ NebulaGraph x 阿⾥云计算巢专场中众安保险的⼤数据应⽤⾼级专家曾⼒带来的《众安资产在 NebulaGraph 的应⽤实践》分享,视频⻅链接。⼤家好,我是众安数据科学应⽤中⼼的曾⼒,今天很⾼兴在这⾥可以跟⼤家分享 NebulaGraph 在众安资产的实践。01 基于事件的数据资产平台设计在了解这⼀切之前,我们需要先知道什么是资产管理平台以及它可以解决什么样的问题。资产管理平台是全域的元数据中⼼,它可以对数据资产进行管理监控,解决企业内部的数据孤岛问题,挖掘数据价值并对业务赋能。它主要解决我们数据找不到、数据从哪⼉取,排查路径⻓、数据复⽤率低这四个非常核⼼的关键问题。设计目标对于资产管理平台,我们有三个⾮常重要的设计⽬标——强扩展:是指实体关系定义、资产操作以及资产查询的扩展性。低耦合:是指资产平台与其他系统对接时,对接入系统业务流程零影响。高时效:是指需要近实时的数据采集、快速的数据处理和查询性能。核心功能数据资产管理平台核⼼功能包括以下三个:类型定义:需提供⼀个抽象的设计定义不同的实体/关系,以及它们包含的属性。每个定义的实体/关系均需要定义唯一性约束,用于数据判重。在此基础上我们可以扩展一些定义类型,比如标签、术语、标签传播等等。元数据采集:主要有通过周期性、流式和手工录入三种方式进行数据采集。元数据管理:数据存储常见的选型是关系型数库存储定义或数据,搜索引擎存储数据、变动记录、统计类信息,图数据库则负责关系查询。数据分析常见的场景是数据地图、血缘及影响性分析、全链路血缘分析。数据应用则是在相关数据采集到平台后,可以快速实现资产割接、数据安全管理以及数据治理等更高层次应用需求。类型定义开源系统 Apache Atlas借鉴于开源系统 Apache Atlas 和 DataHub,我们来初步了解类型定义设计的核心要素。Atlas 的类型定义模式是一套基于 JSON 的 TypeSystem,可以自定义扩展,它的核心概念是实体、关系和属性,并在此基础上扩展出分类、术语、业务数据等定义设计。DataHub 则采用 Avro 进行事件模型的定义、PEGASUS 建模语言进行实体、关系和属性的建模,值得一提的是 Aspect 这个概念,其描述实体特定方面的属性集合,同一实体关联的的多个 Aspect 可以独立更新,相同的 Aspect 也可以再多个实体间共享。DataHub 预置了一些实体和关系模型,我们可以复用这些模式或自定义新模型。通过两个开源系统的类型定义设计,我们不难看出实体、关系、属性是元数据系统当中最基础的三个核心类型定义的元素。基于整体的架构、内部数据模型场景、数据存储选型、学习成本等方面因素的考虑,众安数据资产平台的类型定义是参照 Apache Atlas 的 TypeSystem 设计,定义一套独立的类型定义系统。实体类型定义 EntityDef 的核心要素是类型名称、父类型名称和属性列表。对于类型名称,需要单租户下约束唯一;对于父类型名称,其实就是对一些公共属性集的复用,类似于 Java 类的继承机制,我们可以通过获取父类型及其超类的所有属性。目前为方便类型解析,一个实体仅能定义一个父类型。对于属性列表,一个实体可以有 1~n 个属性,且至少有一个唯一性属性。关系定义 RelationshipDef 的核心要素是定义名称、关系类别、起始/结束端点定义和属性定义;对于类型名称,需要单租户下约束唯一;对于关系类别,根据是否容器关系和端点实体生命周期分为三类。Association 关系:是一种非容器关系,比较典型的例子是调度作业的依赖关系,两者之间不为包含关系,且生命周期独立。Aggregation 关系:是一种容器关系,但端点实体的生命周期独立,比如我们的报表系统,数据模型和画布关系,画布包含模型,但模型可以独立于画布而出存在,生命周期独立。Composition 关系:是一种容器关系,且端点生命周期完全一致,最直观的例子是表和列之间的包含关系,删除表的时候列实体自动被删除。对于端点定义 RelationshipEndDef,端点即是实体关系中关系实体的映射,所以需要定义来源和目标两个端点。每个端点定义需要端点的实体类型名称以及是否为容器。如果关系类别是⼀个容器类型的关系的话,需要设置某⼀个端点容器标志为 true,此时边方向是子项实体指向容器实体。如果关系类别是非容器的关系的话,所有的端点容器标志都需要设置为 false,此时边方向是端点 1 实体指向端点 2 实体。对于属性列表来,一个关系可以有 0~n 个属性。同实体属性定义不同的是,关系定义可以不配置属性定义。属性定义 AttributeDef 核心要素是名称、类型、是否可选、是否唯一属性、是否创建索引、默认值等内容。对于属性类型,因 NebulaGraph 图库支持的类型有限,仅支持基础数据类型。是否支持索引创建,是指创 Nebula tag/edge schema 的时候,对于某个属性是否创建一个 tag/edge 索引,以支持在特殊查询场景下的数据查询。实体的判重是资产平台类型定义的关键设计,我们首先看看开源产品的设计理念。Atlas 类型定义系统当中,所有实体都继承于⼀个⽗实体 Referenceable,它只有⼀个唯一属性 QualifiedName,且被标记为了唯⼀的属性。所有继承于它的实体类型属性中均没有唯一属性。QualifiedName 没有用固定格式,在 Atlas 内置的几个 Hook 中,主要格式为 xxx@meta-namespace。在 Hook 写入时指定,上图的例子就定义的是某个集群、某个存储卷在的唯一性标识。DataHub 实体唯一性标志是 URN,也叫作唯⼀属性资源名称。它有一定的生成规则,即 urn:<namspace>:<Entity Type>:<ID> 命名空间默认设置为 li,类别则是实体定义名称,ID 是指唯一属性集合拼接,可以嵌套 URN,上图的例子一个数据集,代表某个 Kafka 集群下的 Topic。基于两个开源项目分析,不难看出唯一性判断均是基于唯一属性来处理,两者均是在 Ingest 扩展中进行了固定格式的定义写入,而不是基于实体定义中多个明确代表唯一属性进行灵活的拼接处理,其拼接的字段晦涩难以解析。众安设计了一套唯一性判断定义方式,即某个实体注册时,先判断实体定义是否有 Composition 类别关系的边定义引用。如果不存在该关系类别定义,则直接从实体定义的属性定义中检索 isUnique=true 的属性。如果存在改关系类别定义,那当前实体的唯一性属性将不足以约束其唯一性,还需要带上边定义的容器实体的唯一属性才可以保证。这是一个递归的过程,可能需要传入多个实体的唯一性属性才可以判断。比如注册一个 MySQL 表,除了表实体的表名称之外,还需要 MySQL 库实体的 Host、端口、数据库名称等唯一属性才是完整的为唯一属性列表。在获取了唯一属性列表后,还需要加上租户和类型定义名称,继而生成某一租户下对应的唯一实体标志。操作需要三个流程,首先需要把唯⼀性的属性列表,根据其对应的类型名称跟属性名称进行一次正序排序,然后对租户、类型定义名称、唯一属性 key 进行一次正序排序,生成一个可读性高的唯一名称。其次,因唯一名称可能较长,需要进行一次 32 位摘要后进行存储,并加以索引进行查询,可以提升整体查询的有效性。最终全局的资产唯一 ID,则是用 Snowflake 算法生成的唯一 ID。因摘要算法有效概率重复,故使用分布式 ID 生成算法生成 ID,用于数据存储。资产采集流式采集有非常好的优势,可以通过消息队列,实现系统间解耦,实现数据的准实时上报,同时对事件消息也有良好的扩展性。周期性采集是流式采集的⼀个补充,它包括两种⽅式基于 ETL 或系统接口的主动推送,或类似数据发现系统的数据主动拉取。对于以上两种⽅式还没有达成的采集,可以用过人工补录的形式进行填写,以支持注入对接系统无法支持上报或部分血缘无法解析等场景,提升数据完整度。下面给大家介绍一下众安元数据系统⼏个版本采集流程的迭代——V1.0 版本是完全基于 T+1 的离线 ETL,我们会把数据开发⼯作台、调度系统以及阿⾥云 MaxCompute 元数据加载到数仓后,通过 ETL 处理推送到元数据平台,因数据量不大一个支持递归的关系型数据库即可满足要求。若数据量较大,则可以通过搜索引擎和图数据库进行扩展。随着业务的发展,数据开发对于元数据的时效性要求会越来越高,比如分析师创建的临时数据想 T0 就直接分享给其他部门使用,以及元数据整体数量越来越大,处理耗时较长,获取的时间越来越晚。基于以上需求,我们在元数据平台开了⼀层 API,在数据开发工作台进行表操作时,或调度系统创建调度任务时,会调用接口将数据同步给元数据平台。同时晚上我们依然会有离线的 ETL 进行数据补充,两者结合起来进行数据源的数据查询服务。接口模式也会有一定的弊端,在各个对接的业务系统中,会有大量的同步嵌套流程,元数据服务不可用或执行时间过长,例如系统发版时的业务中断,创建一个数百列的表引发的接口超时等,均会影响正常业务流程。于是我们参考各类开源元数据平台设计思路,设计了基于流式事件的元数据平台,基于不同的事件,对接系统通过消息队列上报后,实现系统间解耦。资产平台基于不同事件进行分类处理,并将最终的数据存储到搜索引擎、关系型数据库,以及图数据库当中。平台架构下⾯给⼤家介绍⼀下众安数据资产平台的架构,我们将平台分为了 5 个子系统。Portal 服务对接前端,提供通用的实体页面布局配置接口,实现配置化的页面布局。同时转发请求到 Core Service 进行处理,比如查询、类型定义等。Discovery 服务主要就是周期性的采集服务,通过配置定时的采集任务,并实现元数据的定时采集。系统 SDK 所有服务对接资产平台,均需要通过 SDK 进行对接,包括 Discovery 服务、数据超市、报表平台、开发⼯作台、数据标签平台等,SDK 提供了统一的事件拼装、权限管理、事件推送等功能,可以极大的提升平台间交互的开发效率。Event 服务负责消费消息队列中的消息,进行事件的解析和数据持久化。Core 服务提供统一的查询 API、标签 API 以及类型定义的 API 来实现查询跟类型定义的管理。同时我们提供了统一的数据存储层模块 Repo,来实现查询器和统一数据处理器的相关处理,其内部提供了数据库及图库的扩展 SPI,以便实现相关扩展。我们将资产平台的事件抽象为以下三种:元数据事件 MetadataEvent,包括实体/关系的增删改查等子事件。元数据异常事件 FailMetadataEvent,在处理 MetadataEvent 时失败了,比如类型定义不存在或事件顺序有问题,我们会统一生成一个元数据异常失败事件,可以基于此事件做异常数据落库或告警通知。平台事件 PlatformEvent,包括使用元数据平台触发的埋点事件,包括实体的收藏、查询、使用以及安全分级等事件,其中一部分会做按天级别的统计处理,以便在平台上可以看到类似的统计信息。事件进⾏处理,需要关注以下三点:分而治之,因为整体的事件的数据量会⽐较多,为了保证性能需要按照 Event 类别和影响,使⽤不同的消息队列。对于我们刚才介绍的三种型的事件,我们实际使用了三个 Kafka Topic 进行消息推送。消息的顺序,对于元数据相关事件,消息消费需要严格保证有序,如何来保证有序呢?我们⽬前采⽤的⽅案是由 Kafka Topic 单分区模式来解决的,为什么不⽤多 Partition 呢?因为实体跟关系之间的注册有可能是会分到不同的 Partition 上来进⾏处理的,因为异步消费处理有可能不同分区的数据产生消费堆积,有概率出现不同的分区,消费注册事件先到,实体注册事件后到的情况,导致废消息的出现。最终一致性,因为事件 Event 的异步处理,我们只能保证数据的最终⼀致性。好,那讲完了事件的消费流程,我们下⾯就要来看数据持久化的流程。我们的数据事件从消息队列拿到之后,会被我们的事件服务 Event Service 所消费,Event Service 中的事件处理器在消费数据的时候会⽴刻对这个数据进⾏⼀份数据的存储,它会存到关系型数据库⾥⾯,作为⼀个审计的回溯⽇志。在存储完回复⽇志之后,事件处理器就会开始对事件进⾏处理,如果事件处理异常的话,根据特定的这种事件类型,我们会有选择的把⼀些异常的事件放到异常事件的消息队列⾥⾯,然后供下游的系统进⾏订阅通知,或者是做内部后期的问题排查。如果事件处理成功了之后,我们会把数据丢到联合数据处理器当中。那联合数据处理器内部其实就是我们对关系型数据库以及图库的数据进⾏了⼀个整体的事务的包裹,以便两者之间出现失败的时候,可以对数据内容进⾏回滚。那在数据持久化当中,我们的关系型数据库跟图库当中分别存储了什么内容呢?像关系型数据库当中,我们往往存储了实体跟关系的数据,包括属性跟这种实际的这种名称的⼀些定义,同时还存储了实体的统计类的信息⽤于分析,还有类型定义的数据⽤于各种各样数据的这样⼀种校验。那图库当中主要就是点边的这种关系⽤于图谱的查询。资产的查询分析集成于 Core Service 模块中,目前有两大场景分类,数据地图和血缘分析。数据地图类检索,一般是分查询,我们定义一套类似于 ES DSL 风格的查询接口请求,通过查询解析器,翻译成要查询的关系型数据库语句,目前因为数据量还在PG的承受范围内,我们并没有使用 ES。同时使用、收藏、查询的统计类记录和变动记录,也是存放于 PG 当中,通过指定接口查询。血缘分析类查询,一般是关系查询,我们也通过类似于 ES DSL 风格的查询接口请求,通过查询解析器,翻译成图数据库所识别的 nGQL 或 Cypher 语句,包括 N 跳关系查询、子图查询、属性查询等。对于⼀些特殊场景查询需求,比如数据大盘,或特定实体的扩展事件,我们通过或定制化查询的方式进行处理。02 NebulaGraph 在众安资产平台的实践图数据库选型我们在做⾃主化平台开发之前,对热门开源项目的图数据库选型做了调研。选型主要考虑两⽅⾯的因素,数据库架构和资产平台设计的匹配性。在架构因素⽅⾯,核心因素是读写性能、分布式扩展、事务支持和第三方依赖。对于 Neo4j 来说,虽然它的性能读写性能⾮常优越和原⽣存储,但是因为 3.x 版本之后,社区版已经不再⽀持分布式模式,所以说肯定不能达到我们预期的要求。JanusGraph 和 NebulaGraph 均支持分布式扩展和存算分离架构,但前者的存储、索引均依赖于第三方组件,带来大量额外运维工作,其支持分布式事务,而 NebulaGraph 不支持分布式事务处理。资产平台设计的匹配性因素,核心因素是数据隔离、属性及 Schema 数量上线、属性类型、查询语言等。JanusGraph/Neo4j 社区版属性集均不支持强 Schema,这意味着更灵活的属性配置。同时,属性类型也支持诸如 map、set 等复杂类型。NebulaGraph 属性集虽然有强 Schema 依赖,但属性和 Schema 数量没有上限,也支持 Schema 的修改,唯一美中不足的是不支持 map/set 等复杂类型属性,这将对类型定义和系统设计有一定的影响,以及对潜在的需求场景有一定的约束。三种数据库均有通用的查询语言、以及可以基于 GraphX 进行图算法分析。为什么选择 NebulaGraph基于以下四点的考虑,众安选择了 NebulaGraph——第⼀是分布式的存算分离架构,可以以最优的成本,快速扩缩容相关服务。第二是外部组件依赖较少,⽅便运维。第三是卓越的读写性能,在19 年年底众安金融风控场景,我们对 NebulaGraph 就进⾏了⼀定的性能测试,我们在纯 nGQL的 insert 这种写入方案下,通过 DataX 可以实现 300w record/s 的数据写⼊速度,这个是一个非常惊人的数据同步的体验。第四是数据存储格式,因为众安有大量的子公司租户,需要进行数据的存储隔离,如果是其他产品就需要部署多套图库,或一套图库数据里打租户标签。NebulaGraph可以使用图空间的方式实现天然的数据隔离,大大简化了我们开发的工作量。NebulaGraph 阿⾥云部署模式因为众安没有独立机房,所有的服务均依赖于阿里云金融云,基于阿⾥云 ECS 的能力,可以快速实现服务器以及服务器上存储资源的弹性扩收容。实际部署中,我们将 graphd 跟 mated、 storaged 进行了分开部署,避免大量查询导致内存过高,影响到其他图数据服务的稳定性。graphd 占用了 2 台 4C 8G 服务器,metad/storaged 占用了 3 台 4C 16G 服务器。当前资产平台的实体数量在 2,500w 个左右,边数据在 4左右,主要为数据集类型数据。我们使用每台 ECS 使用了两块 200G 的 ESSD 进行存储,根据 NebulaGraph 的推荐,磁盘的数量越多,图空间 Partition 的扩展的数量就可以越多,可以获得更好的并发处理能力。众安在NebulaGraph中的模型设计下面介绍一下基于 NebulaGraph 的模型设计。对于实体定义来说,对应 NebulaGraph 的某一个 Tag,其相对于其他图数据库类似于 Label 概念,就是某个属性集的名称,通过 Tag 可以更快检索倒到某一个实体点下的属性,类型定义的 Tag 必须同这一类型的点 ID 进行强绑定,注册时需要进行相关校验。另一个属性集的概念是公共标签,公共标签可以做很多事情,比如业务属性集、实体标签等。公共标签在 NebulaGraph 当中也对应一个 Tag,这个 Tag 可以绑定到多种不同的实体,比如环境公共标签,可以赋给 MySQL 数据源实体,也可以赋给 MaxCompute数据源实体等。对于关系定义来说,对应 NebulaGraph 中的某个 Edge Type,类型定义中的来源目标端点的实体类型,必须同这一类型的点 ID 进行强绑定,注册时需要进行相关校验。对于数据隔离来说,上述实体和关系模型,通过 NebulaGraph 的图空间进行隔离,在众安内部的多个租户实体下,比如保险、小贷、科技等,会在租户初始化时创建指定图空间,后续的类型定义均在租户图空间下进行。最后我们再来看⼀下模型设计的继承关系。我们所有的实体根节点是⼀个叫做 Asset 的实体定义,我们将一些公共属性定义其中,包括名称、展示名称、备注、类型等;基于 Asset 类型,我们实现了对接平台的各种资产实体,报表平台里的模型、视图、画布、⻔户等实体,数据超市里的路由 API、数据 API 以及外部扩展 API 等实体,开发工台里的调度任务、流计算任务、工作空间、文件等实体,以及两个比较特殊的资产属主实体和服务资产实体。另一个特殊的实体是数据集实体,我们将不同数据源数据源、表、列等信息均定义了独立的资产实体定义,以便实现不同数据源的差异化属性展示。我们最终的全链路数据资产,均是通过数据集及其子类自定义实现串联,从而实现跨平台的链路分析。比如调度作业的库表血缘,可以关联到报表平台的数据模型,也可以关联到数仓的 Data API 依赖的 Table Store 的某张表等等。03 未来展望2022年年底,众安基本上已经实现了各个平台的各种资产的注册跟上报的过程。2023年,我们将在围绕数据资产割接、数据安全管理和数据治理三个方面进行扩展性开发。数据资产割接,将站在用户实体的角度上,快速识别个人关联的数据资产,时间属主资产切换和离职交接功能。数据安全管理,基于资产平台的能力做出多种扩展,迁移内部老元数据系统的表分级、权限审批功能;内部脱敏规则配置平台及 SDK,扩展支持基于表分级数据加密和白名单策略等。数据治理,基于资产平台的全链路血缘分析能力,观察资产热度、使用等关键指标,清理无效作业和重复计算作业,实现降本增效,减少云平台使用费用。要来感受同众安科技一样的图数据库体验嘛?NebulaGraph 阿里云计算巢现 30 天免费使用中,点击链接来搭建自己的资产管理系统吧!
【DSW Gallery】OneClassSVM 算法解决异常检测问题
直接使用请打开OneClassSVM 算法解决异常检测问题,并点击右上角 “ 在DSW中打开” 。Alink: 如何使用 OneClassSvm 异常检测算法做流式检测在数据挖掘中,异常检测(英语:anomaly detection)对不匹配预期模式或数据集中其他项目的项目、事件或观测值的识别。通常异常项目会转变成银行欺诈、网络攻击、结构缺陷、身体疾病、文本错误等类型的问题。异常也被称为离群值、新奇、噪声、偏差和例外。在实际业务中的异常检测往往希望检测是实时的,也就是今天我们要介绍的流式异常检测。Alink[1] 中提供了多种异常检测算法,例如OneClassSvm、IsolationForest、LOF、SOS、BoxPlot、Ecod、Dbscan、Kde、KSigma等算法,下面将重点介绍如何在DSW中使用OneClassSvm算法搭建一个流式异常检测业务,并且对这些异常检测算法的检测结果进行评估。[1] https://github.com/alibaba/Alink运行环境要求PAI-DSW 官方镜像中默认已经安装了 PyAlink,内存要求 4G 及以上。本 Notebook 的内容可以直接运行查看,不需要准备任何其他文件。# 创建本地的pyalink环境,并设置并行度为2
from pyalink.alink import *
useLocalEnv(2)异常检测数据我们这里选用两个数据集合 ForestCover[1] 来作为我们异常检测的输入数据,搭建我们的异常检测业务流程。该数据集合的介绍及获取参见下面链接内容。其中 ForestCover 数据集合的异常点label集合是:4。[1] http://odds.cs.stonybrook.edu/forestcovercovertype-dataset/forestCover = CsvSourceBatchOp() \
.setFilePath("./forest_cover.csv") \
.setSchemaStr("elevation double, aspect double, slope double, hd_to_hydrology double, vd_to_hydrology double,"
+" hd_to_roadways double, hillshade_9am double, hillshade_noon double, hillshade_3pm double,"
+" hd_to_fps double, label int")
forestCover.lazyPrint(5)
BatchOperator.execute()OneClassSvm 模型OneClassSvm 算法是一个无监督的异常检测算法,我们这里使用线性 kernel 对数据进行训练,得到异常检测模型。我们使用 OneClassSvm 算法来检测 ForestCover 数据。具体包括如下内容:训练模型,并对模型进行评估评估结果比较好的模型部署成一个流服务对实时数据的检测结果进行评估将训练和预测同时在一个组件中完成基于 Window 的实时异常检测训练 OneClassSvm 模型,并评估经过调参后,我们发现当只使用一个特征(elevation)时,得到的效果是最好的。这个说明在异常检测过程中,并不是特征越多越好,有的特征是起反向作用的,只有选择合适的特征才会闹大最好的效果。FEATURE_COLS = ["elevation"]
LABEL_COL = "label"
PREDICTION_COL = "pred"
PREDICTION_DETAIL_COL = "pred_info"
OUTLIER_VALUES = ["4"]
# 异常检测模型训练
ocsvmModel = OcsvmModelOutlierTrainBatchOp() \
.setNu(0.01) \
.setKernelType("LINEAR") \
.setGamma(0.1) \
.setFeatureCols(FEATURE_COLS).linkFrom(forestCover)
# 使用模型对数据检测
results = OcsvmModelOutlierPredictBatchOp() \
.setPredictionCol(PREDICTION_COL) \
.setOutlierThreshold(1.5) \
.setPredictionDetailCol(PREDICTION_DETAIL_COL) \
.linkFrom(ocsvmModel, forestCover)
# 评估模型
results.link(EvalOutlierBatchOp()\
.setLabelCol(LABEL_COL)\
.setPredictionDetailCol(PREDICTION_DETAIL_COL)\
.setOutlierValueStrings(OUTLIER_VALUES)\
.lazyPrintMetrics("OCSVM forest_cover"))
ocsvmModel.link(AkSinkBatchOp().setFilePath("./ocsvm_model.ak").setOverwriteSink(True))
BatchOperator.execute()OCSVM forest_cover
-------------------------------- Metrics: --------------------------------
Outlier values: [4] Normal values: [2]
Auc:0.9994 Accuracy:0.9974 Precision:0.8523 Recall:0.8846 F1:0.8682
|Pred\Real|Outlier|Normal|
|---------|-------|------|
| Outlier| 2430| 421|
| Normal| 317|282880|评估结果比较好的模型部署成一个流服务这里我们构造了一个假的实时数据流来辅助搭建这个异常检测流服务,我们使用上面训练好的模型对这个数据流进行实时异常检测。# 创建一个流数据源,实际业务中可以使用Kafka,SLS等流数据源读入实时数据
streamForestCover = CsvSourceStreamOp() \
.setFilePath("./forest_cover.csv") \
.setSchemaStr("elevation double, aspect double, slope double, hd_to_hydrology double, vd_to_hydrology double,"
+" hd_to_roadways double, hillshade_9am double, hillshade_noon double, hillshade_3pm double,"
+" hd_to_fps double, label int").link(SpeedControlStreamOp().setTimeInterval(0.0001))
# 读入已经训练好的模型
ocsvmModel = AkSourceBatchOp().setFilePath("./ocsvm_model.ak")
# 使用模型搭建一个流预测服务
results = OcsvmModelOutlierPredictStreamOp(ocsvmModel) \
.setPredictionCol(PREDICTION_COL) \
.setPredictionDetailCol(PREDICTION_DETAIL_COL) \
.linkFrom(streamForestCover)
# 打印预测结果数据
results.sample(0.00002).print()'DataStream 9ecd8b04536d4437991ec0cb133b7710 : ( Updated on 2022-10-19 12:03:45, #items received: 6 )'对实时数据的检测结果进行评估我们对实时预测结果使用流评估组件,实时评估某个时间间隔内样本的检测效果,并打印"Accuracy", "AUC","ConfusionMatrix", "F1"等指标。# 对流预测结果进行实时评估,可以设置时间间隔,来控制评估结果输出频次
results.link(EvalOutlierStreamOp()\
.setLabelCol(LABEL_COL)\
.setTimeInterval(10) \
.setPredictionDetailCol(PREDICTION_DETAIL_COL)\
.setOutlierValueStrings(OUTLIER_VALUES)) \
.link(JsonValueStreamOp().setSelectedCol("Data")
.setReservedCols(["Statistics"])
.setOutputCols(["Accuracy", "AUC", "ConfusionMatrix", "F1"])
.setJsonPath(["$.Accuracy", "$.AUC", "$.ConfusionMatrix", "$.F1"])).print()
StreamOperator.execute()'DataStream a5dd6d52e2b249858afdfad5d1a08409 : ( Updated on 2022-10-19 12:03:51, #items received: 10 )'将训练和预测同时在一个组件中完成(不保存模型的批式任务)该过程中,我们可以设定一个训练模型的样本数目,算法会buffer住这些数据并训练异常检测模型并同时用来预测这些样本。# 对数据分批处理,每20000条数据做一次训练和预测,并输出预测结果
results = OcsvmOutlierBatchOp() \
.setNu(0.01) \
.setKernelType("LINEAR") \
.setGamma(0.1) \
.setMaxSampleNumPerGroup(20000) \
.setFeatureCols(FEATURE_COLS)\
.setPredictionCol(PREDICTION_COL) \
.setPredictionDetailCol(PREDICTION_DETAIL_COL) \
.linkFrom(forestCover)
# 对预测结果进行评估,并输出评估指标
results.link(EvalOutlierBatchOp()\
.setLabelCol(LABEL_COL)\
.setPredictionDetailCol(PREDICTION_DETAIL_COL)\
.setOutlierValueStrings(OUTLIER_VALUES)\
.lazyPrintMetrics("OCSVM forest_cover"))\
.link(JsonValueBatchOp().setSelectedCol("Data")\
.setReservedCols(["Statistics"])\
.setOutputCols(["Accuracy", "AUC", "ConfusionMatrix", "F1"])\
.setJsonPath(["$.Accuracy", "$.AUC", "$.ConfusionMatrix", "$.F1"])).print()OCSVM forest_cover
-------------------------------- Metrics: --------------------------------
Outlier values: [4] Normal values: [2]
Auc:0.9993 Accuracy:0.9974 Precision:0.8491 Recall:0.8831 F1:0.8658
|Pred\Real|Outlier|Normal|
|---------|-------|------|
| Outlier| 2426| 431|
| Normal| 321|282870|基于 Window 的实时异常检测实时异常检测是使用当前时刻之前流过N个的样本作为训练集合训练模型对当前模型检测的算法。该算法组件每接收一条样本时会将该条样本之前的N条样本收集起来训练一个模型,并用这个模型预测该条样本。这种方式计算量巨大,每一条样本都会训练一个模型,所以不建议在QPS比较高的业务场景中使用。# 读入流数据,这里可以将数据源改为Kafka,Datahub,SLS等实时数据源
streamForestCover = CsvSourceStreamOp() \
.setFilePath("./forest_cover.csv") \
.setSchemaStr("elevation double, aspect double, slope double, hd_to_hydrology double, vd_to_hydrology double,"
+" hd_to_roadways double, hillshade_9am double, hillshade_noon double, hillshade_3pm double,"
+" hd_to_fps double, label int").sample(0.02).link(SpeedControlStreamOp().setTimeInterval(0.002))
# 实时Ocsvm异常检测
results = OcsvmOutlierStreamOp()\
.setNu(0.000005)\
.setKernelType("LINEAR")\
.setEpsilon(0.0001)\
.setGamma(0.1)\
.setPrecedingRows(1000)\
.setFeatureCols(FEATURE_COLS)\
.setPredictionCol(PREDICTION_COL)\
.setPredictionDetailCol(PREDICTION_DETAIL_COL)\
.linkFrom(streamForestCover)
# 结果采样打印
results.select("label, pred, pred_info").sample(0.002).print()'DataStream 4039631f01704291948e2b8b6d80075c : ( Updated on 2022-10-19 12:08:27, #items received: 11 )'<pyalink.alink.stream.common.stream_op_7.SampleStreamOp at 0x7f96e5fe4cd0># 实时评估算法预测的结果
results.link(EvalOutlierStreamOp()\
.setLabelCol(LABEL_COL)\
.setTimeInterval(10) \
.setPredictionDetailCol(PREDICTION_DETAIL_COL)\
.setOutlierValueStrings(OUTLIER_VALUES))\
.link(JsonValueStreamOp().setSelectedCol("Data")\
.setReservedCols(["Statistics"])\
.setOutputCols(["Accuracy", "AUC", "ConfusionMatrix", "F1"])\
.setJsonPath(["$.Accuracy", "$.AUC", "$.ConfusionMatrix", "$.F1"])).print()
StreamOperator.execute()'DataStream 996a42bd24e648589f5d5e931d91111b : ( Updated on 2022-10-19 12:09:10, #items received: 4 )'总结本文重点介绍了经典无监督异常检测算法 OneClassSVM 以及如何基于 Alink 在 DSW 上快速完成异常检测业务流程的搭建。并且针对不同的业务场景,给出了不同的代码案例,用户可以根据自己业务场景的需求选择合适的算法调用方式最终达到快速搭建业务流程的目的。如果用户的数据已经落盘,建议使用批的方式对数据进行检测,具体参见上面的代码案例。如果用户的数据是实时数据(Kafka,SLS,DataHub等),则建议用户通过流服务的方式使用这个算法。
CPU基础知识详解
冯·诺依曼计算机冯·诺依曼计算机由存储器、运算器、输入设备、输出设备和控制器五部分组成。哈佛结构哈佛结构是一种将程序指令存储和数据存储分开的存储器结构,它的主要特点是将程序和数据存储在不同的存储空间中,即程序存储器和数据存储器是两个独立的存储器,每个存储器独立编址、独立访问,目的是为了减轻程序运行时的访存瓶颈。哈佛架构的中央处理器典型代表ARM9/10及后续ARMv8的处理器,例如:华为鲲鹏920处理器。组成计算机的基础硬件都需要与主板(Motherboard)连接计算机基础硬件 (2)Opening the Box(Apple IPad2)手机的内部结构 – 华为Mate30 Pro主板(来自于 Tech Insights)主板 背面射频板Inside the Processor (CPU)Datapath(数据通路): performs operationson dataControl: sequences datapath, memory, ...Register 寄存器Cache memory 缓存Small, fast: SRAM(静态随机访问存储器)memory for immediate access to dataIntel Core i7-5960X毅力号CPU曝光:250nm工艺、23年旧架构、主频仅233MHz毅力号搭载的处理器是20多年前技术的产品。处理器型号为PowerPC 750处理器,与1998年苹果出品的iMac G3 电脑同款,PowerPC 750 处理器最高主频速度仅233MHz,且晶体管数量也只有600 万个,但单价仍高达20 万美元(约130万元)。抗辐射、耐寒冷-55~125℃对比苹果最近推出的M1ARM 架构处理器拥有最高主频3.2GHz,晶体管数量达160 亿个。处理器发展趋势主流CPU发展路径Through the Looking GlassLCD screen: picture elements (pixels像素)Mirrors content of frame buffer memory帧缓冲存储器Touchscreen(触摸屏)PostPC deviceSupersedes(取代)keyboard and mouseResistive阻性 and Capacitive容性typesMost tablets, smart phones use capacitiveCapacitive allows multiple touches simultaneously(多点同时触控)A Safe Place for DataVolatile main memory(易失性主存)Loses instructions and data when power off(断电)Non-volatile secondary memoryMagnetic disk(磁盘)Flash memory(闪存)Optical disk (CDROM, DVD) 光盘Networks 与其他计算机通信Communication(通信), resource sharing(资源共享), nonlocal access(远程访问)Local area network (LAN): Ethernet,局域网/以太网Wide area network (WAN): the Internet,广域网/互联网Wireless network: WiFi, Bluetooth(蓝牙)计算机基础硬件 (3)Abstractions抽象The BIG PictureAbstraction helps us deal with complexityHide lower-level detailInstruction set architecture (ISA)指令集体系结构The hardware/software (abstraction) interfaceApplication< ---- > binary interface应用二进制接口The ISA plus system software interfaceImplementation(区别于Architecture)The details underlying the interface半导体与集成电路Technology Trends 处理器和存储器制造技术--趋势Electronics technology continues to evolveIncreased capacity and performanceReduced costSemiconductor TechnologySilicon硅: semiconductor 半导体Add materials to transform properties属性:ConductorsInsulatorsSwitch设备列表厂商在制造芯片的过程中,从前端工序、到晶圆制造工序,之后再到封装和测试工序,主要用到的设备依次包括,单晶炉、气相外延炉、氧化炉、低压化学气相沉积系统、磁控溅射台、光刻机、刻蚀机、离子注入机、晶片减薄机、晶圆划片机、键合封装设备、测试机、分选机和探针台等集成电路发明1952年,英国雷达研究所的科学家达默在一次会议上提出:可以把电子线路中的分立元器件,集中制作在一块半导体晶片上,一小块晶片就是一个完整电路,这样一来,电子线路的体积就可大大缩小,可靠性大幅提高。这就是初期集成电路的构想。1956年,美国材料科学专家富勒和赖斯发明了半导体生产的扩散工艺,这样就为发明集成电路提供了工艺技术基础。1958年9月,美国德州仪器公司的青年工程师杰克·基尔比(Jack Kilby),成功地将包括锗晶体管在内的五个元器件集成在一起,基于锗材料制作了一个叫做相移振荡器的简易集成电路,并于1959年2月申请了小型化的电子电路(Miniaturized Electronic Circuit)专利(专利号为No.31838743,批准时间为1964年6月26日),这就是世界上第一块锗集成电路。2000年,集成电路问世42年以后,人们终于了解到他和他的发明的价值,他被授予了诺贝尔物理学奖。诺贝尔奖评审委员会曾经这样评价基尔比:“为现代信息技术奠定了基础”。1959年7月,美国仙童半导体公司的诺伊斯,研究出一种利用二氧化硅屏蔽的扩散技术和PN结隔离技术,基于硅平面工艺发明了世界上第一块硅集成电路,并申请了基于硅平面工艺的集成电路发明专利(专利号为No.2981877,批准时间为1961年4月26日。虽然诺伊斯申请专利在基尔比之后,但批准在前)。基尔比和诺伊斯几乎在同一时间分别发明了集成电路,两人均被认为是集成电路的发明者,而诺伊斯发明的硅集成电路更适于商业化生产,使集成电路从此进入商业规模化生产阶段。Intel Core i7 Wafer300mm wafer, 280 chips, 32nm technologyEach chip is 20.7 x 10.5 mmIntegrated Circuit Cost$Cost per die =\frac{\text { Cost per wafer }}{\text { Dies per wafer } \times \text { Yield }}$$Dies per wafer \approx Wafer area/Die area$$Yield =\frac{1}{(1+(\text { Defects per area } \times \text { Die area } / 2))^{2}}$成品率Defects per area:单位面积缺陷Die area:模具面积Defining PerformanceWhich airplane has the best performance? 从不同的方面进行考察。Response Time and ThroughputResponse time响应时间How long it takes to do a task(the time between the start and completion of a task)Throughput吞吐量Total work done per unit timee.g., tasks/transactions/… per hourHow are response time and throughput affected byReplacing the processor with a faster version? 改善处理器Adding more processors to do separate tasks? 添加更多的处理器Queue ?采用排队机制,改善吞吐量We’ll focus on response time for now…Relative PerformanceDefine Performance = 1/Execution Time“X is n time faster than Y”$Performance _{X} / Performance _{Y} = Execution time _{Y} / Execution time _{X}=n $Example: time taken to run a program10s on A, 15s on BExecution TimeB / Execution TimeA = 15s / 10s = 1.5So A is 1.5 times faster than BMeasuring Execution TimeElapsed time 消逝时间Total response time, including all aspectsProcessing, I/O, OS overhead, idle timeDetermines system performanceCPU time(共享时,独自占用CPU时间)Time spent processing a given jobDiscounts I/O time, other jobs’ sharesComprises user CPU time and system CPUtimeDifferent programs are affected differently byCPU and system performanceCPU ClockingOperation of digital hardware governed(掌控) by a constant-rate clock (数字同步电路)Clock period: duration of a clock cyclee.g., 250ps = 0.25ns = 250×10^–12sClock frequency (rate): cycles per seconde.g., 4.0GHz = 4000MHz = 4.0×10^9HzCPU TimeCPU Time = CPU Clock Cycles x Clock Cycle Time =$\frac{\text { CPU Clock Cycles }}{\text { Clock Rate }}$Performance improved byReducing number of clock cyclesIncreasing clock rateHardware designer must often trade off(折中)clock rate against cycle countCPU Time ExampleComputer A: 2GHz clock, 10s CPU timeDesigning Computer BAim for 6s CPU timeCan do faster clock, but causes 1.2 × clock cyclesHow fast must Computer B clock be?$Clock Cycles _{A}= CPU Time _{A} \times Clock Rate _{A}$=$10 \mathrm{~s} \times 2 \mathrm{GHz}=20 \times 10^{9}$=$\frac{1.2 \times 20 \times 10^{9}}{6 \mathrm{~s}}=\frac{24 \times 10^{9}}{6 \mathrm{~s}}=4 \mathrm{GHz}$Instruction Count and CPI$Clock Cycles = Instruction Count \times Cycles per Instruction$$CPUTime = Instruction Count \times CPI \times Clock Cycle Time$$=\frac{\text { Instruction Count } \times \mathrm{CPI}}{\text { Clock Rate }}$Instruction Count for a programDetermined by program, ISA and compilerAverage cycles per instructionDetermined by CPU hardwareIf different instructions have different CPI 指令具有不同CPIAverage CPI affected by instruction mixCPI ExampleComputer A: Cycle Time = 250ps, CPI = 2.0Computer B: Cycle Time = 500ps, CPI = 1.2Same ISAWhich is faster, and by how much?CPI in More DetailIf different instruction classes take different numbers 每指令类CPI不同,且指令出现频率不同$\text { Clock Cycles }=\sum_{\mathrm{i}=1}^{n}\left(\mathrm{CPI}_{\mathrm{i}} \times \operatorname{Instruction~Count~}_{\mathrm{i}}\right)$Weighted average CPI(平均CPI)$\mathrm{CPI}=\frac{\text { Clock Cycles }}{\text { Instruction Count }}=\sum_{\mathrm{i}=1}^{\mathrm{n}}\left(\mathrm{CPI}_{\mathrm{i}} \times \frac{\text { Instruction Count }_{\mathrm{i}}}{\text { Instruction Count }}\right)$CPI ExampleAlternative compiled code sequences using instructions in classes A, B, C (三类指令)Which code sequence executes the most instructions? sequence2Which will be faster?What is the CPI for each sequence?Performance Summary$\text { CPU Time }=\frac{\text { Instructions }}{\text { Program }} \times \frac{\text { Clock cycles }}{\text { Instruction }} \times \frac{\text { Seconds }}{\text { Clock cycle }}$Performance depends onAlgorithm: affects IC(指令数), possibly CPIProgramming language: affects IC, CPICompiler: affects IC, CPIInstruction set architecture: affects IC, CPI, TcPower TrendsIn CMOS IC technology$\text { Power }=\frac{1}{2} \text { Capacitive load } \times \text { Voltage }^{2} \times \text { Frequency }$Capacitive load:负载电容。Reducing PowerSuppose a new CPU has85% of capacitive load of old CPU15% voltage and 15% frequency reduction$\frac{P_{\text {new }}}{P_{\text {old }}}=\frac{C_{\text {old }} \times 0.85 \times\left(V_{\text {old }} \times 0.85\right)^{2} \times F_{\text {old }} \times 0.85}{C_{\text {old }} \times V_{\text {old }}^{2} \times F_{\text {old }}}=0.85^{4}=0.52$The power wall (功率墙)We can’t reduce voltage further 可能低压泄露We can’t remove more heat 可能sleepHow else can we improve performance?Constrained by power, instruction-level parallelism, memory latency(受到功率、指令级并行性、内存延迟的制约)Multiprocessors(多核)Multicore microprocessorsMore than one processor per chipRequires explicitly parallel programmingCompare with instruction level parallelism(e.g.流水线)Hardware executes multiple instructions at onceHidden from the programmer (程序员不可见)Hard to doProgramming for performance 编程难度增加Load balancing 负载均衡Optimizing communication and synchronizationA R M提供更多计算核心多核架构单位芯片面积提供更强算力,更符合分布式业务的需求A R M多核高并发优势,匹配互联网分布式架构随着多核A R M CPU的性能不断增强,应用领域不断扩展A R M服务器级别处理器一览SPEC CPU BenchmarkPrograms used to measure performanceSupposedly typical of actual workloadStandard Performance Evaluation Corp (SPEC)Develops benchmarks for CPU, I/O, Web, …SPEC CPU2006Elapsed time to execute a selection of programsNegligible I/O, so focuses on CPU performanceNormalize relative to reference machine(参考机器)Summarize as geometric mean of performance ratiosCINT2006 (integer) and CFP2006 (floating-point)$\sqrt[n]{\prod_{\mathrm{i}=1}^{n} \text { Execution time ratio }_{i}}$CINT2006 for Intel Core i7 920SPEC Power BenchmarkPower consumption of server at different workload levelsPerformance: ssj_ops/secPower: Watts (Joules/sec)SPECpower_ssj2008 for Xeon X5650Pitfall(陷阱): Amdahl’s LawImproving an aspect of a computer and expecting a proportional improvement in overall performance$T_{\text {improved }}=\frac{T_{\text {affected }}}{\text { improvement factor }}+T_{\text {unaffected }}$Example: multiply accounts for 80s/100sSpeedup(E)=1/{(1-P)+P/S}Amdahl's law主要的用`途是指出了在计算机体系结构设计过程中,某个部件的优化对整个结构的优化帮助是有上限的,这个极限就是当S->时, speedup(E)= 1/(1-P);也从另外一个方面说明了在体系结构的优化设计过程中,应该挑选对整体有重大影响的部件来进行优化,以得到更好的结果。Fallacy谬误: Low Power at IdleLook back at i7 power benchmarkAt 100% load: 258WAt 50% load: 170W (66%)At 10% load: 121W (47%)Google data centerMostly operates at 10% – 50% loadAt 100% load less than 1% of the timeConsider designing processors to make power proportional to loadPitfall: MIPS as a Performance MetricMIPS: Millions of Instructions Per SecondDoesn’t account for 考虑Differences in ISAs between computersDifferences in complexity between instructions$\begin{aligned}
\text { MIPS } &=\frac{\text { Instruction count }}{\text { Execution time } \times 10^{6}} \\
&=\frac{\text { Instruction count }}{\frac{\text { Instruction count } \times \mathrm{CPI}}{\text { Clock rate }} \times 10^{6}}=\frac{\text { Clock rate }}{\mathrm{CPI} \times 10^{6}}
\end{aligned}$CPI varies between programs on a given CPUConcluding RemarksCost/performance is improvingDue to underlying technology developmentHierarchical layers of abstractionIn both hardware and softwareInstruction set architectureThe hardware/software interfaceExecution time: the best performance measurePower is a limiting factorUse parallelism to improve performance
JAVA面试——Spring 原理
它是一个全面的、企业应用开发一站式的解决方案,贯穿表现层、业务层、持久层。但是 Spring仍然可以和其他的框架无缝整合。6.1.1. Spring 特点6.1.1.1. 轻量级6.1.1.2. 控制反转6.1.1.3. 面向切面6.1.1.4. 容器6.1.1.5. 框架集合6.1.2. Spring 核心组件6.1.3. Spring 常用模块6.1.4. Spring 主要包6.1.5. Spring 常用注解bean 注入与装配的的方式有很多种,可以通过 xml,get set 方式,构造函数或者注解等。简单易用的方式就是使用 Spring 的注解了,Spring 提供了大量的注解方式。6.1.6. Spring 第三方结合6.1.7. Spring IOC 原理6.1.7.1. 概念Spring 通过一个配置文件描述 Bean 及 Bean 之间的依赖关系,利用 Java 语言的反射功能实例化Bean 并建立 Bean 之间的依赖关系。 Spring 的 IoC 容器在完成这些底层工作的基础上,还提供了 Bean 实例缓存、生命周期管理、 Bean 实例代理、事件发布、资源装载等高级服务。6.1.7.2. Spring 容器高层视图Spring 启动时读取应用程序提供的 Bean 配置信息,并在 Spring 容器中生成一份相应的 Bean 配置注册表,然后根据这张注册表实例化 Bean,装配好 Bean 之间的依赖关系,为上层应用提供准备就绪的运行环境。其中 Bean 缓存池为 HashMap 实现6.1.7.3.BeanFactory-框架基础设施IOC 容器实现BeanFactory 是 Spring 框架的基础设施,面向 Spring 本身;ApplicationContext 面向使用Spring 框架的开发者,几乎所有的应用场合我们都直接使用 ApplicationContext 而非底层的 BeanFactory1.1..1.1.1 BeanDefinitionRegistry 注册表1. Spring 配置文件中每一个节点元素在 Spring 容器里都通过一个 BeanDefinition 对象表示,它描述了 Bean 的配置信息。而 BeanDefinitionRegistry 接口提供了向容器手工注册BeanDefinition 对象的方法。1.1..1.1.2 BeanFactory 顶层接口2. 位于类结构树的顶端 ,它最主要的方法就是 getBean(String beanName),该方法从容器中返回特定名称的 Bean,BeanFactory 的功能通过其他的接口得到不断扩展:1.1..1.1.3 ListableBeanFactory3. 该接口定义了访问容器中 Bean 基本信息的若干方法,如查看 Bean 的个数、获取某一类型Bean 的配置名、查看容器中是否包括某一 Bean 等方法;1.1..1.1.4 HierarchicalBeanFactory 父子级联4. 父子级联 IoC 容器的接口,子容器可以通过接口方法访问父容器; 通过HierarchicalBeanFactory 接口, Spring 的 IoC 容器可以建立父子层级关联的容器体系,子容器可以访问父容器中的 Bean,但父容器不能访问子容器的 Bean。Spring 使用父子容器实现了很多功能,比如在 Spring MVC 中,展现层 Bean 位于一个子容器中,而业务层和持久层的 Bean 位于父容器中。这样,展现层 Bean 就可以引用业务层和持久层的 Bean,而业务层和持久层的 Bean 则看不到展现层的 Bean。1.1..1.1.5 ConfigurableBeanFactory5. 是一个重要的接口,增强了 IoC 容器的可定制性,它定义了设置类装载器、属性编辑器、容器初始化后置处理器等方法;1.1..1.1.6 AutowireCapableBeanFactory 自动装配6. 定义了将容器中的 Bean 按某种规则(如按名字匹配、按类型匹配等)进行自动装配的方法;1.1..1.1.7 SingletonBeanRegistry 运行期间注册单例 Bean7. 定义了允许在运行期间向容器注册单实例 Bean 的方法;对于单实例( singleton)的 Bean 来说,BeanFactory 会缓存 Bean 实例,所以第二次使用 getBean() 获取 Bean 时将直接从IoC 容器的缓存中获取 Bean 实例。Spring 在 DefaultSingletonBeanRegistry 类中提供了一个用于缓存单实例 Bean 的缓存器,它是一个用 HashMap 实现的缓存器,单实例的 Bean 以beanName 为键保存在这个 HashMap 中。1.1..1.1.8 依赖日志框框8. 在初始化 BeanFactory 时,必须为其提供一种日志框架,比如使用 Log4J, 即在类路径下提供 Log4J 配置文件,这样启动 Spring 容器才不会报错。ApplicationContext 面向开发应用ApplicationContext 由 BeanFactory 派 生 而 来 , 提 供 了 更 多 面 向 实 际 应 用 的 功 能 。ApplicationContext 继承了 HierarchicalBeanFactory 和 ListableBeanFactory 接口,在此基础上,还通过多个其他的接口扩展了 BeanFactory 的功能:1. ClassPathXmlApplicationContext:默认从类路径加载配置文件2. FileSystemXmlApplicationContext:默认从文件系统中装载配置文件3. ApplicationEventPublisher:让容器拥有发布应用上下文事件的功能,包括容器启动事件、关闭事件等。4. MessageSource:为应用提供 i18n 国际化消息访问的功能;5. ResourcePatternResolver : 所 有 ApplicationContext 实现类都实现了类似于PathMatchingResourcePatternResolver 的功能,可以通过带前缀的 Ant 风格的资源文件路径装载 Spring 的配置文件。6. LifeCycle:该接口是 Spring 2.0 加入的,该接口提供了 start()和 stop()两个方法,主要用于控制异步处理过程。在具体使用时,该接口同时被 ApplicationContext 实现及具体Bean 实现, ApplicationContext 会将 start/stop 的信息传递给容器中所有实现了该接口的 Bean,以达到管理和控制 JMX、任务调度等目的。7. ConfigurableApplicationContext 扩展于 ApplicationContext,它新增加了两个主要的方法: refresh()和 close(),让 ApplicationContext 具有启动、刷新和关闭应用上下文的能力。在应用上下文关闭的情况下调用 refresh()即可启动应用上下文,在已经启动的状态下,调用 refresh()则清除缓存并重新装载配置信息,而调用 close()则可关闭应用上下文。WebApplication 体系架构WebApplicationContext 是专门为 Web 应用准备的,它允许从相对于 Web 根目录的路径中装载配置文件完成初始化工作。从 WebApplicationContext 中可以获得ServletContext 的引用,整个 Web 应用上下文对象将作为属性放置到 ServletContext 中,以便 Web 应用环境可以访问 Spring 应用上下文。6.1.7.4. Spring Bean 作用域Spring 3 中为 Bean 定义了 5 中作用域,分别为 singleton(单例)、prototype(原型)、request、session 和 global session,5 种作用域说明如下:singleton:单例模式(多线程下不安全)1. singleton:单例模式,Spring IoC 容器中只会存在一个共享的 Bean 实例,无论有多少个Bean 引用它,始终指向同一对象。该模式在多线程下是不安全的。Singleton 作用域是Spring 中的缺省作用域,也可以显示的将 Bean 定义为 singleton 模式,配置为:<bean id="userDao" class="com.ioc.UserDaoImpl" scope="singleton"/>prototype:原型模式每次使用时创建2. prototype:原型模式,每次通过 Spring 容器获取 prototype 定义的 bean 时,容器都将创建一个新的 Bean 实例,每个 Bean 实例都有自己的属性和状态,而 singleton 全局只有一个对象。根据经验,对有状态的bean使用prototype作用域,而对无状态的bean使用singleton作用域。Request:一次 request 一个实例3. request:在一次 Http 请求中,容器会返回该 Bean 的同一实例。而对不同的 Http 请求则会产生新的 Bean,而且该 bean 仅在当前 Http Request 内有效,当前 Http 请求结束,该 bean实例也将会被销毁。<bean id="loginAction" class="com.cnblogs.Login" scope="request"/>session4. session:在一次 Http Session 中,容器会返回该 Bean 的同一实例。而对不同的 Session 请求则会创建新的实例,该 bean 实例仅在当前 Session 内有效。同 Http 请求相同,每一次session 请求创建新的实例,而不同的实例之间不共享属性,且实例仅在自己的 session 请求内有效,请求结束,则实例将被销毁。<bean id="userPreference" class="com.ioc.UserPreference" scope="session"/>global Session5. global Session:在一个全局的 Http Session 中,容器会返回该 Bean 的同一个实例,仅在使用 portlet context 时有效。6.1.7.5. Spring Bean 生命周期实例化1. 实例化一个 Bean,也就是我们常说的 new。IOC 依赖注入2. 按照 Spring 上下文对实例化的 Bean 进行配置,也就是 IOC 注入。setBeanName 实现3. 如果这个 Bean 已经实现了 BeanNameAware 接口,会调用它实现的 setBeanName(String)方法,此处传递的就是 Spring 配置文件中 Bean 的 id 值BeanFactoryAware 实现4. 如果这个 Bean 已经实现了 BeanFactoryAware 接口,会调用它实现的 setBeanFactory,setBeanFactory(BeanFactory)传递的是 Spring 工厂自身(可以用这个方式来获取其它 Bean,只需在 Spring 配置文件中配置一个普通的 Bean 就可以)。ApplicationContextAware 实现5. 如果这个 Bean 已经实现了 ApplicationContextAware 接口,会调用setApplicationContext(ApplicationContext)方法,传入 Spring 上下文(同样这个方式也可以实现步骤 4 的内容,但比 4 更好,因为 ApplicationContext 是 BeanFactory 的子接口,有更多的实现方法)postProcessBeforeInitialization 接口实现-初始化预处理6. 如果这个 Bean 关联了 BeanPostProcessor 接口,将会调用postProcessBeforeInitialization(Object obj, String s)方法,BeanPostProcessor 经常被用作是 Bean 内容的更改,并且由于这个是在 Bean 初始化结束时调用那个的方法,也可以被应用于内存或缓存技术。init-method7. 如果 Bean 在 Spring 配置文件中配置了 init-method 属性会自动调用其配置的初始化方法。postProcessAfterInitialization8. 如果这个 Bean 关联了 BeanPostProcessor 接口,将会调用postProcessAfterInitialization(Object obj, String s)方法。注:以上工作完成以后就可以应用这个 Bean 了,那这个 Bean 是一个 Singleton 的,所以一般情况下我们调用同一个 id 的 Bean 会是在内容地址相同的实例,当然在 Spring 配置文件中也可以配置非 Singleton。Destroy 过期自动清理阶段9. 当 Bean 不再需要时,会经过清理阶段,如果 Bean 实现了 DisposableBean 这个接口,会调用那个其实现的 destroy()方法;destroy-method 自配置清理10. 最后,如果这个 Bean 的 Spring 配置中配置了 destroy-method 属性,会自动调用其配置的销毁方法11. bean 标签有两个重要的属性(init-method 和 destroy-method)。用它们你可以自己定制初始化和注销方法。它们也有相应的注解(@PostConstruct 和@PreDestroy)。<bean id="" class="" init-method="初始化方法" destroy-method="销毁方法"> 6.1.7.6. Spring 依赖注入四种方式 构造器注入 /*带参数,方便利用构造器进行注入*/ public CatDaoImpl(String message){ this. message = message; } <bean id="CatDaoImpl" class="com.CatDaoImpl"> <constructor-arg value=" message "></constructor-arg> </bean>setter 方法注入public class Id { private int id; public int getId() { return id; } public void setId(int id) { this.id = id; } } <bean id="id" class="com.id "> <property name="id" value="123"></property> </bean> 静态工厂注入静态工厂顾名思义,就是通过调用静态工厂的方法来获取自己需要的对象,为了让 spring 管理所有对象,我们不能直接通过"工程类.静态方法()"来获取对象,而是依然通过 spring 注入的形式获取: public class DaoFactory { //静态工厂 public static final FactoryDao getStaticFactoryDaoImpl(){ return new StaticFacotryDaoImpl(); } } public class SpringAction { private FactoryDao staticFactoryDao; //注入对象 //注入对象的 set 方法 public void setStaticFactoryDao(FactoryDao staticFactoryDao) { this.staticFactoryDao = staticFactoryDao; } } //factory-method="getStaticFactoryDaoImpl"指定调用哪个工厂方法 <bean name="springAction" class=" SpringAction" > <!--使用静态工厂的方法注入对象,对应下面的配置文件--> <property name="staticFactoryDao" ref="staticFactoryDao"></property> </bean> <!--此处获取对象的方式是从工厂类中获取静态方法--> <bean name="staticFactoryDao" class="DaoFactory" factory-method="getStaticFactoryDaoImpl"></bean> 实例工厂实例工厂的意思是获取对象实例的方法不是静态的,所以你需要首先 new 工厂类,再调用普通的实例方法:public class DaoFactory { //实例工厂public FactoryDao getFactoryDaoImpl(){ return new FactoryDaoImpl(); } } public class SpringAction { private FactoryDao factoryDao; //注入对象 public void setFactoryDao(FactoryDao factoryDao) { this.factoryDao = factoryDao; } } <bean name="springAction" class="SpringAction"> <!--使用实例工厂的方法注入对象,对应下面的配置文件--> <property name="factoryDao" ref="factoryDao"></property> </bean> <!--此处获取对象的方式是从工厂类中获取实例方法--> <bean name="daoFactory" class="com.DaoFactory"></bean> <bean name="factoryDao" factory-bean="daoFactory"factory-method="getFactoryDaoImpl"></bean> 6.1.7.7. 5 种不同方式的自动装配Spring 装配包括手动装配和自动装配,手动装配是有基于 xml 装配、构造方法、setter 方法等自动装配有五种自动装配的方式,可以用来指导 Spring 容器用自动装配方式来进行依赖注入。1. no:默认的方式是不进行自动装配,通过显式设置 ref 属性来进行装配。2. byName:通过参数名 自动装配,Spring 容器在配置文件中发现 bean 的 autowire 属性被设置成 byname,之后容器试图匹配、装配和该 bean 的属性具有相同名字的 bean。3. byType:通过参数类型自动装配,Spring 容器在配置文件中发现 bean 的 autowire 属性被设置成 byType,之后容器试图匹配、装配和该 bean 的属性具有相同类型的 bean。如果有多个 bean 符合条件,则抛出错误。4. constructor:这个方式类似于 byType, 但是要提供给构造器参数,如果没有确定的带参数的构造器参数类型,将会抛出异常。5. autodetect:首先尝试使用 constructor 来自动装配,如果无法工作,则使用 byType 方式6.1.8. Spring AOP原理6.1.8.1. 概念"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。使用"横切"技术,AOP 把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。AOP 主要应用场景有:1. Authentication 权限2. Caching 缓存3. Context passing 内容传递4. Error handling 错误处理5. Lazy loading 懒加载6. Debugging 调试7. logging, tracing, profiling and monitoring 记录跟踪 优化 校准8. Performance optimization 性能优化9. Persistence 持久化10. Resource pooling 资源池11. Synchronization 同步12. Transactions 事务6.1.8.2. AOP 核心概念1、切面(aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象2、横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点。3、连接点(joinpoint):被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。4、切入点(pointcut):对连接点进行拦截的定义5、通知(advice):所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类。6、目标对象:代理的目标对象7、织入(weave):将切面应用到目标对象并导致代理对象创建的过程8、引入(introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段6.1.8.1. AOP 两种代理方式Spring 提供了两种方式来生成代理对象: JDKProxy 和 Cglib,具体使用哪种方式生成由AopProxyFactory 根据 AdvisedSupport 对象的配置来决定。默认的策略是如果目标类是接口,则使用 JDK 动态代理技术,否则使用 Cglib 来生成代理。JDK 动态接口代理1. JDK 动态代理主要涉及到 java.lang.reflect 包中的两个类:Proxy 和 InvocationHandler。InvocationHandler是一个接口,通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编制在一起。Proxy 利用 InvocationHandler 动态创建一个符合某一接口的实例,生成目标类的代理对象。CGLib 动态代理2. :CGLib 全称为 Code Generation Library,是一个强大的高性能,高质量的代码生成类库,可以在运行期扩展 Java 类与实现 Java 接口,CGLib 封装了 asm,可以再运行期动态生成新的 class。和 JDK 动态代理相比较:JDK 创建代理有一个限制,就是只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,则可以通过 CGLib 创建动态代理。 6.1.8.2. 实现原理 @Aspectpublic class TransactionDemo { @Pointcut(value="execution(* com.yangxin.core.service.*.*.*(..))") public void point(){ } @Before(value="point()") public void before(){ System.out.println("transaction begin"); } @AfterReturning(value = "point()") public void after(){ System.out.println("transaction commit"); } @Around("point()") public void around(ProceedingJoinPoint joinPoint) throws Throwable{ System.out.println("transaction begin");joinPoint.proceed();System.out.println("transaction commit");}}6.1.9. Spring MVC 原理Spring 的模型-视图-控制器(MVC)框架是围绕一个 DispatcherServlet 来设计的,这个 Servlet会把请求分发给各个处理器,并支持可配置的处理器映射、视图渲染、本地化、时区与主题渲染等,甚至还能支持文件上传。6.1.9.1. MVC 流程Http 请求到 DispatcherServlet(1) 客户端请求提交到 DispatcherServlet。HandlerMapping 寻找处理器(2) 由 DispatcherServlet 控制器查询一个或多个 HandlerMapping,找到处理请求的Controller。调用处理器 Controller(3) DispatcherServlet 将请求提交到 Controller。Controller 调用业务逻辑处理后,返回 ModelAndView(4)(5)调用业务处理和返回结果:Controller 调用业务逻辑处理后,返回 ModelAndView。DispatcherServlet 查询 ModelAndView(6)(7)处理视图映射并返回模型: DispatcherServlet 查询一个或多个 ViewResoler 视图解析器,找到 ModelAndView 指定的视图。ModelAndView 反馈浏览器 HTTP(8) Http 响应:视图负责将结果显示到客户端。6.1.9.1. MVC 常用注解6.1.10. Spring Boot 原理Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot 致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。其特点如下:1. 创建独立的 Spring 应用程序2. 嵌入的 Tomcat,无需部署 WAR 文件3. 简化 Maven 配置4. 自动配置 Spring5. 提供生产就绪型功能,如指标,健康检查和外部配置6. 绝对没有代码生成和对 XML 没有要求配置 [1]6.1.11. JPA 原理6.1.11.1. 事务事务是计算机应用中不可或缺的组件模型,它保证了用户操作的原子性 ( Atomicity )、一致性( Consistency )、隔离性 ( Isolation ) 和持久性 ( Durabilily )。6.1.11.2. 本地事务紧密依赖于底层资源管理器(例如数据库连接 ),事务处理局限在当前事务资源内。此种事务处理方式不存在对应用服务器的依赖,因而部署灵活却无法支持多数据源的分布式事务。在数据库连接中使用本地事务示例如下:public void transferAccount() { Connection conn = null; Statement stmt = null; try{ conn = getDataSource().getConnection(); // 将自动提交设置为 false,若设置为 true 则数据库将会把每一次数据更新认定为一个事务并自动提交conn.setAutoCommit(false);stmt = conn.createStatement(); // 将 A 账户中的金额减少 500stmt.execute("update t_account set amount = amount - 500 where account_id = 'A'");// 将 B 账户中的金额增加 500 stmt.execute("update t_account set amount = amount + 500 where account_id = 'B'");// 提交事务 conn.commit();// 事务提交:转账的两步操作同时成功} catch(SQLException sqle){ // 发生异常,回滚在本事务中的操做 conn.rollback();// 事务回滚:转账的两步操作完全撤销stmt.close(); conn.close(); } }6.1.11.1. 分布式事务Java 事务编程接口(JTA:Java Transaction API)和 Java 事务服务 (JTS;Java Transaction Service) 为 J2EE 平台提供了分布式事务服务。分布式事务(Distributed Transaction)包括事务管理器(Transaction Manager)和一个或多个支持 XA 协议的资源管理器 ( Resource Manager )。我们可以将资源管理器看做任意类型的持久化数据存储;事务管理器承担着所有事务参与单元的协调与控制。public void transferAccount() { UserTransaction userTx = null; Connection connA = null; Statement stmtA = null; Connection connB = null; Statement stmtB = null; try{ // 获得 Transaction 管理对象 userTx = (UserTransaction)getContext().lookup("java:comp/UserTransaction"); connA = getDataSourceA().getConnection();// 从数据库 A 中取得数据库连接connB = getDataSourceB().getConnection();// 从数据库 B 中取得数据库连接userTx.begin(); // 启动事务stmtA = connA.createStatement();// 将 A 账户中的金额减少 500 stmtA.execute("update t_account set amount = amount - 500 where account_id = 'A'");// 将 B 账户中的金额增加 500 stmtB = connB.createStatement(); stmtB.execute("update t_account set amount = amount + 500 where account_id = 'B'");userTx.commit();// 提交事务 // 事务提交:转账的两步操作同时成功(数据库 A 和数据库 B 中的数据被同时更新)} catch(SQLException sqle){ // 发生异常,回滚在本事务中的操纵userTx.rollback();// 事务回滚:数据库 A 和数据库 B 中的数据更新被同时撤销} catch(Exception ne){ } }6.1.11.1. 两阶段提交两阶段提交主要保证了分布式事务的原子性:即所有结点要么全做要么全不做,所谓的两个阶段是指:第一阶段:准备阶段;第二阶段:提交阶段。1 准备阶段事务协调者(事务管理器)给每个参与者(资源管理器)发送 Prepare 消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的 redo 和 undo 日志,但不提交,到达一种“万事俱备,只欠东风”的状态。2 提交阶段:如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)将提交分成两阶段进行的目的很明确,就是尽可能晚地提交事务,让事务在提交前尽可能地完成所有能完成的工作。6.1.12. Mybatis 缓存Mybatis 中有一级缓存和二级缓存,默认情况下一级缓存是开启的,而且是不能关闭的。一级缓存是指 SqlSession 级别的缓存,当在同一个 SqlSession 中进行相同的 SQL 语句查询时,第二次以后的查询不会从数据库查询,而是直接从缓存中获取,一级缓存最多缓存 1024 条 SQL。二级缓存是指可以跨 SqlSession 的缓存。是 mapper 级别的缓存,对于 mapper 级别的缓存不同的sqlsession 是可以共享的。6.1.12.1. Mybatis 的一级缓存原理(sqlsession 级别)第一次发出一个查询 sql,sql 查询结果写入 sqlsession 的一级缓存中,缓存使用的数据结构是一个 map。key:MapperID+offset+limit+Sql+所有的入参value:用户信息同一个 sqlsession 再次发出相同的 sql,就从缓存中取出数据。如果两次中间出现 commit 操作(修改、添加、删除),本 sqlsession 中的一级缓存区域全部清空,下次再去缓存中查询不到所以要从数据库查询,从数据库查询到再写入缓存。6.1.12.2. 二级缓存原理(mapper 基本)二级缓存的范围是 mapper 级别(mapper 同一个命名空间),mapper 以命名空间为单位创建缓存数据结构,结构是 map。mybatis 的二级缓存是通过 CacheExecutor 实现的。CacheExecutor其实是 Executor 的代理对象。所有的查询操作,在 CacheExecutor 中都会先匹配缓存中是否存在,不存在则查询数据库。key:MapperID+offset+limit+Sql+所有的入参具体使用需要配置:1. Mybatis 全局配置中启用二级缓存配置2. 在对应的 Mapper.xml 中配置 cache 节点3. 在对应的 select 查询节点中添加 useCache=true6.1.13. Tomcat 架构
python数据可视化大杀器之Seaborn详解
Python数据可视化个人主页:JoJo的数据分析历险记个人介绍:小编大四统计在读,目前保研到统计学top3高校继续攻读统计研究生如果文章对你有帮助,欢迎关注、点赞、收藏、订阅专栏本章主要介绍python的seaborn==数据可视化==的应用参考资料:Python数据可视化大杀器之地阶技法:matplotlib(含详细代码) https://github.com/fengdu78/Data-Science-Notes@TOCpython数据可视化大杀器之Seaborn详解一张好的图胜过一千个字,一个好的数据分析师必须学会用图说话。python作为数据分析最常用的工具之一,它的可视化功能也很强大,matplotlib和seaborn库使得绘图变得更加简单。本章主要介绍一下Searborn绘图。学过matplotlib的小伙伴们一定被各种参数弄得迷糊,而seaborn则避免了这些问题,废话少说,我们来看看seaborn具体是怎样使用的。Seaborn中概况起来可以分为五大类图1.关系类绘图2.分类型绘图3.分布图4.回归图5.矩阵图接下来我们一一讲解这些图形的应用,首先我们要导入一下基本的库%matplotlib inline
# 如果不添加这句,是无法直接在jupyter里看到图的
import seaborn as sns
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt如果上面报错的话需要安装相应的包pip install seaborn
pip install numpy
pip install pandas
pip install matplotlib我们可以使用set()设置一下seaborn的主题,一共有:darkgrid,whitegrid,dark,white,ticks,大家可以根据自己的喜好设置相应的主题,默认是darkgrid。我这里就设置darkgrid风格sns.set(style="darkgrid")接下来导入我们需要的数据集,seaborn和R语言ggplot2(感兴趣欢迎阅读我的R语言ggplot2专栏)一样有许多自带的样例数据集# 导入anscombe数据集
df = sns.load_dataset('anscombe')
# 观察一下数据集形式
df.head()
dataset
x
y
0
I
10.0
8.04
1
I
8.0
6.95
2
I
13.0
7.58
3
I
9.0
8.81
4
I
11.0
8.33
️1.关系图1.1 lineplot绘制线段seaborn里的lineplot函数所传数据必须为一个pandas数组,这一点跟matplotlib里有较大区别,并且一开始使用较为复杂,sns.lineplot里有几个参数值得注意。x: plot图的x轴labely: plot图的y轴labelci: 置信区间data: 所传入的pandas数组绘制时间序列图# 导入数据集
fmri = sns.load_dataset("fmri")
# 绘制不同地区不同时间 x和y的线性关系图
sns.lineplot(x="timepoint", y="signal",
hue="region", style="event",
data=fmri)<AxesSubplot:xlabel='timepoint', ylabel='signal'>
rs = np.random.RandomState(365)
values = rs.randn(365, 4).cumsum(axis=0)
dates = pd.date_range("1 1 2016", periods=365, freq="D")
data = pd.DataFrame(values, dates, columns=["A", "B", "C", "D"])
data = data.rolling(7).mean()
sns.lineplot(data=data, palette="tab10", linewidth=2.5)<AxesSubplot:>
1.2 relplot这是一个图形级别的函数,它用散点图和线图两种常用的手段来表现统计关系。# 导入数据集
dots = sns.load_dataset("dots")
sns.relplot(x="time", y="firing_rate",
hue="coherence", size="choice", col="align",
size_order=["T1", "T2"],
height=5, aspect=.75, facet_kws=dict(sharex=False),
kind="line", legend="full", data=dots)<seaborn.axisgrid.FacetGrid at 0x1d7d3634e50>
1.3 scatterplot(散点图)diamonds.head()
carat
cut
color
clarity
depth
table
price
x
y
z
0
0.23
Ideal
E
SI2
61.5
55.0
326.0
3.95
3.98
2.43
1
0.21
Premium
E
SI1
59.8
61.0
326.0
3.89
3.84
2.31
2
0.23
Good
E
VS1
56.9
65.0
327.0
4.05
4.07
2.31
3
0.29
Premium
I
VS2
62.4
58.0
334.0
4.20
4.23
2.63
4
0.31
Good
J
SI2
63.3
58.0
335.0
4.34
4.35
2.75
sns.set(style="whitegrid")
# Load the example iris dataset
diamonds = sns.load_dataset("diamonds")
# Draw a scatter plot while assigning point colors and sizes to different
# variables in the dataset
f, ax = plt.subplots(figsize=(6.5, 6.5))
sns.despine(f, left=True, bottom=True)
sns.scatterplot(x="depth", y="table",
data=diamonds, ax=ax)<AxesSubplot:xlabel='depth', ylabel='table'>
1.4 气泡图气泡图是在散点图的基础上,指定size参数,根据size参数的大小来绘制点的大小1.4.1 普通气泡图# 导入鸢尾花数据集
planets = sns.load_dataset("planets")
cmap = sns.cubehelix_palette(rot=-.2, as_cmap=True)
ax = sns.scatterplot(x="distance", y="orbital_period",
hue="year", size="mass",
palette=cmap, sizes=(10, 200),
data=planets)
1.4.2 彩色气泡图sns.set(style="white")
#加载示例mpg数据集
mpg = sns.load_dataset("mpg")
# 绘制气泡图
sns.relplot(x="horsepower", y="mpg", hue="origin", size="weight",
sizes=(40, 400), alpha=.5, palette="muted",
height=6, data=mpg)2. 分类型图表2.1 boxplot(箱线图)箱形图(Box-plot)又称为盒须图、盒式图或箱线图,是一种用作显示一组数据分散情况资料的统计图。它能显示出一组数据的最大值、最小值、中位数及上下四分位数。绘制分组箱线图# 导入数据集
tips = sns.load_dataset("tips")
# 绘制嵌套的箱线图,按日期和时间显示账单
sns.boxplot(x="day", y="total_bill",
hue="smoker", palette=["m", "g"],
data=tips)
sns.despine(offset=10, trim=True) 2.2 violinplot(小提琴图)violinplot与boxplot扮演类似的角色,它显示了定量数据在一个(或多个)分类变量的多个层次上的分布,这些分布可以进行比较。不像箱形图中所有绘图组件都对应于实际数据点,小提琴绘图以基础分布的核密度估计为特征。绘制简单的小提琴图# 生成模拟数据集
rs = np.random.RandomState(0)
n, p = 40, 8
d = rs.normal(0, 2, (n, p))
d += np.log(np.arange(1, p + 1)) * -5 + 10
# 使用cubehelix获得自定义的顺序调色板
pal = sns.cubehelix_palette(p, rot=-.5, dark=.3)
# 如何使用小提琴和圆点进行每种分布
sns.violinplot(data=d, palette=pal, inner="point")<AxesSubplot:>
绘制分组小提琴图tips = sns.load_dataset("tips")
# 绘制一个嵌套的小提琴图,并拆分小提琴以便于比较
sns.violinplot(x="day", y="total_bill", hue="smoker",
split=True, inner="quart",
palette={"Yes": "y", "No": "b"},
data=tips)
sns.despine(left=True)2.3 barplot(条形图)条形图表示数值变量与每个矩形高度的中心趋势的估计值,并使用误差线提供关于该估计值附近的不确定性的一些指示。绘制水平的条形图crashes = sns.load_dataset("car_crashes").sort_values("total", ascending=False)
# 初始化画布大小
f, ax = plt.subplots(figsize=(6, 15))
# 绘出总的交通事故
sns.set_color_codes("pastel")
sns.barplot(x="total", y="abbrev", data=crashes,
label="Total", color="b")
# 绘制涉及酒精的车祸
sns.set_color_codes("muted")
sns.barplot(x="alcohol", y="abbrev", data=crashes,
label="Alcohol-involved", color="b")
# 添加图例和轴标签
ax.legend(ncol=2, loc="lower right", frameon=True)
ax.set(xlim=(0, 24), ylabel="",
xlabel="Automobile collisions per billion miles")
sns.despine(left=True, bottom=True)绘制分组条形图titanic = sns.load_dataset("titanic")
# 绘制分组条形图
g = sns.barplot(x="class", y="survived", hue="sex", data=titanic,
palette="muted")2.4 pointplot(点图)点图代表散点图位置的数值变量的中心趋势估计,并使用误差线提供关于该估计的不确定性的一些指示。点图可能比条形图更有用于聚焦一个或多个分类变量的不同级别之间的比较。他们尤其善于表现交互作用:一个分类变量的层次之间的关系如何在第二个分类变量的层次之间变化。连接来自相同色调等级的每个点的线允许交互作用通过斜率的差异进行判断,这比对几组点或条的高度比较容易。sns.set(style="whitegrid")
iris = sns.load_dataset("iris")
# 将数据格式调整
iris = pd.melt(iris, "species", var_name="measurement")
# 初始化图形
f, ax = plt.subplots()
sns.despine(bottom=True, left=True)
sns.stripplot(x="value", y="measurement", hue="species",
data=iris, dodge=True, jitter=True,
alpha=.25, zorder=1)
# 显示条件平均数
sns.pointplot(x="value", y="measurement", hue="species",
data=iris, dodge=.532, join=False, palette="dark",
markers="d", scale=.75, ci=None)
# 图例设置
handles, labels = ax.get_legend_handles_labels()
ax.legend(handles[3:], labels[3:], title="species",
handletextpad=0, columnspacing=1,
loc="lower right", ncol=3, frameon=True) 可以看出各种鸢尾花四个特征的分布情况,以setosa为例,发现其petal_width值集中分布在0.2左右2.5 swarmplot能够显示分布密度的分类散点图sns.set(style="whitegrid", palette="muted")
# 加载数据集
iris = sns.load_dataset("iris")
# 处理数据集
iris = pd.melt(iris, "species", var_name="measurement")
# 绘制分类散点图
sns.swarmplot(x="measurement", y="value", hue="species",
palette=["r", "c", "y"], data=iris) 2.6 catplot(分类型图表的接口)可以通过指定kind参数分别绘制下列图形:stripplot() 分类散点图swarmplot() 能够显示分布密度的分类散点图boxplot() 箱图violinplot() 小提琴图boxenplot() 增强箱图pointplot() 点图barplot() 条形图countplot() 计数图3.分布图3.1 displot(单变量分布图)在seaborn中想要对单变量分布进行快速了解最方便的就是使用distplot()函数,默认情况下它将绘制一个直方图,并且可以同时画出核密度估计(KDE)图。具体用法如下:# 设置并排绘图,讲一个画布分为2*2,大小为7*7,X轴固定,通过ax参数指定绘图位置,可以看第六章具体怎么绘制多个图在一个画布中
f, axes = plt.subplots(2, 2, figsize=(7, 7), sharex=True)
sns.despine(left=True)
rs = np.random.RandomState(10)
# 生成随机数
d = rs.normal(size=100)
# 绘制简单的直方图,kde=False不绘制核密度估计图,下列其他图类似
sns.distplot(d, kde=False, color="b", ax=axes[0, 0])
# 绘制核密度估计图和地毯图
sns.distplot(d, hist=False, rug=True, color="r", ax=axes[0, 1])
# 绘制填充核密度估计图
sns.distplot(d, hist=False, color="g", kde_kws={"shade": True}, ax=axes[1, 0])
# 绘制直方图和核密度估计
sns.distplot(d, color="m", ax=axes[1, 1])
plt.setp(axes, yticks=[])
plt.tight_layout()3.2kdeplot(核密度估计图)核密度估计(kernel density estimation)是在统计学中用来估计未知分布的密度函数,属于非参数检验方法之一。通过核密度估计图可以比较直观的看出数据样本本身的分布特征。具体用法如下:简单的二维核密度估计图sns.set(style="dark")
rs = np.random.RandomState(50)
x, y = rs.randn(2, 50)
sns.kdeplot(x, y)
f.tight_layout()
多个核密度估计图sns.set(style="darkgrid")
iris = sns.load_dataset("iris")
# 按物种对iris数据集进行子集划分
setosa = iris.query("species == 'setosa'")
virginica = iris.query("species == 'virginica'")
f, ax = plt.subplots(figsize=(8, 8))
ax.set_aspect("equal")
# 画两个密度图
ax = sns.kdeplot(setosa.sepal_width, setosa.sepal_length,
cmap="Reds", shade=True, shade_lowest=False)
ax = sns.kdeplot(virginica.sepal_width, virginica.sepal_length,
cmap="Blues", shade=True, shade_lowest=False)
# 将标签添加到绘图中
red = sns.color_palette("Reds")[-2]
blue = sns.color_palette("Blues")[-2]
ax.text(2.5, 8.2, "virginica", size=16, color=blue)
ax.text(3.8, 4.5, "setosa", size=16, color=red) ☘️3.3绘制山脊图rs = np.random.RandomState(1979)
x = rs.randn(500)
g = np.tile(list("ABCDEFGHIJ"), 50)
df = pd.DataFrame(dict(x=x, g=g))
m = df.g.map(ord)
df["x"] += m
# 初始化FacetGrid对象
pal = sns.cubehelix_palette(10, rot=-.25, light=.7)
g = sns.FacetGrid(df, row="g", hue="g", aspect=15, height=.5, palette=pal)
# 画出密度
g.map(sns.kdeplot, "x", clip_on=Fals
"?e, shade=True, alpha=1, lw=1.5, bw=.2)
g.map(sns.kdeplot, "x", clip_on=False, color="w", lw=2, bw=.2)
g.map(plt.axhline, y=0, lw=2, clip_on=False)
# 定义并使用一个简单的函数在坐标轴中标记绘图
def label(x, color, label):
ax = plt.gca()
ax.text(0, .2, label, fontweight="bold", color=color,
ha="left", va="center", transform=ax.transAxes)
g.map(label, "x")
# 将子地块设置为重叠
g.fig.subplots_adjust(hspace=-.25)
# 删除与重叠不协调的轴
g.set_titles("")
g.set(yticks=[])
g.despine(bottom=True, left=True)<seaborn.axisgrid.FacetGrid at 0x1d7da3567c0>
3.4 joinplot(双变量关系分布图)用于绘制两个变量间分布图sns.set(style="white")
# 创建模拟数据集
rs = np.random.RandomState(5)
mean = [0, 0]
cov = [(1, .5), (.5, 1)]
x1, x2 = rs.multivariate_normal(mean, cov, 500).T
x1 = pd.Series(x1, name="$X_1$")
x2 = pd.Series(x2, name="$X_2$")
# 使用核密度估计显示联合分布
g = sns.jointplot(x1, x2, kind="kde", height=7, space=0)rs = np.random.RandomState(11)
x = rs.gamma(2, size=1000)
y = -.5 * x + rs.normal(size=1000)
sns.jointplot(x, y, kind="hex", color="#4CB391") tips = sns.load_dataset("tips")
g = sns.jointplot("total_bill", "tip", data=tips, kind="reg",
xlim=(0, 60), ylim=(0, 12), color="m", height=7)
3.5 pairplot(变量关系图)变量关系组图,绘制各变量之间散点图df = sns.load_dataset("iris")
sns.pairplot(df) 4. 回归图4.1 lmplotlmplot是用来绘制回归图的,通过lmplot我们可以直观地总览数据的内在关系,lmplot可以简单通过指定x,y,data绘制# 绘制整体数据的回归图
sns.lmplot(x='x',y='y',data=df)<seaborn.axisgrid.FacetGrid at 0x1d7cdfbec10>
# 使用分面绘图,根据dataset分面
sns.lmplot(x="x", y="y", col="dataset", hue="dataset", data=df,
col_wrap=2, ci=None) 上面显示了每一张图内画一个回归线,下面我们来看如何在一张图中画多个回归线# 加载鸢尾花数据集
iris = sns.load_dataset("iris")
g = sns.lmplot(x="sepal_length", y="sepal_width", hue="species",
truncate=True, height=5, data=iris)
# 使用truncate参数
# 设置坐标轴标签
g.set_axis_labels("Sepal length (mm)", "Sepal width (mm)")
<seaborn.axisgrid.FacetGrid at 0x1d7d16cea60>
可以看出setosa类型的鸢尾花主要集中在左侧,下面我们再来看一下怎么绘制logistic回归曲线# 加载 titanic dataset
df = sns.load_dataset("titanic")
# 显示不同性别年龄和是否存活的关系
g = sns.lmplot(x="age", y="survived", col="sex", hue="sex", data=df,
y_jitter=.02, logistic=True)
g.set(xlim=(0, 80), ylim=(-.05, 1.05)) 虽然仅仅使用一个变量来拟合logistic回归效果不好,但是为了方便演示,我们暂且这样做,从logistic回归曲线来看,男性随着年龄增长,存活率下降,而女性随着年龄上升,存活率上升4.2 residplot(残差图)线性回归残差图绘制现象回归得到的残差回归图sns.set(style="whitegrid")
# 模拟y对x的回归数据集
rs = np.random.RandomState(7)
x = rs.normal(2, 1, 75)
y = 2 + 1.5 * x + rs.normal(0, 2, 75)
# 绘制残差数据集,并拟合曲线
sns.residplot(x, y, lowess=True, color="g") 从结果来看,回归结果较好,这是因为我们的数据就是通过回归的形式生成的5.矩阵图5.1 heatmap(热力图)常见的我们使用热力图可以看数据表中多个变量间的相似度# 加载数据
flights_long = sns.load_dataset("flights")
# 绘制不同年份不同月份的乘客数量
flights = flights_long.pivot("month", "year", "passengers")
# 绘制热力图,并且在每个单元中添加一个数字
f, ax = plt.subplots(figsize=(9, 6))
sns.heatmap(flights, annot=True, fmt="d", linewidths=.5, ax=ax)绘制相关系数矩阵,绘制26个英文字母之间的相关系数矩阵from string import ascii_letters
sns.set(style="white")
# 随机数据集
rs = np.random.RandomState(33)
d = pd.DataFrame(data=rs.normal(size=(100, 26)),
columns=list(ascii_letters[26:]))
# 计算相关系数
corr = d.corr()
mask = np.zeros_like(corr, dtype=np.bool)
mask[np.triu_indices_from(mask)] = True
# 设置图形大小
f, ax = plt.subplots(figsize=(11, 9))
# 生成自定义颜色
cmap = sns.diverging_palette(220, 10, as_cmap=True)
# 绘制热力图
sns.heatmap(corr, mask=mask, cmap=cmap, vmax=.3, center=0,
square=True, linewidths=.5, cbar_kws={"shrink": .5}) 5.2 clustermap聚类图sns.set()
# 加载大脑网络示例数据集
df = sns.load_dataset("brain_networks", header=[0, 1, 2], index_col=0)
# 选择网络的子集
used_networks = [1, 5, 6, 7, 8, 12, 13, 17]
used_columns = (df.columns.get_level_values("network")
.astype(int)
.isin(used_networks))
df = df.loc[:, used_columns]
# 创建一个分类调色板来识别网络
network_pal = sns.husl_palette(8, s=.45)
network_lut = dict(zip(map(str, used_networks), network_pal))
# 将调色板转换为将在矩阵侧面绘制的向量
networks = df.columns.get_level_values("network")
network_colors = pd.Series(networks, index=df.columns).map(network_lut)
# 画出完整的聚类图
sns.clustermap(df.corr(), center=0, cmap="vlag",
row_colors=network_colors, col_colors=network_colors,
linewidths=.75, figsize=(13, 13))
✏️6.FacetGrid绘制多个图表是一个绘制多个图表(以网格形式显示)的接口。步骤:1、实例化对象2、map,映射到具体的 seaborn 图表类型3、添加图例✒️6.1 绘制多个直方图sns.set(style="darkgrid")
tips = sns.load_dataset("tips")
g = sns.FacetGrid(tips, row="sex", col="time", margin_titles=True)
bins = np.linspace(0, 60, 13)
g.map(plt.hist, "total_bill", color="steelblue", bins=bins) ️6.2 绘制多个折线图sns.set(style="ticks")
# 创建一个包含许多短随机游动的数据集
rs = np.random.RandomState(4)
pos = rs.randint(-1, 2, (20, 5)).cumsum(axis=1)
pos -= pos[:, 0, np.newaxis]
step = np.tile(range(5), 20)
walk = np.repeat(range(20), 5)
df = pd.DataFrame(np.c_[pos.flat, step, walk],
columns=["position", "step", "walk"])
# 为每一次行走初始化一个带有轴的网格
grid = sns.FacetGrid(df, col="walk", hue="walk", palette="tab20c",
col_wrap=4, height=1.5)
# 画一条水平线以显示起点
grid.map(plt.axhline, y=0, ls=":", c=".5")
# 画一个直线图来显示每个随机行走的轨迹
grid.map(plt.plot, "step", "position", marker="o")
# 调整刻度位置和标签
grid.set(xticks=np.arange(5), yticks=[-3, 3],
xlim=(-.5, 4.5), ylim=(-3.5, 3.5))
# 调整图形的布局
grid.fig.tight_layout(w_pad=1) 文章推荐如果想了解更多数据可视化技巧,欢迎访问下列文章玩转数据可视化之R语言ggplot2:(三)ggplot2实现将多张图放在一起,包括并排和插图绘制(快速入门)玩转数据可视化之R语言ggplot2::(四)单一基础几何图形绘制玩转数据可视化之R语言ggplot2:(五)分组画图☀️玩转数据可视化之R语言ggplot2:(六)统计变换绘图:包括加权绘图、数据分布图、曲面图、图形重叠处理等最近小伙伴问我有什么刷题网站推荐,在这里推荐一下牛客网,里面包含各种面经题库,全是免费的题库,可以全方面提升你的职业竞争力,提升编程实战技巧,赶快来和我一起刷题吧!牛客网链接|python篇
如何优雅的消除系统重复代码
引言很多同学在工作一段时间之后可能都有这样的困境,大家觉得自己总是在写业务代码,技术上感觉好像没有多大的长进,不知不觉就成为了CURD Boy或者Girl,自己想要去改变但是又不知道该从何处进行入手。有的同学会去学习如何做架构、有的同学可能会去学习各种新技术还有的同学甚至转产品经理来试图解除困境。但是我觉得找到跨出这种困境的途径反而还是要从我们每天写的代码入手。即便当前每天做着CRUD的事情,但是我们自己不能把自己定义为只会CURD的工具人。那么我们到底如何从代码层面入手改变困境呢?我们可以回过头看看自己以前写的代码,或者是当前正在实现的各种各样的需求,反问自己以下5个问题。1、有没有使用设计模式优化代码结构?2、有没有利用一些高级特性来简化代码实现?3、有没有借助框架的能力来扩展应用能力?4、自己设计的业务模型够不够抽象?5、代码扩展性强不强,需求如果有变化模块代码能不能做到最小化修改?通过这样的反问和思考,我们可以不断自我审视自己写的代码。通过在代码上的深耕细作,我们所负责的模块的质量就会比别人更高,出现Bug的概率就会更低,稳定性就会更高,那么未来负责更多业务模块的机会也就会更多,只有这样我们才能真正跨出困境,实现突破。因此本文主要从优化日常工作中经常遇到的重复代码入手,和大家探讨下如何通过一些技巧来消除平台中的重复代码,以消除系统中的重复代码为切入点,提升系统稳定性。为什么要消除重复代码在程序猿的日常工作中,不仅要跟随业务侧的发展不断开发新的需求,同时也需要维护老的已有平台。无论是开发新需求还是维护老系统,我们都会遇到同样一个问题,系统中总是充斥着很多重复的代码。可能是由于工期很赶没时间优化,也有可能是历史原因欠下的技术债。无论是什么原因,系统中大量的重复代码非常影响平台整体的可维护性。大神们的谆谆教导Don’t Repeat Yourself 言犹在耳。那么平台中的重复代码会带来怎样的稳定性风险呢?系统维护成本高如果项目中出现大量的重复代码,说明系统中这部分业务逻辑并没有进行很好的抽象,因此会导致后期的代码维护面临很多问题。无论是修改原有逻辑还是新增业务逻辑可能需要在不同的文件中进行修改,项目维护成本相当高。另外后期维护的同学看到同样的逻辑写了多遍,不明白这到底是代码的坏味道还是有什么特殊的业务考虑,这也在无形中增加了后期维护者的代码逻辑理解难度。程序Bug概率高大家都知道重复代码意味着业务逻辑相同或者相似,假如这些相同或者相似的代码出现了Bug,在修复的过程中就需要修改很多地方,导致一次上线变更的内容比较多,存在一定的风险,毕竟线上问题70%-80%都是由于新的变更引起的。另外如果重复的地方比较多,很有可能出现漏改的情况。因此重复的代码实际就是隐藏在工程中的老炸弹,可能一直相安无事,也可能不知道什么时候就会Bom一声给你惊喜,因此我们必须要进行重复代码消除。如何优雅的消除重复代码在消除重复代码之前,我们首先需要确定到底什么是重复代码,或者说重复代码的特征到底是什么。有的同学可能会说,这还不简单嘛,重复代码不就是那些一模一样的但是散落在工程不同地方的代码嘛。当然这句话也没错,但是不够全面,重复代码不仅仅指那些不同文件中的完全相同的代码,还有一些代码业务流程相似但是并不是完全相同,这类代码我们也把它称之为重复代码。重复代码的几个特性:1、代码结构完全相同比如工程中好几个地方都有读取配置文件的逻辑,代码都是相同的,那么我们可以把不同地方读取配置文件的逻辑放到一个工具类中,这样今后再有读取配置文件的需要的时候可以直接调用工具类中方法即可,不需要再重复写相同的代码,这也是我们日常工作中最常见的使用方式。2、代码逻辑结构相似在项目中经常遇到虽然代码并不是完全相同,但是逻辑结构却非常相似。比如电商平台在进行营销活动的时候,常常通过邀请的方式来进行用户红包领取的活动,但是对于新老用户的红包赠予规则是不同的,同时也会根据邀请用户的数量的不同给予不同的红包优惠。但是无论新老用户都会经历根据用户类型获取红包计算规则,根据规则计算减免的红包,最后付款的时候减去红包数额这样一个业务逻辑。虽然表面看上去代码并不相同,但是实际上逻辑基本是一样的,因此也属于重复代码。下面就和大家分享几种比较实用的消除重复的代码的技巧,考虑到安全性,代码都进行了脱敏以及简化处理。统一参数校验当我们进行项目开发的时候,会编写一些类的实现方法,不可避免的会进行一些参数校验或者业务规则校验,因此会在实现方法中写一些判断参数是否有效或者返回结果是否有效的的的代码。public OrderDTO queryOrderById(String id) {
if(StringUtils.isEmpty(id)) {
return null;
}
OrderDTO order = orderBizService.queryOrder(id);
if(Objects.isNull(Order)) {
return null;
}
...
}
public List<UserDTO> queryUsersByType(List<String> type) {
if(StringUtils.isEmpty(id)) {
return null;
}
...
}这种参数校验的方式,很多人会喜欢使用@Valid这种注解来进行参数有效性的判断,但是我觉得还是不够方便,它只能进行一些参数的校验,并不能进行业务结果的有效性判断。那么对于这种校验类的代码如何才能消除重复if...else...判断代码呢?因此我一般会统一定义一个Assert断言来进行参数或者业务结果的校验,当然也可以使用Spring框架提供的Assert抽象类来进行判断,但是它抛出的异常是IllegalArgumentException,我习惯抛出自己定义的全局统一的异常信息,这样可以通过全局的异常处理类来进行统一处理。因此我们首先定义一个业务断言类,主要针对biz层出现的参数以及业务结果进行断言,这样可以避免重复写if...else...判断代码。public class Assert {
public static void notEmpty(String param) {
if(StringUtils.isEmpty(param)) {
throw new BizException(ErrorCodes.PARAMETER_IS_NULL, "param is empty or null");
}
}
public static void notNull(Object o) {
if (Objects.isNull(o)) {
throw new BizException(ErrorCodes.PARAMETER_IS_NULL, "object is null");
}
}
public static void notEmpty(Collection collection) {
if(CollectionUtils.isEmpty(collection)) {
throw new BizException(ErrorCodes.PARAMETER_IS_NULL, "collection is empty or null");
}
}
}我们看下优化后的代码是不是看上去清爽许多。public OrderDTO queryOrderById(String id) {
Assert.notEmpty(id);
OrderDTO order = orderBizService.queryOrder(id);
Assert.notNull(order);
...
}
public List<UserDTO> queryUsersByType(List<String> type) {
Assert.notEmpty(type);
...
}统一异常处理以下这类Controller代码在项目中是不是很常见?大家可以翻翻自己的项目工程代码,可能很多工程中Cotroller层都充斥着这样的try{}catch{}逻辑处理,相当于每个接口实现都要进行异常处理,看起来非常冗余写起来也麻烦。实际上我们可以通过定义统一的全局异常处理器来进行优化,避免重复的进行异常捕获。@GetMapping("list")
public ResponseResult<OrderVO> getOrderList(@RequestParam("id")String userId) {
try {
OrderVO orderVo = orderBizService.queryOrder(userId);
return ResponseResultBuilder.buildSuccessResponese(orderDTO);
} catch (BizException be) {
// 捕捉业务异常
return ResponseResultBuilder.buildErrorResponse(be.getCode, be.getMessage());
} catch (Exception e) {
// 捕捉兜底异常
return ResponseResultBuilder.buildErrorResponse(e.getMessage());
}
}那么我们应该怎么优化这些重复的异常捕捉处理代码呢?首先我们需要定义一个统一的异常处理器,通过它来对Controller接口的异常进行统一的异常处理,包括异常捕获以及异常信息提示等等。这样就不用在每个实现接口中编写try{}catch{}异常处理逻辑了。示意代码只是简单的说明实现方法,在项目中进行落地的时候,大家可以定义处理更多的异常类型。@ControllerAdvice
@ResponseBody
public class UnifiedException {
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(BizException.class)
@ResponseBody
public ResponseResult handlerBizException(BizException bizexception) {
return ResponseResultBuilder.buildErrorResponseResult(bizexception.getCode(), bizexception.getMessage());
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseResult handlerException(Exception ex) {
return ResponseResultBuilder.buildErrorResponseResult(ex.getMessage());
}
}优化后的Controller如下所示,大量的try...catch...不见了,代码结构变得更加清晰直接。@GetMapping("list")
public ResponseResult<OrderVO> getOrderList(@RequestParam("id")String userId) {
List<OrderVO> orderVo = orderBizService.queryOrder(userId);
return ResponseResultBuilder.buildSuccessResponese(orderVo);
}优雅的属性拷贝在实际的项目开发中我们所开发的微服务都是分层的有的是MVC三层,有的按照DDD领域分层是四层。无论是三层还是四层都会涉及不同层级的之间的调用,而每个层级都有自己的数据对象模型,比如biz层是dto,domain层是model,repo层是po。因此必然会涉及到数据模型对象之间的相关转换。在一些场景下模型之间的字段很多都是一样的,有的甚至是完全一模一样。比如将DTO转化为业务模型Model,实际上他们之间很多的字段都是一样的,所以经常会出现以下的这种代码,会出现大量的属性赋值 的操作来达到模型转换的需求。实际上我们可以通过一些工具包或者工具类进行属性的拷贝,避免出现大量的重复赋值代码。public class TaskConverter {
public static TaskDTO taskModel2DTO(TaskModel taskModel) {
TaskDTO taskDTO = new TaskDTO();
taskDTO.setId(taskModel.getId());
taskDTO.setName(taskModel.getName());
taskDTO.setType(taskModel.getType());
taskDTO.setContent(taskModel.getContent());
taskDTO.setStartTime(taskModel.getStartTime());
taskDTO.setEndTime(taskModel.getEndTime());
return taskDTO;
}
}使用BeanUtils的进行属性赋值,很明显不再有那又长又没有感情的一条又一条的属性赋值语句了,整个任务数据模型对象的转换代码看上去立马舒服很多。public class TaskConverter {
public static TaskDTO taskModel2DTO(TaskModel taskModel) {
TaskDTO taskDTO = new TaskDTO();
BeanUtils.copyProperties(taskModel, taskDTO);
return taskDTO;
}
}当然很多人会说,BeanUtils会存在深拷贝的问题。但是在一些浅拷贝的场景下使用起来还是比较方便的。另外还有Mapstruct工具,大家也可以试用一下。核心能力抽象假设有这样的业务场景,系统中需要根据不同的用户类型计算商品结算金额,大致的计算逻辑有三个步骤,分别是计算用户商品总价格,计算不同用户对应的优惠金额,最后计算出用户的结算金额。我们先来看下原有系统中的实现方式。普通用户结算逻辑:public Class NormalUserSettlement {
//省略代码
...
public Bigdecimal calculate(String userId) {
//计算商品总价格
List<Goods> goods = shoppingService.queryGoodsById(userId);
Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);
//计算优惠
Bigdecimal discount = total.multiply(new Bigdecimal(0.1));
//计算应付金额
Bigdecimal payPrice = total - dicount;
return payPrice;
}
//省略代码
...
}VIP用户结算逻辑:public Class VIPUserSettlement {
//省略代码
...
public Bigdecimal calculate(String userId) {
//计算商品总价格
List<Goods> goods = shoppingService.queryGoodsById(userId);
Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);
//计算优惠
Bigdecimal discount = total.multiply(new Bigdecimal(0.2));
//计算应付金额
Bigdecimal payPrice = total - dicount;
return payPrice;
}
//省略代码
...
}黑卡用户结算逻辑:public Class VIPUserSettlement {
//省略代码
...
public Bigdecimal calculate(String userId) {
//计算商品总价格
List<Goods> goods = shoppingService.queryGoodsById(userId);
Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);
//计算优惠
Bigdecimal discount = total.multiply(new Bigdecimal(0.2));
//计算应付金额
Bigdecimal payPrice = total - dicount;
return payPrice;
}
//省略代码
...
}在这样的场景下,我们可以发现,在三个类中计算商品总额以及计算最后的应付金额逻辑都是一样的,唯一不同的是每个用户类型对应的优惠金额是不同的。因此我们可以把逻辑相同的部分抽象到AbstractSettleMent中,然后定义计算优惠金额的抽象方法由各个不同的用类型子类去实现。这样各个子类只要关心自己的优惠实现就可以了,重复的代码都被抽象复用大大减少重复代码的使用。public Class AbstractSettlement {
//省略代码
...
public abstact Bigdecimal calculateDiscount();
public Bigdecimal calculate(String userId) {
//计算商品总价格
List<Goods> goods = shoppingService.queryGoodsById(userId);
Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);
//计算优惠
Bigdecimal discount = calculateDiscount();
//计算应付金额
Bigdecimal payPrice = total - dicount;
return payPrice;
}
//省略代码
...
}自定义注解和AOP用过Spring框架的同学都知道,AOP是Spring框架核心特性之一,它不仅是一种编程思想更是实际项目中可以落地的技术实现技巧。通过自定义注解和AOP的组合使用,可以实现一些通用能力的抽象。比如很多接口都需要进行鉴权、日志记录或者执行时间统计等操作,但是如果在每个接口中都编写鉴权或者日志记录的代码那就很容易产生很多重复代码,在项目后期不好维护。针对这种场景 我们可以使用AOP同时结合自定义注解实现接口的切面编程,在需要进行通用逻辑处理的接口或者类中增加对应的注解即可。假设有这样的业务场景,需要计算指定某些接口的耗时情况,一般的做法是在每个接口中都加上计算接口耗时的逻辑,这样各个接口中就会有这样重复计算耗时的逻辑,重复代码就这样产生了。那么通过自定义注解和AOP的方式可以轻松地解决代码重复的问题。首先定义一个注解,用于需要统计接口耗时的接口方法上。@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeCost {
}定义切面实现类:@Aspect
@Component
public class CostTimeAspect {
@Pointcut(value = "@annotation(com.mufeng.eshop.anotation.CostTime)")
public void costTime(){ }
@Around("runTime()")
public Object costTimeAround(ProceedingJoinPoint joinPoint) {
Object obj = null;
try {
long beginTime = System.currentTimeMillis();
obj = joinPoint.proceed();
//获取方法名称
String method = joinPoint.getSignature().getName();
//获取类名称
String class = joinPoint.getSignature().getDeclaringTypeName();
//计算耗时
long cost = System.currentTimeMillis() - beginTime;
log.info("类:[{}],方法:[{}] 接口耗时:[{}]", class, method, cost + "毫秒");
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return obj;
}
}优化前的代码:@GetMapping("/list")
public ResponseResult<List<OrderVO>> getOrderList(@RequestParam("id")String userId) {
long beginTime = System.currentTimeMillis();
List<OrderVO> orderVo = orderBizService.queryOrder(userId);
log.info("getOrderList耗时:" + System.currentTimeMillis() - beginTime + "毫秒");
return ResponseResultBuilder.buildSuccessResponese(orderVo);
}
@GetMapping("/item")
public ResponseResult<OrderVO> getOrderById(@RequestParam("id")String orderId) {
long beginTime = System.currentTimeMillis();
OrderVO orderVo = orderBizService.queryOrderById(orderId);
log.info("getOrderById耗时:" + System.currentTimeMillis() - beginTime + "毫秒");
return ResponseResultBuilder.buildSuccessResponese(orderVo);
}优化后的代码:@GetMapping("/list")
@TimeCost
public ResponseResult<List<OrderVO>> getOrderList(@RequestParam("id")String userId) {
List<OrderVO> orderVo = orderBizService.queryOrder(userId);
return ResponseResultBuilder.buildSuccessResponese(orderVo);
}
@GetMapping("/item")
@TimeCost
public ResponseResult<OrderVO> getOrderList(@RequestParam("id")String orderId) {
OrderVO orderVo = orderBizService.queryOrderById(orderId);
return ResponseResultBuilder.buildSuccessResponese(orderVo);
}引入规则引擎大家在做业务开发的时候,可能会遇到这样的场景,业务中充斥着各种各样的规则判断,同时这些业务规则还可能经常发生变化。即便是我们用了策略模式等设计模式来优化代码结构,但是还是不能避免代码中出现大量的if...else...判断代码,一旦增加或者修改规则都需要在原来的业务规则代码中进行修改,维护起来非常不方便。假设设有这样的业务,销售人员的奖励根据实际的利润进行计算,不同的利润计算奖励的规则并不相同。使用规则引擎之前,可能会有这样的代码结构,需要根据实际利润所处的区间来计算最终的奖励金额,不同区间范围对应的返点规则是不一样的,因此会有很多的if...else...判断。另外规则有可能随着业务的发展还会经常变化,因此后期可能面临不断修改这部分的计算奖励的代码的情况。public double calculate(int profit) {
if(profit < 1000) {
return profit * 0.1;
} else if(1000 < profit && profit< 2000) {
return profit * 0.15;
} else if(2000 < profit && profit < 3000) {
return profit * 0.2;
}
return profit * 0.3;
}如果遇到这种业务场景,我们就可以考虑使用规则引擎。通过引入规则引擎,我们可以实现业务代码与业务规则相分离,将各种业务判断规则从原有的平台代码中抽离出来,以后规则的修改都在规则文件中直接修改就可以了,避免代码本身的变更,从而大大提升代码的扩展性。这里简单介绍下常用的规则引擎Drools是如何实现规则扩展管理的。使用Drools之后:使用规则引擎优化之后,所有的规则也就是所有的if...else...都会放在规则文件reward.drl中,因此代码中不会再有各种重复的if...else...代码,真正实现了业务规则与业务数据相分离。// 奖励规则
package reward.rule
import com.mufeng.eshop.biz.Reward
// rule1:如果利润小于1000,则奖励计算规则为profit*0.1
rule "reward_rule_1"
when
$reward: Reward(profit < 1000)
then
$reward.setReward($reward.getProfit() * 0.1);
System.out.println("匹配规则1,奖励为利润的1成");
end
// rule2:如果利润大于1000小于2000,则奖励计算规则为profit*0.15
rule "reward_rule_2"
when
$reward: Reward(profit >= 1000 && profit < 2000)
then
$reward.setReward($reward.getProfit() * 0.15);
System.out.println("匹配规则2,奖励为利润的1.5成");
end
// rule3:如果利润大于2000小于3000,则奖励计算规则为profit*0.2
rule "reward_rule_3"
when
$order: Order(profit >= 2000 && profit < 3000)
then
$reward.setReward($reward.getProfit() * 0.2);
System.out.println("匹配规则3,奖励为利润的2成");
end
// rule4:如果利润大于等于3000,则奖励计算规则为profit*0.3
rule "reward_rule_4"
when
$order: Order(profit >= 3000)
then
$reward.setReward($reward.getProfit() * 0.3);
System.out.println("匹配规则4,奖励为利润的3成");
end在代码中只要将待判断的数据插入到规则引擎的工作内存中,然后执行规则就可以获取到最终的结果,是不是很方便的实现业务规则的解耦,在实际的Java代码中也不用看到各种if...else...判断。定义规则引擎实现:public class DroolsEngine {
private KieHelper kieHelper;
public DroolsEngine() {
this.kieHelper = new KieHelper();
}
public void executeRule(String rule, Object unit, boolean clear) {
kieHelper.addContent(rule, ResourceType.DRL);
KieSession kieSession = kieHelper.getKieContainer().newKieSession();
//插入判断实体
kieSession.insert(unit);
//执行规则
kieSession.fireAllRules();
if (clear) {
kieSession.dispose();
}
}
}public class Profit {
public double calculateReward(Reward reward) {
String rule = "classpath:rules/reward.drl";
File rewardFile = new File(rule);
String rewardDrl = FileUtils.readFile(rewardFile, "utf-8");
DroolsEngine engine = new DroolsEngine();
engine.executeRule(rewardDrl, reward, true);
return reward.getReward();
}
}通过引入Drools规则引擎,代码中不再有各种规则判断的重复的if...else...判断语句,而且如果后期要修改奖励规则,代码不用修改,直接更改规则即可,系统的扩展性以及可维护性进一步提升。消除重复代码方法论上文中给大家介绍了几种消除重复代码的实战小技巧,不知道大家有没有发现虽然具体落地实操的手段各不相同,无论是提取公用逻辑作为工具类、使用AOP进行面向切面编程还是进行通用逻辑抽象,又或者是借助规则引擎分离实现与规则。实际它们的核心思想本质上都是一致的,都是通过抽离或者抽象相似代码逻辑后进行统一处理。将这种核心思想放在微服务内部就是在系统中的消除重复业务逻辑,如果放在架构层面来看其实和中台思想的本质也是相通的,将用户、支付这种各个平台都会用到的服务抽象为中台,实际就是一种混乱到有序的软件复杂度治理过程以及一种万物归一的思想。那么在日常的实际项目中我们应该怎么落地实践消除重复代码呢?这里总结了通过上述文章对于重复代码的处理,我们来试图来提炼消除重复代码的方法论。Find:技术同学需要有一双可以发现重复代码的眼睛,能够将表面上的重复我代码以及隐藏的重复代码识别出来。重复代码不仅仅是表示长得一模一样的代码,那些核心业务逻辑一样实际也是一种重复代码。Analysis:当我们找到了重复代码之后,就要考虑该如何进行优化了,如果只是工具类型的重复代码,那么直接提取作为一个工具类就可以了,也不用考虑太多。但是如果是涉及业务流程可能需要进一步的进行抽象。Action:根据不同的重复代码的类型,我们需要制定不通过的优化重复代码的方案。根据不同的方案实现通过引入规则引擎还是模板方法进行抽象。总结不知不觉又到凌晨12点了,每次在这种夜深人静的时候写文章,也是自己最享受的时光。白天工作很忙,晚上又是各种加班,每天能留给自己的时间真的是少之又少。在睡觉前的这一个小时左右的时间,能够将自己的总结和思考沉淀下来其实一件非常值得开心的事情,如果可以给看到文章的同学一点点启发,那更是善莫大焉。今天和大家主要分享了几种项目中消除重复代码的实践方案,同时沉淀了如何优雅消除代码重复的方法论,希望通过这样的沉淀以及总结可以在大家遇到同样的问题的时候可以有所帮助,通过实际的优化代码落地来提升平台的可维护性。大家有没有在项目中实战过的其他消灭重复代码的实践案例呢?欢迎一起分享讨论交流哦。
实现SpringBoot项目的多数据源配置的两种方式(dynamic-datasource-spring-boot-starter和自定义注解的方式)
1. 简介最近项目需要配置多数据源,本项目采用的技术是SpringBoot+mybatis-plus+Druid。为了图个方便直接想直接集成dynamic-datasource-spring-boot-starter进行多数据源配置。dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。其支持 Jdk 1.7+, SpringBoot 1.4.x 1.5.x 2.x.x。其官方文档的地址是:https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611该官方文档分为免费部分和付费部分。付费部分也仅仅只需要29块,29块也不多,就算原作者的支持,个人觉得这29块花的值。强烈建议使用最新版本,可以在版本记录里查找最新版本前提这里默认你已经集成并配置好了mybatis-plus。集成(第一种实现方式)仅仅只看基础部分的集成手册是远远不够的。网上好多博客也仅仅只是介绍了基础部分的内容,最终还是达不到想要的效果。本文的集成亲测有效,欢迎读者老爷们阅读。这里再次强烈建议采用最新版本的dynamic-datasource-spring-boot-starter,具体的版本记录请点击1. 添加依赖<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>2. 添加数据源配置在application.yml文件中将单数据源配置成多数据源,数据源配置的语法结构如下所示:spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
slave_1:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave_2:
url: ENC(xxxxx) # 内置加密,使用请查看详细文档
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.jdbc.Driver
#......省略
#以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2此处我的配置实例是:spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master :
url: jdbc:mysql://127.0.0.1:23306/db1?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL&autoReconnect=true
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
slave:
url: jdbc:mysql://127.0.0.1:23306/db2?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL&autoReconnect=true
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver3. 使用 @DS 切换数据源。@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。注解结果没有@DS使用默认数据源@DS(“dsName”)dsName可以为组名也可以为具体某个库的名称官方文档里配置到这里就结束了,实际上还远远不够。4. 排除掉DruidDataSourceAutoConfigure在启动类中需要排除掉DruidDataSourceAutoConfigure.class,就是取消Druid的数据源的自动配置类。@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DruidDataSourceAutoConfigure.class})
@MapperScan(basePackages = {"com.jay.multidatasource.mapper"})
public class MultidatasourceApplication {
public static void main(String[] args) {
SpringApplication.run(MultidatasourceApplication.class, args);
}
}原理解释(第二种实现方式)多数据源的配置本质上就是加载多个数据源,并设置默认数据源,给每个数据源设置不同的键值对,当需要切换数据源时就传入目标数据源的键,然后重新设置数据源。下面就做一个简单的演示,就是不使用dynamic-datasource-spring-boot-starter。1. 定义数据源配置在application.yml文件中将单数据源配置成多数据源spring:
datasource:
druid:
db1:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:23306/db1?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL&autoReconnect=true
username: root
password: 123456
db2:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:23306/db2?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL&autoReconnect=true
username: root
password: 123456
test-on-borrow: true2. 定义全局的数据源构造类DynamicDataSourceContextHolder这个类的作用就是管理每个数据源的键,设置当前数据源的键,获取当前数据源的键。public class DynamicDataSourceContextHolder {
private static ThreadLocal<Object> CONTEXT_HOLDER = ThreadLocal.withInitial(() -> DataSourceKey.DCS.getName());
public static List<Object> dataSourceKeys = new ArrayList<Object>();
public static void setDataSourceKey(String key){
CONTEXT_HOLDER.set(key);
}
public static Object getDataSourceKey(){
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceKey(){
CONTEXT_HOLDER.remove();
}
public static Boolean containDataSourceKey(String key){
return dataSourceKeys.contains(key);
}
}2. 自定义DynamicRoutingDataSource/**
* 该类继承自 AbstractRoutingDataSource 类,
* 在访问数据库时会调用该类的 determineCurrentLookupKey() 方法获取数据库实例的 key
*/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
logger.info("current datasource is : {}", DynamicDataSourceContextHolder.getDataSourceKey());
return DynamicDataSourceContextHolder.getDataSourceKey();
}
}3. 定义数据源配置类该类的作用就是初始化数据源DataSource实例,以及初始化SqlSessionFactory实例。这里需要注意的是必须使用MybatisSqlSessionFactoryBean来获取会话工厂SqlSessionFactory,不然的话,baseMapper中的生成动态SQL的方法就不能使用了。@Configuration
public class DataSourceConfigurer {
/**
* 配置数据源
*
* @return
*/
@Bean(name = "db1")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.druid.db1")
public DataSource db1() {
return DruidDataSourceBuilder.create().build();
}
/**
* 配置数据源
*
* @return
*/
@Bean(name = "db2")
@ConfigurationProperties(prefix = "spring.datasource.druid.db2")
public DataSource db2() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<Object, Object>(2);
dataSourceMap.put("db1", db1());
dataSourceMap.put("db2", db2());
dynamicRoutingDataSource.setDefaultTargetDataSource(dcs());
dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
DynamicDataSourceContextHolder.dataSourceKeys.addAll(dataSourceMap.keySet());
return dynamicRoutingDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
//MybatisPlus使用的是MybatisSqlSessionFactory
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
//此处设置为了解决找不到mapper文件的问题
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory());
}
/**
* 事务
*
* @return
*/
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}4. 自定义注解TargetDataSource该注解只是作用在方法上,这里默认的数据源是db1.@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
String value() default "db1";
}5. 定义切面DynamicDataSourceAspect切面顾名思义就是拦击标注TargetDataSource注解的方法,并且根据注解指定的数据源的key切换数据源。@Aspect
@Component
public class DynamicDataSourceAspect {
private Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
@Before("@annotation(targetDataSource))")
public void switchDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) {
if (!DynamicDataSourceContextHolder.containDataSourceKey(targetDataSource.value().getName())) {
logger.error("DataSource [{}] doesn't exist, use default DataSource [{}]", targetDataSource.value());
} else {
DynamicDataSourceContextHolder.setDataSourceKey(targetDataSource.value().getName());
logger.info("Switch DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.getDataSourceKey(), joinPoint.getSignature());
}
}
@After("@annotation(targetDataSource))")
public void restoreDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) {
DynamicDataSourceContextHolder.clearDataSourceKey();
logger.info("Restore DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.getDataSourceKey(), joinPoint.getSignature());
}
}6. 使用注解没有添加注解的方法使用的是默认数据源,当需要使用非默认数据源时,则需要在方法上添加 @TargetDataSource("db2") 注解。需要注意的是,该注解最好添加到xxxMapper类的方法上。@TargetDataSource("db2")
ClassVO getClassStudent(@Param("open_id") String openId);总结本文详细介绍了两种数据源配置的方式
THREE.JS 实现看房自由(VR 看房)
一、前言概述:基于WebGL的三维引擎,目前是国内资料最多、使用最广泛的三维引擎,可以制作一些3D可视化项目目前随着元宇宙概念的爆火,THREE技术已经深入到了物联网、VR、游戏、数据可视化等多个平台,今天我们主要基于THREE实现一个三维的VR看房小项目二、基础知识Three.js一般分为三个部分:场景、相机、渲染器,这三个主要的分支就构成了THREE.JS的主要功能区,这三大部分还有许多细小的分支,这些留到我们后续抽出一些章节专门讲解一下。工作流程:场景——相机——渲染器从实际生活中拍照角度立方体网格模型和光照组成了一个虚拟的三维场景,相机对象就像你生活中使用的相机一样可以拍照,只不过一个是拍摄真实的景物,一个是拍摄虚拟的景物。拍摄一个物体的时候相机的位置和角度需要设置,虚拟的相机还需要设置投影方式,当你创建好一个三维场景,相机也设置好,就差一个动作“咔”,通过渲染器就可以执行拍照动作。三、场景概述:场景主要由网络模型与光照组成,网络模型分为几何体与材质3.1 网络模型几何体就像我们小时候学我们就知道点线面体四种概念,点动成线,线动成面,面动成体,而材质就像是是几何体上面的涂鸦,有不同的颜色、图案......例子如下://打造酷炫三角形
for (let i = 0; i < 50; i++) {
const geometry = new THREE.BufferGeometry();
const arr = new Float32Array(9);
for (let j = 0; j < 9; j++) {
arr[j] = Math.random() * 5;
}
geometry.setAttribute('position', new THREE.BufferAttribute(arr, 3));
let randomColor = new THREE.Color(Math.random(), Math.random(), Math.random());
const material = new THREE.MeshBasicMaterial({
color: randomColor,
transparent: true,
opacity:0.5,
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
}
const geometry = new THREE.BoxGeometry(100, 100, 100);
const material = new THREE.MeshStandardMaterial({ color: 0x0000ff });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
const geometry = new THREE.ConeGeometry(5, 15, 32);//底面半径 高 侧边三角分段
const material = new THREE.MeshStandardMaterial({ color: 0x0000ff });
const clone = new THREE.Mesh(geometry, material);
scene.add(clone);
3.2 光照3.2.1 环境光概念:光照对three.js的物体全表面进行光照测试,有可能会发生光照融合//环境光
const ambient = new THREE.AmbientLight(0x404040);
scene.add(ambient);
3.2.2 平行光概念:向特定方向发射的光,太阳光也视作平行的一种,和上面比较,物体变亮了//平行光 颜色 强度
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(100, 100, 100);//光源位置
directionalLight.target = cube;//光源目标 默认 0 0 0
scene.add(directionalLight);
3.2.3 点光源概念:由中间向四周发射光、强度比平行光小// 颜色 强度 距离 衰退量(默认1)
const pointLight = new THREE.PointLight(0xff0000, 1, 100, 1);
pointLight.position.set(50, 50, 50);
scene.add(pointLight);
3.2.4 聚光灯概念:家里面的节能灯泡,强度较好//聚光灯
const spotLigth = new THREE.PointLight(0xffffff);
spotLigth.position.set(50, 50, 50);
spotLigth.target = cube;
spotLigth.angle = Math.PI / 6;
scene.add(spotLigth);
3.2.5 半球光概念:光源直接放置于场景之上,光照颜色从天空光线颜色渐变到地面光线颜色//半球光
const light = new THREE.HemisphereLight(0xffffbb, 0x080820, 1);
scene.add(light);复制代码四、相机4.1 正交相机| 参数(属性) | 含义 |
| :--------- | :----------------------------------------------------------- |
| left | 渲染空间的左边界 |
| right | 渲染空间的右边界 |
| top | 渲染空间的上边界 |
| bottom | 渲染空间的下边界 |
| near | near属性表示的是从距离相机多远的位置开始渲染,一般情况会设置一个很小的值。 默认值0.1 |
| far | far属性表示的是距离相机多远的位置截止渲染,如果设置的值偏小小,会有部分场景看不到。 默认值1000 | || :--------- | :----------------------------------------------------------- || left | 渲染空间的左边界 || right | 渲染空间的右边界 || top | 渲染空间的上边界 || bottom | 渲染空间的下边界 || near | near属性表示的是从距离相机多远的位置开始渲染,一般情况会设置一个很小的值。 默认值0.1 || far | far属性表示的是距离相机多远的位置截止渲染,如果设置的值偏小小,会有部分场景看不到。 默认值1000 |let width = window.innerWidth;
let height = window.innerHeight;
const camera = new THREE.OrthographicCamera(width / - 2, width / 2, height / 2, height / - 2, 1, 1000);
scene.add(camera);
camera.position.set(100, 200, 100);
4.2 透视相机| 参数 | 含义 | 默认值 |
| :----- | :----------------------------------------------------------- | :----------------------------------- |
| fov | fov表示视场,所谓视场就是能够看到的角度范围,人的眼睛大约能够看到180度的视场,视角大小设置要根据具体应用,一般游戏会设置60~90度 | 45 |
| aspect | aspect表示渲染窗口的长宽比,如果一个网页上只有一个全屏的canvas画布且画布上只有一个窗口,那么aspect的值就是网页窗口客户区的宽高比 | window.innerWidth/window.innerHeight |
| near | near属性表示的是从距离相机多远的位置开始渲染,一般情况会设置一个很小的值。 | 0.1 |
| far | far属性表示的是距离相机多远的位置截止渲染,如果设置的值偏小,会有部分场景看不到 | 1000 | | 默认值 || :----- | :----------------------------------------------------------- | :----------------------------------- || fov | fov表示视场,所谓视场就是能够看到的角度范围,人的眼睛大约能够看到180度的视场,视角大小设置要根据具体应用,一般游戏会设置60~90度 | 45 || aspect | aspect表示渲染窗口的长宽比,如果一个网页上只有一个全屏的canvas画布且画布上只有一个窗口,那么aspect的值就是网页窗口客户区的宽高比 | window.innerWidth/window.innerHeight || near | near属性表示的是从距离相机多远的位置开始渲染,一般情况会设置一个很小的值。 | 0.1 || far | far属性表示的是距离相机多远的位置截止渲染,如果设置的值偏小,会有部分场景看不到 | 1000 |复制代码let width = window.innerWidth;
let height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000);
camera.position.set(150, 100, 300);
camera.lookAt(scene.position);
五、渲染器概述:从WEBGL的角度来看,three就是对它的进一步封装,想要进一步了解渲染器这方面的知识点还需要了解一下WEBGL,这里我们就不做过多介绍了。六、贴图纹理6.1 基础介绍概述:这部分对于我们是否能够给别人呈现一个真实的渲染场景来说,很重要,比如下面一个普普通通的正方体,我们只要一加上贴图,立马不一样了。以前之后6.2 环境贴图概述:目前有许许多多的贴图,比如基础、透明、环境、法线、金属、粗糙、置换等等,今天我们呢主要讲解一下环境和一点 HDR处理在THREE的世界里面,坐标抽x、y、z的位置关系图如下所示:红、绿、蓝分别代表x、z、y,我们的贴图就是在px nx py ny pz nz这六个方向防止一张图片,其中 p 就代表坐标轴的正方向CubeTextureLoader:加载CubeTexture的一个类。 内部使用ImageLoader来加载文件。//场景贴图
const sphereTexture = new THREE.CubeTextureLoader().setPath('./textures/course/environmentMaps/0/');
const envTexture= sphereTexture.load([
'px.jpg',
'nx.jpg',
'py.jpg',
'ny.jpg',
'pz.jpg',
'nz.jpg'
]);
//场景添加背景
scene.background = envTexture;
//场景的物体添加环境贴图(无默认情况使用)
scene.environment = envTexture;
const sphereGeometry = new THREE.SphereGeometry(5, 30, 30);
const sphereMaterial = new THREE.MeshStandardMaterial({
roughness: 0,//设置粗糙程度
metalness: 1,//金属度
envMap:envTexture,
});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphere);
复制代码gif图片有点大上传不了,我就截了几张图这里我们换几张贴图就可以让上面一个外景的 VR 变成内景的房间,如下图所示:6.3 HDR 处理概述:高动态范围图像,相比普通的图像,能够提供更多的动态范围和图像细节,一般被运用于电视显示产品以及图片视频拍摄制作当中。import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader;
const rgbeLoader = new RGBELoader().setPath('./textures/course/hdr/');
//异步加载
rgbeLoader.loadAsync('002.hdr').then((texture) => {
//设置加载方式 等距圆柱投影的环境贴图
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.background = texture;
})
复制代码七、拓展7.1 坐标系概述:坐标轴能够更好的反馈物体的位置信息,红、绿、蓝分别代表x、z、yconst axesHelper = new THREE.AxesHelper(20);//里面的数字代表坐标抽长度scene.add(axesHelper);复制代码7.2 控制器概述:通过鼠标控制物体和相机的移动、旋转、缩放导包import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'复制代码应用const controls = new OrbitControls(camera, renderer.domElement)复制代码自旋转controls.autoRotate = true复制代码必须在render函数调用update实时更新才奏效7.3 自适应概述:根据屏幕大小自适应场景//自适应屏幕window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() renderer.setSize(window.innerWidth, window.innerHeight) renderer.setPixelRatio(window.devicePixelRatio)})复制代码设置相机的宽高比、重新更新渲染相机、渲染器的渲染大小、设备的像素比7.4 全屏响应概述:双击进入全屏,再次双击/ESC 退出全屏window.addEventListener('dblclick', () => { let isFullScreen = document.fullscreenElement if (!isFullScreen) { renderer.domElement.requestFullscreen() } else { document.exitFullscreen() }})复制代码7.5 信息面板概述;通过操作面板完成界面的移动物体的相关应用链接:https://www.npmjs.com/package/dat.gui//安装npmnpm install --save dat.gui//如果出现...标记错误,安装到开发依赖就可以了npm i --save-dev @types/dat.gui复制代码//界面操作const gui = new dat.GUI();
//操作物体位置gui .add(cube.position, 'x') .min(0) .max(10) .step(0.1) .name('X轴移动') .onChange((value) => { console.log('修改的值为' + value); }) .onFinishChange((value) => { console.log('完全停止' + value); });//操作物体颜色const colors = { color: '#0000ff',};gui .addColor(colors, 'color') .onChange((value) => { //修改物体颜色 cube.material.color.set(value); });复制代码7.6 频率检测概述:检测帧率导包import Stats from 'three/addons/libs/stats.module.js';复制代码应用const stats = new Stats();document.body.appendChild(stats.dom);复制代码自变化stats.update()复制代码必须在render函数调用update实时更新才奏效7.7 导航网格概述:底部二维平面的网格化,帮助我们更好的创建场景const gridHelper = new THREE.GridHelper(10, 20)//网格大小、细分次数scene.add(gridHelper)复制代码八、源码//导入包
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import * as dat from 'dat.gui';
import Stats from 'three/addons/libs/stats.module.js';
let scene,camera,renderer
//场景
scene = new THREE.Scene();
//坐标抽
const axesHelper = new THREE.AxesHelper(20);
scene.add(axesHelper);
//场景贴图
const sphereTexture = new THREE.CubeTextureLoader().setPath('./textures/course/environmentMaps/0/');
const envTexture= sphereTexture.load([
'px.jpg',
'nx.jpg',
'py.jpg',
'ny.jpg',
'pz.jpg',
'nz.jpg'
]);
//场景添加背景
scene.background = envTexture;
//场景的物体添加环境贴图(无默认情况使用)
scene.environment = envTexture;
const sphereGeometry = new THREE.SphereGeometry(5, 30, 30);
const sphereMaterial = new THREE.MeshStandardMaterial({
roughness: 0,//设置粗糙程度
metalness: 1,//金属度
envMap:envTexture,
});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphere);
//光照
const ambient = new THREE.AmbientLight(0xffffff);
scene.add(ambient);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.05);
directionalLight.position.set(10,10,10);
directionalLight.lookAt(scene.position);
scene.add( directionalLight );
//相机
camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
1,
2000,
);
camera.position.set(10,10,20);
camera.lookAt(scene.position);
scene.add(camera);
//渲染器
renderer = new THREE.WebGLRenderer({
//防止锯齿
antialias: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
// renderer.setClearColor(0xb9d3ff, 1);
document.body.appendChild(renderer.domElement);
//鼠标控制器
const controls = new OrbitControls(camera, renderer.domElement);
//阻尼 必须在 render函数调用 controls.update();
controls.dampingFactor = true;
controls.autoRotate=true
const stats=new Stats()
document.body.appendChild(stats.dom);
function render () {
renderer.render(scene, camera);
requestAnimationFrame(render);
controls.update();//调用
stats.update()
}
render();
//全屏操作
window.addEventListener('dblclick', () => {
//查询是否全屏
let isFullScene = document.fullscreenElement;
console.log(isFullScene);
if (!isFullScene) {
renderer.domElement.requestFullscreen();
}
else {
document.exitFullscreen();
}
})
//自适应
window.addEventListener('resize', () => {
//宽高比
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);//设置像素比
})
//界面操作
const gui = new dat.GUI();
//操作物体位置
gui
.add(sphere.position, 'x')
.min(0)
.max(10)
.step(0.1)
.name('X轴移动')
.onChange((value) => {
console.log('修改的值为' + value);
})
.onFinishChange((value) => {
console.log('完全停止' + value);
});
//操作物体颜色
const colors = {
color: '#0000ff',
};
gui
.addColor(colors, 'color')
.onChange((value) => {
//修改物体颜色
sphere.material.color.set(value);
});