
【String注解驱动开发】如何按照条件向Spring容器中注册bean?这次我懂了!! 写在前面当bean是单实例,并且没有设置懒加载时,Spring容器启动时,就会实例化bean,并将bean注册到IOC容器中,以后每次从IOC容器中获取bean时,直接返回IOC容器中的bean,不再创建新的bean。 如果bean是单实例,并且使用@Lazy注解设置了懒加载,则Spring容器启动时,不会实例化bean,也不会将bean注册到IOC容器中,只有第一次获取bean的时候,才会实例化bean,并且将bean注册到IOC容器中。 如果bean是多实例,则Spring容器启动时,不会实例化bean,也不会将bean注册到IOC容器中,以后每次从IOC容器中获取bean时,都会创建一个新的bean返回。 Spring支持按照条件向IOC容器中注册bean,满足条件的bean就会被注册到IOC容器中,不满足条件的bean就不会被注册到IOC容器中。接下来,我们就一起来探讨Spring中如何实现按照条件向IOC容器中注册bean。 项目工程源码已经提交到GitHub:https://github.com/sunshinelyz/spring-annotation @Conditional注解概述@Conditional注解可以按照一定的条件进行判断,满足条件向容器中注册bean,不满足条件就不向容器中注册bean。 @Conditional注解是由 SpringFramework 提供的一个注解,位于 org.springframework.context.annotation 包内,定义如下。 package org.springframework.context.annotation; import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target; @Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Conditional { Class<? extends Condition>[] value(); }从@Conditional注解的源码来看,@Conditional注解可以添加到类上,也可以添加到方法上。在@Conditional注解中,存在一个Condition类型或者其子类型的Class对象数组,Condition是个啥?我们点进去看一下。 package org.springframework.context.annotation; import org.springframework.beans.factory.config.BeanFactoryPostProcessor;import org.springframework.core.type.AnnotatedTypeMetadata;@FunctionalInterfacepublic interface Condition { boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }可以看到,Condition是一个函数式接口,对于函数式接口不了解的同学可以参见【Java8新特性】中的《【Java8新特性】还没搞懂函数式接口?赶快过来看看吧!》一文。也可以直接查看《Java8新特性专栏》来系统学习Java8的新特性。 所以,我们使用@Conditional注解时,需要一个类实现Spring提供的Condition接口,它会匹配@Conditional所符合的方法,然后我们可以使用我们在@Conditional注解中定义的类来检查。 @Conditional注解的使用场景如下所示。 可以作为类级别的注解直接或者间接的与@Component相关联,包括@Configuration类;可以作为元注解,用于自动编写构造性注解;作为方法级别的注解,作用在任何@Bean方法上。向Spring容器注册bean不带条件注册bean我们在PersonConfig2类中新增person01()方法和person02()方法,并为两个方法添加@Bean注解,如下所示。 @Bean("binghe001")public Person person01(){ return new Person("binghe001", 18); } @Bean("binghe002")public Person person02(){ return new Person("binghe002", 20); }那么,这两个bean默认是否会被注册到Spring容器中呢,我们新建一个测试用例来测试一下。在SpringBeanTest类中新建testAnnotationConfig6()方法,如下所示。 @Testpublic void testAnnotationConfig6(){ ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class); String[] names = context.getBeanNamesForType(Person.class); Arrays.stream(names).forEach(System.out::println); }我们运行testAnnotationConfig6()方法,输出的结果信息如下所示。 personbinghe001binghe002从输出结果可以看出,同时输出了binghe001和binghe002。说明默认情况下,Spring容器会将单实例并且非懒加载的bean注册到IOC容器中。 接下来,我们再输出bean的名称和bean实例对象信息,此时我们在testAnnotationConfig6()方法中添加相应的代码片段,如下所示。 @Testpublic void testAnnotationConfig6(){ ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class); String[] names = context.getBeanNamesForType(Person.class); Arrays.stream(names).forEach(System.out::println); Map<String, Person> beans = context.getBeansOfType(Person.class); System.out.println(beans); }再次运行SpringBeanTest类中的testAnnotationConfig6()方法,输出结果如下所示。 personbinghe001binghe002给容器中添加Person....{person=Person(name=binghe002, age=18), binghe001=Person(name=binghe001, age=18), binghe002=Person(name=binghe002, age=20)}可以看到,输出了注册到容器的bean。 带条件注册bean现在,我们就要提出新的需求了,比如,如果当前操作系统是Windows操作系统,则向Spring容器中注册binghe001;如果当前操作系统是Linux操作系统,则向Spring容器中注册binghe002。此时,我们就需要使用@Conditional注解了。 这里,有小伙伴可能会问:如何获取操作系统的类型呢,别急,这个问题很简单,我们继续向下看。 使用Spring的ApplicationContext接口就能够获取到当前操作系统的类型,如下所示。 ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);Environment environment = context.getEnvironment();String osName = environment.getProperty("os.name");System.out.println(osName);我们将上述代码整合到SpringBeanTest类中的testAnnotationConfig6()方法中,如下所示。 @Testpublic void testAnnotationConfig6(){ ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class); Environment environment = context.getEnvironment(); String osName = environment.getProperty("os.name"); System.out.println(osName); String[] names = context.getBeanNamesForType(Person.class); Arrays.stream(names).forEach(System.out::println); Map<String, Person> beans = context.getBeansOfType(Person.class); System.out.println(beans); }接下来,我们运行SpringBeanTest类中的testAnnotationConfig6()方法,输出的结果信息如下所示。 Windows 10personbinghe001binghe002给容器中添加Person....{person=Person(name=binghe002, age=18), binghe001=Person(name=binghe001, age=18), binghe002=Person(name=binghe002, age=20)}由于我使用的操作系统是Windows 10操作系统,所以在结果信息中输出了Windows 10。 到这里,我们成功获取到了操作系统的类型,接下来,就可以实现:如果当前操作系统是Windows操作系统,则向Spring容器中注册binghe001;如果当前操作系统是Linux操作系统,则向Spring容器中注册binghe002的需求了。此时,我们就需要借助Spring的@Conditional注解来实现了。 要想使用@Conditional注解,我们需要实现Condition接口来为@Conditional注解设置条件,所以,这里,我们创建了两个实现Condition接口的类,分别为WindowsCondition和LinuxCondition,如下所示。 WindowsConditionpackage io.mykit.spring.plugins.register.condition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;import org.springframework.beans.factory.support.BeanDefinitionRegistry;import org.springframework.context.annotation.Condition;import org.springframework.context.annotation.ConditionContext;import org.springframework.core.env.Environment;import org.springframework.core.type.AnnotatedTypeMetadata; /** @author binghe @version 1.0.0 @description Windows条件,判断操作系统是否是Windows*/ public class WindowsCondition implements Condition { /** * ConditionContext:判断条件使用的上下文环境 * AnnotatedTypeMetadata:注释信息 */ @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { //判断是否是Linux系统 //1.获取到IOC容器使用的BeanFactory ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); //2.获取类加载器 ClassLoader classLoader = context.getClassLoader(); //3.获取当前的环境信息 Environment environment = context.getEnvironment(); //4.获取bean定义的注册类,我们可以通过BeanDefinitionRegistry对象查看 //Spring容器中注册了哪些bean,也可以通过BeanDefinitionRegistry对象向 //Spring容器中注册bean,移除bean,查看bean的定义,查看是否包含某个bean的定义 BeanDefinitionRegistry registry = context.getRegistry(); String property = environment.getProperty("os.name"); return property.contains("Windows"); } }LinuxConditionpackage io.mykit.spring.plugins.register.condition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;import org.springframework.beans.factory.support.BeanDefinitionRegistry;import org.springframework.context.annotation.Condition;import org.springframework.context.annotation.ConditionContext;import org.springframework.core.env.Environment;import org.springframework.core.type.AnnotatedTypeMetadata; /** @author binghe @version 1.0.0 @description Linux条件,判断操作系统是否是Linux*/ public class LinuxCondition implements Condition { /** * ConditionContext:判断条件使用的上下文环境 * AnnotatedTypeMetadata:注释信息 */ @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { //判断是否是Linux系统 //1.获取到IOC容器使用的BeanFactory ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); //2.获取类加载器 ClassLoader classLoader = context.getClassLoader(); //3.获取当前的环境信息 Environment environment = context.getEnvironment(); //4.获取bean定义的注册类,我们可以通过BeanDefinitionRegistry对象查看 //Spring容器中注册了哪些bean,也可以通过BeanDefinitionRegistry对象向 //Spring容器中注册bean,移除bean,查看bean的定义,查看是否包含某个bean的定义 BeanDefinitionRegistry registry = context.getRegistry(); String property = environment.getProperty("os.name"); return property.contains("linux"); } }接下来,我们就需要在PersonConfig2类中使用@Conditional注解添加条件了。添加注解后的方法如下所示。 @Conditional({WindowsCondition.class})@Bean("binghe001")public Person person01(){ return new Person("binghe001", 18); } @Conditional({LinuxCondition.class})@Bean("binghe002")public Person person02(){ return new Person("binghe002", 20); }此时,我们再次运行SpringBeanTest类中的testAnnotationConfig6()方法,输出的结果信息如下所示。 Windows 10personbinghe001给容器中添加Person....{person=Person(name=binghe002, age=18), binghe001=Person(name=binghe001, age=18)}可以看到,输出结果中不再含有名称为binghe002的bean了,说明程序中检测到当前操作系统为Windows10,没有向Spring容器中注册名称为binghe002的bean。 @Conditional注解也可以标注在类上,标注在类上含义为:满足当前条件,这个类中配置的所有bean注册才能生效,大家可以自行验证@Conditional注解标注在类上的情况 @Conditional的扩展注解@ConditionalOnBean:仅仅在当前上下文中存在某个对象时,才会实例化一个Bean。@ConditionalOnClass:某个class位于类路径上,才会实例化一个Bean。@ConditionalOnExpression:当表达式为true的时候,才会实例化一个Bean。@ConditionalOnMissingBean:仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean。@ConditionalOnMissingClass:某个class类路径上不存在的时候,才会实例化一个Bean。@ConditionalOnNotWebApplication:不是web应用,才会实例化一个Bean。@ConditionalOnBean:当容器中有指定Bean的条件下进行实例化。@ConditionalOnMissingBean:当容器里没有指定Bean的条件下进行实例化。@ConditionalOnClass:当classpath类路径下有指定类的条件下进行实例化。@ConditionalOnMissingClass:当类路径下没有指定类的条件下进行实例化。@ConditionalOnWebApplication:当项目是一个Web项目时进行实例化。@ConditionalOnNotWebApplication:当项目不是一个Web项目时进行实例化。@ConditionalOnProperty:当指定的属性有指定的值时进行实例化。@ConditionalOnExpression:基于SpEL表达式的条件判断。@ConditionalOnJava:当JVM版本为指定的版本范围时触发实例化。@ConditionalOnResource:当类路径下有指定的资源时触发实例化。@ConditionalOnJndi:在JNDI存在的条件下触发实例化。@ConditionalOnSingleCandidate:当指定的Bean在容器中只有一个,或者有多个但是指定了首选的Bean时触发实例化。 @Conditional 与@Profile 的对比Spring3.0 也有一些和@Conditional 相似的注解,它们是Spring SPEL 表达式和Spring Profiles 注解 Spring4.0的@Conditional 注解要比@Profile 注解更加高级。@Profile 注解用来加载应用程序的环境。@Profile注解仅限于根据预定义属性编写条件检查。 @Conditional注释则没有此限制。 Spring中的@Profile 和 @Conditional 注解用来检查"If…then…else"的语义。然而,Spring4 @Conditional是@Profile 注解的更通用法。 Spring 3中的 @Profile仅用于编写基于Environment变量的条件检查。 配置文件可用于基于环境加载应用程序配置。Spring 4 @Conditional注解允许开发人员为条件检查定义用户定义的策略。 @Conditional可用于条件bean注册。好了,咱们今天就聊到这儿吧!别忘了给个在看和转发,让更多的人看到,一起学习一起进步!! 项目工程源码已经提交到GitHub:https://github.com/sunshinelyz/spring-annotation 写在最后如果觉得文章对你有点帮助,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习Spring注解驱动开发。公众号回复“spring注解”关键字,领取Spring注解驱动开发核心知识图,让Spring注解驱动开发不再迷茫。 参考:https://www.cnblogs.com/cxuanBlog/p/10960575.html 原文地址https://www.cnblogs.com/binghe001/p/13084108.html
涨见识了,在终端执行 Python 代码的 6 种方式! 为了我们推出的 VS Code 的 Python 插件 [1],我写了一个简单的脚本来生成变更日志 [2](类似于Towncrier [3],但简单些,支持 Markdown,符合我们的需求)。在发布过程中,有一个步骤是运行python news ,它会将 Python 指向我们代码中的"news"目录。 前几天,一位合作者问这是如何工作的,似乎我们团队中的每个人都知道如何使用-m ?(请参阅我的有关带 -m 使用 pip 的文章 [4],了解原因)(译注:关于此话题,我也写过一篇更为详细的文章 ) 这使我意识到其他人可能不知道有五花八门的方法可以将 Python 指向要执行的代码,因此有了这篇文章。 1、通过标准输入和管道因为如何用管道传东西给一个进程是属于 shell 的内容,我不打算深入解释。毋庸置疑,你可以将代码传递到 Python 中。 管道传内容给 python echo "print('hi')" | python如果将文件重定向到 Python,这显然也可以。 重定向一个文件给 python python < spam.py归功于 Python 的 UNIX 传统,这些都不太令人感到意外。 2、通过-c 指定的字符串如果你只需要快速地检查某些内容,则可以在命令行中将代码作为字符串传递。 使用 python 的 -c 参数 python -c "print('hi')"当需要检查仅一行或两行代码时,我个人会使用它,而不是启动 REPL(译注:Read Eval Print Loop,即交互式解释器,例如在 windows 控制台中输入python, 就会进入交互式解释器。-c 参数用法可以省去进入解释器界面的过程) 。 3、文件的路径最众所周知的传代码给 python 的方法很可能是通过文件路径。 指定 python 的文件路径 python spam.py要实现这一点的关键是将包含该文件的目录放到sys.path 里。这样你的所有导入都可以继续使用。但这也是为什么你不能/不应该传入包含在一个包里的模块路径。因为sys.path 可能不包含该包的目录,因此所有的导入将相对于与你预期的包不同的目录。 4、对包使用 -m执行 Python 包的正确方法是使用 -m 并指定要运行的包名。 python -m spam它在底层使用了runpy [5]。要在你的项目中做到这点,只需要在包里指定一个__main__.py 文件,它将被当成__main__ 执行。而且子模块可以像任何其它模块一样导入,因此你可以对其进行各种测试。 我知道有些人喜欢在一个包里写一个main 子模块,然后将其__main__.py 写成: from . import main if name == "__main__": main.main() 就我个人而言,我不感冒于单独的main 模块,而是直接将所有相关的代码放入__main__.py ,因为我感觉这些模块名是多余的。 (译注:即作者不关心作为入口文件的"main"或者“__main__”模块,因为执行时只需用它们的包名即可。我认为这也暗示了入口模块不该再被其它模块 import。我上篇文章 [6]比作者的观点激进,认为连那句 if 语句都不该写。) 5、目录定义__main__.py也可以扩展到目录。如果你看一下促成此博客文章的示例,python news 可执行,就是因为 news 目录有一个 __main__.py 文件。该目录就像一个文件路径被 Python 执行了。 现在你可能会问:“为什么不直接指定文件路径呢?”好吧,坦白说,关于文件路径,有件事得说清楚。在发布过程中,我可以简单地写上说明,让运行python news/announce.py ,但是并没有确切的理由说明这种机制何时存在。 再加上我以后可以更改文件名,而且没人会注意到。再加上我知道代码会带有辅助文件,因此将其放在目录中而不是单独作为单个文件是有意义的。 当然,我也可以将它变为一个使用 -m 的包,但是没必要,因为 announce 脚本很简单,我知道它要保持成为一个单独的自足的文件(少于 200 行,并且测试模块也大约是相同的长度)。 况且,__main__.py 文件非常简单。 import runpy Change 'announce' to whatever module you want to run. runpy.run_module('announce', run_name='__main__', alter_sys=True)现在显然必须要处理依赖关系,但是如果你的脚本仅使用标准库或将依赖模块放在__main__.py 旁边(译注:即同级目录),那么就足够了! (译注:我觉得作者在此有点“炫技”了,因为这种写法的前提是得知道 runpy 的用法,但是就像前一条所写的用 -m 参数运行一个包,在底层也是用了 runpy。不过炫技的好处也非常明显,即__main__.py 里不用导入 announce 模块,还是以它为主模块执行,也就不会破坏原来的依赖导入关系) 6、执行一个压缩文件如果你确实有多个文件和/或依赖模块,并且希望将所有代码作为一个单元发布,你可以用一个__main__.py ,放置在一个压缩文件中,并把压缩文件所在目录放在 sys.path 里,Python 会替你运行__main__.py 文件。 将一个压缩包传给 Python python app.pyz人们现在习惯上用 .pyz 文件扩展名来命名此类压缩文件,但这纯粹是传统,不会影响任何东西;你当然也可以用 .zip 文件扩展名。 为了简化创建此类可执行的压缩文件,标准库提供了zipapp [7]模块。它会为你生成__main__.py并添加一条组织行(shebang line),因此你甚至不需要指定 python,如果你不想在 UNIX 上指定它的话。如果你想移动一堆纯 Python 代码,这是一种不错的方法。 不幸的是,仅当压缩文件包含的所有代码都是纯 Python 时,才能这样运行压缩文件。执行压缩文件对扩展模块无效(这就是为什么 setuptools 有一个 zip_safe [8]标志的原因)。(译注:扩展模块 extension module,即 C/C++ 之类的非 Python 文件) 要加载扩展模块,Python 必须调用 dlopen() [9]函数,它要传入一个文件路径,但当该文件路径就包含在压缩文件内时,这显然不起作用。 我知道至少有一个人与 glibc 团队交谈过,关于支持将内存缓冲区传入压缩文件,以便 Python 可以将扩展模块读入内存,并将其传给压缩文件,但是如果内存为此服务,glibc 团队并不同意。 但是,并非所有希望都丧失了!你可以使用诸如shiv [10]之类的项目,它会捆绑(bundle)你的代码,然后提供一个__main__.py 来处理压缩文件的提取、缓存,然后为你执行代码。尽管不如纯 Python 解决方案理想,但它确实可行,并且在这种情况下算得上是优雅的。 (译注:翻译水平有限,难免偏差。我加注了部分内容,希望有助于阅读。请搜索关注“Python猫”,阅读更多优质的原创或译作。) 原作:BRETT CANNON 译者:豌豆花下猫@Python猫 英文:https://snarky.ca/the-many-ways-to-pass-code-to-python-from-the-terminal 转载地址https://www.cnblogs.com/pythonista/p/13055856.html
Java多线程之深入解析ThreadLocal和ThreadLocalMap ThreadLocal概述ThreadLocal是线程变量,ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。 它具有3个特性: 线程并发:在多线程并发场景下使用。传递数据:可以通过ThreadLocal在同一线程,不同组件中传递公共变量。线程隔离:每个线程变量都是独立的,不会相互影响。在不使用ThreadLocal的情况下,变量不隔离,得到的结果具有随机性。 public class Demo { private String variable; public String getVariable() { return variable; } public void setVariable(String variable) { this.variable = variable; } public static void main(String[] args) { Demo demo = new Demo(); for (int i = 0; i < 5; i++) { new Thread(()->{ demo.setVariable(Thread.currentThread().getName()); System.out.println(Thread.currentThread().getName()+" "+demo.getVariable()); }).start(); } } } 输出结果: View Code在不使用ThreadLocal的情况下,变量隔离,每个线程有自己专属的本地变量variable,线程绑定了自己的variable,只对自己绑定的变量进行读写操作。 public class Demo { private ThreadLocal<String> variable = new ThreadLocal<>(); public String getVariable() { return variable.get(); } public void setVariable(String variable) { this.variable.set(variable); } public static void main(String[] args) { Demo demo = new Demo(); for (int i = 0; i < 5; i++) { new Thread(()->{ demo.setVariable(Thread.currentThread().getName()); System.out.println(Thread.currentThread().getName()+" "+demo.getVariable()); }).start(); } } } 输出结果: View Codesynchronized和ThreadLocal的比较上述需求,通过synchronized加锁同样也能实现。但是加锁对性能和并发性有一定的影响,线程访问变量只能排队等候依次操作。TreadLocal不加锁,多个线程可以并发对变量进行操作。 public class Demo { private String variable; public String getVariable() { return variable; } public void setVariable(String variable) { this.variable = variable; } public static void main(String[] args) { Demo demo = new Demo1(); for (int i = 0; i < 5; i++) { new Thread(()->{ synchronized (Demo.class){ demo.setVariable(Thread.currentThread().getName()); System.out.println(Thread.currentThread().getName()+" "+demo.getVariable()); } }).start(); } } } ThreadLocal和synchronized都是用于处理多线程并发访问资源的问题。ThreadLocal是以空间换时间的思路,每个线程都拥有一份变量的拷贝,从而实现变量隔离,互相不干扰。关注的重点是线程之间数据的相互隔离关系。synchronized是以时间换空间的思路,只提供一个变量,线程只能通过排队访问。关注的是线程之间访问资源的同步性。ThreadLocal可以带来更好的并发性,在多线程、高并发的环境中更为合适一些。 ThreadLocal使用场景转账事务的例子JDBC对于事务原子性的控制可以通过setAutoCommit(false)设置为事务手动提交,成功后commit,失败后rollback。在多线程的场景下,在service层开启事务时用的connection和在dao层访问数据库的connection应该要保持一致,所以并发时,线程只能隔离操作自已的connection。 解决方案1:service层的connection对象作为参数传递给dao层使用,事务操作放在同步代码块中。 存在问题:传参提高了代码的耦合程度,加锁降低了程序的性能。 解决方案2:当需要获取connection对象的时候,通过ThreadLocal对象的get方法直接获取当前线程绑定的连接对象使用,如果连接对象是空的,则去连接池获取连接,并通过ThreadLocal对象的set方法绑定到当前线程。使用完之后调用ThreadLocal对象的remove方法解绑连接对象。 ThreadLocal的优势: 可以方便地传递数据:保存每个线程绑定的数据,需要的时候可以直接获取,避免了传参带来的耦合。可以保持线程间隔离:数据的隔离在并发的情况下也能保持一致性,避免了同步的性能损失。ThreadLocal的原理每个ThreadLocal维护一个ThreadLocalMap,Map的Key是ThreadLocal实例本身,value是要存储的值。 每个线程内部都有一个ThreadLocalMap,Map里面存放的是ThreadLocal对象和线程的变量副本。Thread内部的Map通过ThreadLocal对象来维护,向map获取和设置变量副本的值。不同的线程,每次获取变量值时,只能获取自己对象的副本的值。实现了线程之间的数据隔离。 JDK1.8的设计相比于之前的设计(通过ThreadMap维护了多个线程和线程变量的对应关系,key是Thread对象,value是线程变量)的好处在于,每个Map存储的Entry数量变少了,线程越多键值对越多。现在的键值对的数量是由ThreadLocal的数量决定的,一般情况下ThreadLocal的数量少于线程的数量,而且并不是每个线程都需要创建ThreadLocal变量。当Thread销毁时,ThreadLocal也会随之销毁,减少了内存的使用,之前的方案中线程销毁后,ThreadLocalMap仍然存在。 ThreadLocal源码解析set方法首先获取线程,然后获取线程的Map。如果Map不为空则将当前ThreadLocal的引用作为key设置到Map中。如果Map为空,则创建一个Map并设置初始值。 get方法首先获取当前线程,然后获取Map。如果Map不为空,则Map根据ThreadLocal的引用来获取Entry,如果Entry不为空,则获取到value值,返回。如果Map为空或者Entry为空,则初始化并获取初始值value,然后用ThreadLocal引用和value作为key和value创建一个新的Map。 remove方法删除当前线程中保存的ThreadLocal对应的实体entry。 initialValue方法该方法的第一次调用发生在当线程通过get方法访问线程的ThreadLocal值时。除非线程先调用了set方法,在这种情况下,initialValue才不会被这个线程调用。每个线程最多调用依次这个方法。 该方法只返回一个null,如果想要线程变量有初始值需要通过子类继承ThreadLocal的方式去重写此方法,通常可以通过匿名内部类的方式实现。这个方法是protected修饰的,是为了让子类覆盖而设计的。 ThreadLocalMap源码分析ThreadLocalMap是ThreadLocal的静态内部类,没有实现Map接口,独立实现了Map的功能,内部的Entry也是独立实现的。 与HashMap类似,初始容量默认是16,初始容量必须是2的整数幂。通过Entry类的数据table存放数据。size是存放的数量,threshold是扩容阈值。 Entry继承自WeakReference,key是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。 弱引用和内存泄漏内存溢出:没有足够的内存供申请者提供 内存泄漏:程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等验证后沟。内存泄漏的堆积会导致内存溢出。 弱引用:垃圾回收器一旦发现了弱引用的对象,不管内存是否足够,都会回收它的内存。 内存泄漏的根源是ThreadLocalMap和Thread的生命周期是一样长的。 如果在ThreadLocalMap的key使用强引用还是无法完全避免内存泄漏,ThreadLocal使用完后,ThreadLocal Reference被回收,但是Map的Entry强引用了ThreadLocal,ThreadLocal就无法被回收,因为强引用链的存在,Entry无法被回收,最后会内存泄漏。 在实际情况中,ThreadLocalMap中使用的key为ThreadLocal的弱引用,value是强引用。如果ThreadLocal没有被外部强引用的话,在垃圾回收的时候,key会被清理,value不会。这样ThreadLocalMap就出现了为null的Entry。如果不做任何措施,value永远不会被GC回收,就会产生内存泄漏。 ThreadLocalMap中考虑到这个情况,在set、get、remove操作后,会清理掉key为null的记录(将value也置为null)。使用完ThreadLocal后最后手动调用remove方法(删除Entry)。 也就是说,使用完ThreadLocal后,线程仍然运行,如果忘记调用remove方法,弱引用比强引用可以多一层保障,弱引用的ThreadLocal会被回收,对应的value会在下一次ThreadLocalMap调用get、set、remove方法的时候被清除,从而避免了内存泄漏。 Hash冲突的解决ThreadLocalMap的构造方法 构造函数创建一个长队为16的Entry数组,然后计算firstKey的索引,存储到table中,设置size和threshold。 firstKey.threadLocalHashCode & (INITIAL_CAPACITY-1)用来计算索引,nextHashCode是Atomicinteger类型的,Atomicinteger类是提供原子操作的Integer类,通过线程安全的方式来加减,适合高并发使用。 每次在当前值上加上一个HASH_INCREMENT值,这个值和斐波拉契数列有关,主要目的是为了让哈希码可以均匀的分布在2的n次方的数组里,从而尽量的避免冲突。 当size为2的幂次的时候,hashCode & (size - 1)相当于取模运算hashCode % size,位运算比取模更高效一些。为了使用这种取模运算, 所有size必须是2的幂次。这样一来,在保证索引不越界的情况下,减少冲突的次数。 ThreadLocalMap的set方法 ThreadLocalMao使用了线性探测法来解决冲突。线性探测法探测下一个地址,找到空的地址则插入,若整个空间都没有空余地址,则产生溢出。例如:长度为8的数组中,当前key的hash值是6,6的位置已经被占用了,则hash值加一,寻找7的位置,7的位置也被占用了,回到0的位置。直到可以插入为止,可以将这个数组看成一个环形数组。 原文地址https://www.cnblogs.com/xdcat/p/13051561.html
.NET Core Session源码探究 前言#随着互联网的兴起,技术的整体架构设计思路有了质的提升,曾经Web开发必不可少的内置对象Session已经被慢慢的遗弃。主要原因有两点,一是Session依赖Cookie存放SessionID,即使不通过Cookie传递,也要依赖在请求参数或路径上携带Session标识,对于目前前后端分离项目来说操作起来限制很大,比如跨域问题。二是Session数据跨服务器同步问题,现在基本上项目都使用负载均衡技术,Session同步存在一定的弊端,虽然可以借助Redis或者其他存储系统实现中心化存储,但是略显鸡肋。虽然存在一定的弊端,但是在.NET Core也并没有抛弃它,而且借助了更好的实现方式提升了它的设计思路。接下来我们通过分析源码的方式,大致了解下新的工作方式。 Session如何使用#.NET Core的Session使用方式和传统的使用方式有很大的差别,首先它依赖存储系统IDistributedCache来存储数据,其次它依赖SessionMiddleware为每一次请求提供具体的实例。所以使用Session之前需要配置一些操作,相信介绍情参阅微软官方文档会话状态。简单来说大致配置如下 Copypublic class Startup{ public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddDistributedMemoryCache(); services.AddSession(options => { options.IdleTimeout = TimeSpan.FromSeconds(10); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseSession(); } }Session注入代码分析#注册的地方设计到了两个扩展方法AddDistributedMemoryCache和AddSession.其中AddDistributedMemoryCache这是借助IDistributedCache为Session数据提供存储,AddSession是Session实现的核心的注册操作。 IDistributedCache提供存储#上面的示例中示例中使用的是基于本地内存存储的方式,也可以使用IDistributedCache针对Redis和数据库存储的扩展方法。实现也非常简单就是给IDistributedCache注册存储操作实例 Copypublic static IServiceCollection AddDistributedMemoryCache(this IServiceCollection services){ if (services == null) { throw new ArgumentNullException(nameof(services)); } services.AddOptions(); services.TryAdd(ServiceDescriptor.Singleton<IDistributedCache, MemoryDistributedCache>()); return services; }关于IDistributedCache的其他使用方式请参阅官方文档的分布式缓存篇,关于分布式缓存源码实现可以通过Cache的Github地址自行查阅。 AddSession核心操作#AddSession是Session实现的核心的注册操作,具体实现代码来自扩展类SessionServiceCollectionExtensions,AddSession扩展方法大致实现如下 Copypublic static IServiceCollection AddSession(this IServiceCollection services){ if (services == null) { throw new ArgumentNullException(nameof(services)); } services.TryAddTransient<ISessionStore, DistributedSessionStore>(); services.AddDataProtection(); return services; }这个方法就做了两件事,一个是注册了Session的具体操作,另一个是添加了数据保护保护条例支持。和Session真正相关的其实只有ISessionStore,话不多说,继续向下看DistributedSessionStore实现 Copypublic class DistributedSessionStore : ISessionStore{ private readonly IDistributedCache _cache; private readonly ILoggerFactory _loggerFactory; public DistributedSessionStore(IDistributedCache cache, ILoggerFactory loggerFactory) { if (cache == null) { throw new ArgumentNullException(nameof(cache)); } if (loggerFactory == null) { throw new ArgumentNullException(nameof(loggerFactory)); } _cache = cache; _loggerFactory = loggerFactory; } public ISession Create(string sessionKey, TimeSpan idleTimeout, TimeSpan ioTimeout, Func<bool> tryEstablishSession, bool isNewSessionKey) { if (string.IsNullOrEmpty(sessionKey)) { throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(sessionKey)); } if (tryEstablishSession == null) { throw new ArgumentNullException(nameof(tryEstablishSession)); } return new DistributedSession(_cache, sessionKey, idleTimeout, ioTimeout, tryEstablishSession, _loggerFactory, isNewSessionKey); } }这里的实现也非常简单就是创建Session实例DistributedSession,在这里我们就可以看出创建Session是依赖IDistributedCache的,这里的sessionKey其实是SessionID,当前会话唯一标识。继续向下找到DistributedSession实现,这里的代码比较多,因为这是封装Session操作的实现类。老规矩先找到我们最容易下手的Get方法 Copypublic bool TryGetValue(string key, out byte[] value){ Load(); return _store.TryGetValue(new EncodedKey(key), out value); }我们看到调用TryGetValue之前先调用了Load方法,这是内部的私有方法 Copyprivate void Load(){ //判断当前会话中有没有加载过数据 if (!_loaded) { try { //根据会话唯一标识在IDistributedCache中获取数据 var data = _cache.Get(_sessionKey); if (data != null) { //由于存储的是按照特定的规则得到的二进制数据,所以获取的时候要将数据反序列化 Deserialize(new MemoryStream(data)); } else if (!_isNewSessionKey) { _logger.AccessingExpiredSession(_sessionKey); } //是否可用标识 _isAvailable = true; } catch (Exception exception) { _logger.SessionCacheReadException(_sessionKey, exception); _isAvailable = false; _sessionId = string.Empty; _sessionIdBytes = null; _store = new NoOpSessionStore(); } finally { //将数据标识设置为已加载状态 _loaded = true; } } } private void Deserialize(Stream content){ if (content == null || content.ReadByte() != SerializationRevision) { // Replace the un-readable format. _isModified = true; return; } int expectedEntries = DeserializeNumFrom3Bytes(content); _sessionIdBytes = ReadBytes(content, IdByteCount); for (int i = 0; i < expectedEntries; i++) { int keyLength = DeserializeNumFrom2Bytes(content); //在存储的数据中按照规则获取存储设置的具体key var key = new EncodedKey(ReadBytes(content, keyLength)); int dataLength = DeserializeNumFrom4Bytes(content); //将反序列化之后的数据存储到_store _store[key] = ReadBytes(content, dataLength); } if (_logger.IsEnabled(LogLevel.Debug)) { _sessionId = new Guid(_sessionIdBytes).ToString(); _logger.SessionLoaded(_sessionKey, _sessionId, expectedEntries); } }通过上面的代码我们可以得知Get数据之前之前先Load数据,Load其实就是在IDistributedCache中获取数据然后存储到了_store中,通过当前类源码可知_store是本地字典,也就是说Session直接获取的其实是本地字典里的数据。 Copyprivate IDictionary _store;这里其实产生两点疑问:1.针对每个会话存储到IDistributedCache的其实都在一个Key里,就是以当前会话唯一标识为key的value里,为什么没有采取组合会话key单独存储。2.每次请求第一次操作Session,都会把IDistributedCache里针对当前会话的数据全部加载到本地字典里,一般来说每次会话操作Session的次数并不会很多,感觉并不会节约性能。接下来我们在再来查看另一个我们比较熟悉的方法Set方法 Copypublic void Set(string key, byte[] value){ if (value == null) { throw new ArgumentNullException(nameof(value)); } if (IsAvailable) { //存储的key是被编码过的 var encodedKey = new EncodedKey(key); if (encodedKey.KeyBytes.Length > KeyLengthLimit) { throw new ArgumentOutOfRangeException(nameof(key), Resources.FormatException_KeyLengthIsExceeded(KeyLengthLimit)); } if (!_tryEstablishSession()) { throw new InvalidOperationException(Resources.Exception_InvalidSessionEstablishment); } //是否修改过标识 _isModified = true; //将原始内容转换为byte数组 byte[] copy = new byte[value.Length]; Buffer.BlockCopy(src: value, srcOffset: 0, dst: copy, dstOffset: 0, count: value.Length); //将数据存储到本地字典_store _store[encodedKey] = copy; } }这里我们可以看到Set方法并没有将数据放入到存储系统,只是放入了本地字典里。我们再来看其他方法 Copypublic void Remove(string key){ Load(); _isModified |= _store.Remove(new EncodedKey(key)); } public void Clear(){ Load(); _isModified |= _store.Count > 0; _store.Clear(); }这些方法都没有对存储系统DistributedCache里的数据进行操作,都只是操作从存储系统Load到本地的字典数据。那什么地方进行的存储呢,也就是说我们要找到调用_cache.Set方法的地方,最后在这个地方找到了Set方法,而且看这个方法名就知道是提交Session数据的地方 Copypublic async Task CommitAsync(CancellationToken cancellationToken = default){ //超过_ioTimeout CancellationToken将自动取消 using (var timeout = new CancellationTokenSource(_ioTimeout)) { var cts = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken); //数据被修改过 if (_isModified) { if (_logger.IsEnabled(LogLevel.Information)) { try { cts.Token.ThrowIfCancellationRequested(); var data = await _cache.GetAsync(_sessionKey, cts.Token); if (data == null) { _logger.SessionStarted(_sessionKey, Id); } } catch (OperationCanceledException) { } catch (Exception exception) { _logger.SessionCacheReadException(_sessionKey, exception); } } var stream = new MemoryStream(); //将_store字典里的数据写到stream里 Serialize(stream); try { cts.Token.ThrowIfCancellationRequested(); //将读取_store的流写入到DistributedCache存储里 await _cache.SetAsync( _sessionKey, stream.ToArray(), new DistributedCacheEntryOptions().SetSlidingExpiration(_idleTimeout), cts.Token); _isModified = false; _logger.SessionStored(_sessionKey, Id, _store.Count); } catch (OperationCanceledException oex) { if (timeout.Token.IsCancellationRequested) { _logger.SessionCommitTimeout(); throw new OperationCanceledException("Timed out committing the session.", oex, timeout.Token); } throw; } } else { try { await _cache.RefreshAsync(_sessionKey, cts.Token); } catch (OperationCanceledException oex) { if (timeout.Token.IsCancellationRequested) { _logger.SessionRefreshTimeout(); throw new OperationCanceledException("Timed out refreshing the session.", oex, timeout.Token); } throw; } } } } private void Serialize(Stream output){ output.WriteByte(SerializationRevision); SerializeNumAs3Bytes(output, _store.Count); output.Write(IdBytes, 0, IdByteCount); //将_store字典里的数据写到Stream里 foreach (var entry in _store) { var keyBytes = entry.Key.KeyBytes; SerializeNumAs2Bytes(output, keyBytes.Length); output.Write(keyBytes, 0, keyBytes.Length); SerializeNumAs4Bytes(output, entry.Value.Length); output.Write(entry.Value, 0, entry.Value.Length); } }那么问题来了当前类里并没有地方调用CommitAsync,那么到底是在什么地方调用的该方法呢?姑且别着急,我们之前说过使用Session的三要素,现在才说了两个,还有一个UseSession的中间件没有提及到呢。 UseSession中间件#通过上面注册的相关方法我们大概了解到了Session的工作原理。接下来我们查看UseSession中间件里的代码,探究这里究竟做了什么操作。我们找到UseSession方法所在的地方SessionMiddlewareExtensions找到第一个方法 Copypublic static IApplicationBuilder UseSession(this IApplicationBuilder app){ if (app == null) { throw new ArgumentNullException(nameof(app)); } return app.UseMiddleware<SessionMiddleware>(); }SessionMiddleware的源码 Copypublic class SessionMiddleware{ private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create(); private const int SessionKeyLength = 36; // "382c74c3-721d-4f34-80e5-57657b6cbc27" private static readonly Func ReturnTrue = () => true; private readonly RequestDelegate _next; private readonly SessionOptions _options; private readonly ILogger _logger; private readonly ISessionStore _sessionStore; private readonly IDataProtector _dataProtector; public SessionMiddleware( RequestDelegate next, ILoggerFactory loggerFactory, IDataProtectionProvider dataProtectionProvider, ISessionStore sessionStore, IOptions<SessionOptions> options) { if (next == null) { throw new ArgumentNullException(nameof(next)); } if (loggerFactory == null) { throw new ArgumentNullException(nameof(loggerFactory)); } if (dataProtectionProvider == null) { throw new ArgumentNullException(nameof(dataProtectionProvider)); } if (sessionStore == null) { throw new ArgumentNullException(nameof(sessionStore)); } if (options == null) { throw new ArgumentNullException(nameof(options)); } _next = next; _logger = loggerFactory.CreateLogger<SessionMiddleware>(); _dataProtector = dataProtectionProvider.CreateProtector(nameof(SessionMiddleware)); _options = options.Value; //Session操作类在这里被注入的 _sessionStore = sessionStore; } public async Task Invoke(HttpContext context) { var isNewSessionKey = false; Func<bool> tryEstablishSession = ReturnTrue; var cookieValue = context.Request.Cookies[_options.Cookie.Name]; var sessionKey = CookieProtection.Unprotect(_dataProtector, cookieValue, _logger); //会话首次建立 if (string.IsNullOrWhiteSpace(sessionKey) || sessionKey.Length != SessionKeyLength) { //将会话唯一标识通过Cookie返回到客户端 var guidBytes = new byte[16]; CryptoRandom.GetBytes(guidBytes); sessionKey = new Guid(guidBytes).ToString(); cookieValue = CookieProtection.Protect(_dataProtector, sessionKey); var establisher = new SessionEstablisher(context, cookieValue, _options); tryEstablishSession = establisher.TryEstablishSession; isNewSessionKey = true; } var feature = new SessionFeature(); //创建Session feature.Session = _sessionStore.Create(sessionKey, _options.IdleTimeout, _options.IOTimeout, tryEstablishSession, isNewSessionKey); //放入到ISessionFeature,给HttpContext中的Session数据提供具体实例 context.Features.Set<ISessionFeature>(feature); try { await _next(context); } finally { //置空为了在请求结束后可以回收掉Session context.Features.Set<ISessionFeature>(null); if (feature.Session != null) { try { //请求完成后提交保存Session字典里的数据到DistributedCache存储里 await feature.Session.CommitAsync(); } catch (OperationCanceledException) { _logger.SessionCommitCanceled(); } catch (Exception ex) { _logger.ErrorClosingTheSession(ex); } } } } private class SessionEstablisher { private readonly HttpContext _context; private readonly string _cookieValue; private readonly SessionOptions _options; private bool _shouldEstablishSession; public SessionEstablisher(HttpContext context, string cookieValue, SessionOptions options) { _context = context; _cookieValue = cookieValue; _options = options; context.Response.OnStarting(OnStartingCallback, state: this); } private static Task OnStartingCallback(object state) { var establisher = (SessionEstablisher)state; if (establisher._shouldEstablishSession) { establisher.SetCookie(); } return Task.FromResult(0); } private void SetCookie() { //会话标识写入到Cookie操作 var cookieOptions = _options.Cookie.Build(_context); var response = _context.Response; response.Cookies.Append(_options.Cookie.Name, _cookieValue, cookieOptions); var responseHeaders = response.Headers; responseHeaders[HeaderNames.CacheControl] = "no-cache"; responseHeaders[HeaderNames.Pragma] = "no-cache"; responseHeaders[HeaderNames.Expires] = "-1"; } internal bool TryEstablishSession() { return (_shouldEstablishSession |= !_context.Response.HasStarted); } }}通过SessionMiddleware中间件里的代码我们了解到了每次请求Session的创建,以及Session里的数据保存到DistributedCache都是在这里进行的。不过这里仍存在一个疑问由于调用CommitAsync是在中间件执行完成后统一进行存储的,也就是说中途对Session进行的Set Remove Clear的操作都是在Session方法的本地字典里进行的,并没有同步到DistributedCache里,如果中途出现程序异常结束的情况下,保存到Session里的数据,并没有真正的存储下来,会出现丢失的情况,不知道在设计这部分逻辑的时候是出于什么样的考虑。 总结#通过阅读Session相关的部分源码大致了解了Session的原理,工作三要素,IDistributedCache存储Session里的数据,SessionStore是Session的实现类,UseSession是Session被创建到当前请求的地方。同时也留下了几点疑问 针对每个会话存储到IDistributedCache的其实都在一个Key里,就是以当前会话唯一标识为key的value里,为什么没有采取组合会话key单独存储。每次请求第一次操作Session,都会把IDistributedCache里针对当前会话的数据全部加载到本地字典里,一般来说每次会话操作Session的次数并不会很多,感觉并不会节约性能。调用CommitAsync是在中间件执行完成后统一进行存储的,也就是说中途对Session进行的Set Remove Clear的操作都是在Session方法的本地字典里进行的,并没有同步到DistributedCache里,如果中途出现程序异常结束的情况下,保存到Session里的数据,并没有真正的存储下来,会出现丢失的情况。对于以上疑问,不知道是个人理解不足,还是在设计的时候出于别的考虑。欢迎在评论区多多沟通交流,希望能从大家那里得到更好的解释和答案。 作者: yi念之间 出处:https://www.cnblogs.com/wucy/p/13044467.html
Python 为什么没有 main 函数?为什么我不推荐写 main 函数? 毫无疑问 Python 中没有所谓的 main 入口函数,但是网上经常看到一些文章提“Python 的 main 函数”、“建议写 main 函数”…… 有些人是知情的,他的意图可能是模仿那些正宗的 main 函数,但还有不少人明显是被误导了(或自己误解了),就写出来很累赘的代码。 本期“Python 为什么”栏目来聊聊 Python 为什么没有 main 函数? 在开始正题之前,先要来回答这两个问题:所谓的 “main 函数”是指什么?为什么有些编程语言需要强制写一个 main 函数? 某些编程语言以 main 函数作为程序的执行入口,例如 C/C++、C#、 Java、Go 和 Rust 等,它们具有特定的含义: main 函数名是强制的,也就是要求必须有一个 main 函数main 函数最多只能有一个,也就是说程序的入口是唯一的语法格式有一定的要求,具有相对固定的模板为什么要强制一个 main 入口函数呢? 这些语言是编译型语言,需要把代码编译成可执行的二进制文件,为了让操作系统/启动器找到程序的起点,所以要约定这一个函数。简单地说,就是在一大堆代码里,需要定义一个显著的可用于执行的开头。 不难看出,main 函数是那些语言中重要而不可缺的有机组成部分。 然而,我们再来看看 Python,情况就大不相同了。 Python 是解释型语言,即脚本语言,运行过程是从上往下,逐行解析运行,也就是说它的起点是可知的每个 .py 文件就是一个可执行文件,都可作为整个程序的入口文件,也就是说程序的入口是灵活可变的,没有必须遵守的约定有时候运行 Python 项目,并没有指定入口文件(命令行中较常见,例如"python -m http.server 8000"), 那可能是存在 __main__.py 文件,它所在的包被当成一个“文件”来执行了归结起来,意思是说 Python 这种脚本语言跟编译型语言不同,它不管是在单个模块层面(即一个 .py 文件),还是在由多个模块组成的包层面,都可选择灵活的执行方式,不像其它语言缺了约定好的入口就没法执行。 也就是说,Python 没有必要在语法层面规定程序员必须定义出一个统一的入口(不管是函数还是类还是什么东西)。 有些同学可能会有疑惑,因为他们经常看到或者自己写出下面这样的代码: main 里是某些主体代码 def main(): …… if name == '__main__': main() 难道这不就是 Python 的 main 函数么?相信有不少同学会这么想! 非也!非也! 除了函数名是“main”以外,它跟我们前面介绍的正统的 main 函数没有半毛钱关系,既没有强制性,也没有必然决定程序执行顺序的作用。缺少它,也不会导致什么语法问题。 之所以有些知情人要命名出一个”main“函数,其实是想强调它的”主要“地位,想要人为地安排它作为第一个执行的函数。他们可能认为这样命名的函数,比较容易记忆。 之所以有些知情人要写if name == '__main__' ,可能想表明 main() 只有在当前脚本被直接执行时才运行,不希望被导入其它模块时运行。 对于这些“知情人”,他们有一定的道理。 但是,我个人并不推荐这种写法,甚至有时候会非常反感! 最明显的例子:明明只有几十行代码,或者仅有一个脚本文件,实现一个简单的功能(一小段爬虫、用 turtle 画张图等等),但是它们都按前面的样式写了。 我每次看到这种不假思索的累赘代码,就觉得难受。为什么要写那行 if 语句呢?可能的话,应该拆分 main 函数,甚至不必封装成一个函数啊! 我个人总结出以下的经验: 打破惯性思维,写出地道的代码。main 入口函数是某些语言特有的,不该在 Python 中“照猫画虎”,应该了解脚本语言的特点,写出简洁优雅的风格使用 main.py 而非 main()。因为 Python 的程序执行单位其实是脚本文件,而非某个函数或者类,所以建议把入口文件命名为 main.py,内部的函数按需求而定可以的话,使用__main__.py 作为入口文件。这个文件结合命令行的“-m”参数使用,非常好用。推荐阅读:Python 中 -m 的典型用法、原理解析与发展演变不推荐写if name == '__main__' 。首先,如果只有一个文件的话,因为不存在导出的可能,不建议写。其次,存在多文件时,入口文件(main.py)中极不推荐写这一句,此文件的代码逻辑应该精炼,理论上其内容不该被导出到其它模块使用,因为它是起点!最后,多文件的非入口文件也不建议写,因为在非入口文件中写这个判断,最大的作用就是写一些测试代码,但是测试代码应该分离出来,写到专门的目录或文件中。小结:本文首先解释了什么是 main 入口函数,以及为什么某些语言会强制要求写 main 函数;接着,解释了为什么 Python 不需要写 main 函数;最后则是针对某些人存在的惯性误区,分享了我个人的四点编程经验。 本文属于“Python为什么”系列文章(Python猫出品),该系列主要关注 Python 的语法、设计和发展等话题,以一个个“为什么”式的问题为切入点,试着展现 Python 的迷人魅力。部分话题会推出视频版,请在 B 站收看,观看地址:视频地址 原文地址https://www.cnblogs.com/pythonista/p/13040342.html
【asp.net core 系列】3 视图以及视图与控制器 0.前言在之前的几篇中,我们大概介绍了如何创建一个asp.net core mvc项目以及http请求如何被路由转交给对应的执行单元。这一篇我们将介绍一下控制器与视图直接的关系。 视图这里的视图不是数据库里的视图,是一种展示技术。在asp.net core mvc项目中视图是指以cshtml做扩展名的文件,通常在Views文件夹。 那么现在我们进到之前创建的测试项目 MvcWeb的Views目录下,如果小伙伴们没有做修改的话,能看到如下的目录结构: ├── Home│ ├── Index.cshtml│ └── Privacy.cshtml├── Shared│ ├── Error.cshtml│ ├── _Layout.cshtml│ └── _ValidationScriptsPartial.cshtml├── _ViewImports.cshtml└── _ViewStart.cshtml在Views根目录下,有两个文件分别是:_ViewImports.cshtml 、_ViewStart.cshtml 两个文件(注意,有个前置下划线)。 1.1 在视图中引用命名空间我们知道,在cshtml文件中,虽然极大的减少了服务器代码,但是有时候无法避免的使用一些C#代码。那么就会产生一个问题,很多类都有自己的命名空间,如果我们在某个或某几个或某些视图中需要访问这些类和方法,那么一个视图一个视图的写引用有点不太现实,因为这太繁琐了。 所以asp.net core mvc 设置了在名为_ViewImports.cshtml的文件中添加引用,则在Views下所有视图中都生效。那么,先来看看这个文件里有啥吧: @using MvcWeb@using MvcWeb.Models@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers可以看到,这里引用了项目的命名空间和项目下Modes命名空间的所有内容。因为我们之前创建的测试项目名称就是 MvcWeb。 最后一行是一个 cshtml标记引用,第一个星号表示当前项目的所有TagHelper实现都引用,后面的表示引入aps.net core mvc内置的TagHelper。 关于 TagHelper,这篇就先不介绍了。 1.2 ViewsStart_ViewStart.cshtml 作用从名字中可见一二,这个文件用来配置一些在视图刚开始加载时的一些配置内容。先看一下,默认的里面是什么吧: @{ Layout = "_Layout"; }先做个介绍,@符号后面用一对大括号包裹,里面是C# 代码。也就是说 Layout = "_Layout",这行的意思是给某个名为Layout的属性设置值为_Layout。 那么,Layout的属性是哪里的呢? 对于asp.net core mvc而言,一个视图也是一个类只不过这个类是动态生成的,不是一个由程序员编写出来的类,但是这个类继承自: namespace Microsoft.AspNetCore.Mvc.Razor{ public abstract class RazorPageBase : IRazorPage { } }Layout正好是这个类的一个属性,表示视图是否使用了某个布局页。所以上面的代码表示,Views里的新建视图,默认是使用名为_Layout的视图作为布局页。 当然,这个页面不只有这个作用,小伙伴们可以自己尝试下哦。 1.3 视图检索在上一节中,我们指定了一个布局页的名称。布局页也是视图中的一种,但我们也只指定了名称,但没有指定路径。asp.net core是如何发现这个名称的视图呢? asp.net core 会按照以下顺序查找对应的视图文件: Views/[ControllerName]/[ViewName].cshtmlViews/Shared/[ViewName].cshtml所以,_Layout也会按照这个顺序查找,为了避免不必要的混淆,我们只在Shared目录下写了_Layout.cshtml。这也是通常的做法,该文件表示一个全局的布局页。 控制器与视图的关系在上一篇《【asp.net core 系列】2 控制器与路由的恩怨情仇》中,我们介绍了三种创建控制器的方法,并且最后推荐使用名字以Controller结尾并继承Controller类的写法。我将在这里为大家再次讲解为什么推荐这样写: 以Controller结尾,可以很明确的告诉其他人或者未来的自己这是一个控制器,不是别的类继承Controller,是因为Controller类为我们提供了控制器用到的属性和方法嗯,暂时就这两点。别看少,但是这很重要。 2.1 使用视图在之前介绍的时候,有提到过当我们访问一个URL的时候,路由会自动为我们寻找到对应的可执行代码单元。但是,没有进一步内容的介绍。当我们寻找到对应的可执行代码单元也就是Action之后,Action进行一系列的处理,会对这个请求做出响应。有一种响应就是返回一个展示页面,也就是View。 那么,如何返回一个View呢? 创建一个控制器,名为ViewDemoController,并添加一个方法Index,返回类型为IActionResult: using Microsoft.AspNetCore.Mvc; namespace MvcWeb.Controllers{ public class ViewDemoController:Controller { public IActionResult Index() { return View(); } } }其中 View() 表示返回一个View,这View的名称是 Index,在ViewDemo控制器下。所以,它的路径应该是: Views/ViewDemo/Index.cshtml在对应目录创建该文件,然后在文件里随便写一些内容,之后启动项目(项目的端口在第一部分就已经修改过了): http://localhost:5006 然后访问: http://localhost:5006/ViewDemo/ 应该是类似的页面。 IActionResult 是一个接口,表示是一个Action的处理结果,在这里可以理解为固定写法。 2.2 指定视图在控制器里,View 方法表示使用一个视图进行渲染,默认是使用方法同名的视图。当然,既然是默认的,那就一定有不默认的时候。对的,View方法提供了几个重载版本,这些重载版本里有一个名字为viewName的参数,这个参数就是用来指定视图名称的。 那么,我们可以指定哪些视图名称: 同一个控制器文件夹下的其他视图Shared 文件夹下的视图这两种都是不用携带路径的视图名,可以省略文件扩展名(cshtml)。 当然,还可以指定其他路径下的视图文件,如: Views/Home/About.cshtml表示从根目录下查找到这个视图,这种写法必须指定扩展名../Manage/Index 表示在Manage控制器目录下的Index2.3 给视图传递数据之前介绍了如何使用视图、如何指定视图名称,但是还缺最关键的一步,那就是如何给视图传递数据。 通常情况下,Action方法中给视图传递数据,只有这三种是推荐的: 使用ViewData使用ViewDataAttribute使用ViewBag使用ViewModelController类有一个属性是 ViewData,它的声明如下: public ViewDataDictionary ViewData { get; set; }可以看到这是一个字典型的属性,所以给它赋值是这样使用的: public IActionResult Index(){ ViewData["Title"] = "ViewDemo"; return View(); }ViewBag也是 Controller类的一个属性,它的声明如下: public dynamic ViewBag { get; }可以看到这是一个动态类,实际上ViewBag里的数据与ViewData是互通的,换句话说就是ViewBag是对ViewData的一次封装,两者并没有实际上的区别。赋值使用: public IActionResult Index(){ ViewBag.Name = "小李"; return View(); }而ViewDataAttribute则与上两个,不太一样,这个属性标注给控制器的属性上,asp.net core mvc就会把这个属性的值填充给ViewData,键值就是属性名: [ViewData]public string AttributeTest{get;set;}与 ViewData["AttributeTest"]效果一致。 在View方法的一些重载版本里,需要一个名为 model的参数,类型是object。这个参数就是一个ViewModel。使用: 在MvcWeb/Models 下添加一个类: namespace MvcWeb.Models{ public class ViewModelTestModel { public string Name{get;set;} public int Age{get;set;} } }回到刚刚的Index方法里,创建一个ViewModelTestModel实例,并传给View方法: public IActionResult Index(){ ViewData["Title"] = "ViewDemo"; ViewBag.Name = "小李"; var model = new ViewModelTestModel { Name = "测试实例", Age = 1 }; return View(model); }2.4 在视图中使用在上一小节中,我们分别使用ViewData和ViewBag以及ViewModel给视图传递了三个数据,那么如何在视图中获取这三个数据呢? @ViewData["Title"] ViewDemo-->与字典一样,@起头,表示后面跟着一个属性或者一段C#表达式,并将表达式的结果输出到页面上。ViewBag的访问与ViewData类似,只不过ViewBag是动态对象,可以认为它的类型并没有发生改变,继续按照之前的类型进行使用: @ViewBag.Name 对于ViewModel的使用,View内置了一个dynamic的Model属性,在不做特殊处理的情况下,我们在页面上使用@Model 会得到一个dynamic对象(如果传了ViewModel的话)。虽然也能用,但是这不太友好。这时候,就需要我们在视图的开头处,添加: @model ViewModelTestModel这时候,再使用@Model的时候,就会自动解析成ViewModelTestModel了。 整体Index.cshtml内容如下: @model ViewModelTestModelHello World! @ViewData["Title"] @ViewBag.Name @Model.Name + @Model.Age然后重启服务后,刷新页面,会看到类似的内容: 总结我们在这一篇介绍了视图的一些概念,并介绍了如何使用控制器给视图传递数据。下一篇将讲解一下路由的高级作用,如何通过路由携带数据。 原文地址https://www.cnblogs.com/c7jie/p/13034221.html
Telegraf和Grafana监控多平台上的SQL Server 问题SQL Server在很多企业中部署在多个平台上(Windows,Linux和Container),需要一种能支持多平台的解决方案用于收集和展示相关的监控指标。 我选择企业中比较流行的监控展示工具Grafana和监控指标收集工具Telegraf进行实现。这也是为了方便与企业中已经在存在监控平台进行整合和对接。 如上图所示,Telegraf部署在SQL所在host,收集数据发送给时序数据库Influxdb存储,然后Grafana用于展示数据。 解决方案安装和配置InfluxDB我将InfluxDB和Grafana安装在同一台CentOS主机上,生产环境中最好是分开。 下载1.8的stable version后进行安装 wget https://dl.influxdata.com/influxdb/releases/influxdb-1.8.0.x86_64.rpmchmod 755 influxdb-1.8.0.x86_64.rpmyum localinstall influxdb-1.8.0.x86_64.rpm 启动并设置自启动 systemctl start influxdbsystemctl enable influxdb 8086用于客户端的HTTP连接,8088用于CLI调用RPC进行备份和还原操作 firewall-cmd --zone=public --add-port=8086/tcp --permanentfirewall-cmd --zone=public --add-port=8088/tcp --permanentfirewall-cmd --reload 连接到influxdb并创建用户 fluxdb CREATE USER admin WITH PASSWORD '' WITH ALL PRIVILEGES 启用http用户验证,修改influxdb.conf中http section中auth-enabled = true vim /etc/influxdb/influxdb.confsystemctl restart influxdb 创建用于存储监控数据的数据库,保存6个月的数据 influx -username 'admin' -password '' CREATE DATABASE telegrafCREATE RETENTION POLICY telegraf_6m ON telegraf DURATION 180d REPLICATION 1 DEFAULTSHOW DATABASES 安装和配置Grafana 下载并安装Grafana wget https://dl.grafana.com/oss/release/grafana-7.0.1-1.x86_64.rpmchmod 775 grafana-7.0.1-1.x86_64.rpmyum localinstall grafana-7.0.1-1.x86_64.rpm 设置自启动 systemctl start grafana-server.servicesystemctl enable grafana-server.service 允许Grafana默认的端口3000 firewall-cmd --zone=public --add-port=3000/tcp --permanentfirewall-cmd --reload然后在Browser中访问http://:3000,第一次访问时默登录认账号和密码都为admin,登录后会提示修改密码。 在客户端主机安装和配置Telegraf所谓客户端,就是SQL所在主机 Telegraf连接到SQL,需要一个login,具有 VIEW SERVER STATE and VIEW ANY DEFINITION的权限,所以在每个被监控的实例上都需要创建之。 USE master;GOCREATE LOGIN [telegraf] WITH PASSWORD = N'1qaz@WSX';GOGRANT VIEW SERVER STATE TO [telegraf];GOGRANT VIEW ANY DEFINITION TO [telegraf];GOTelegraf on Linuxwget https://dl.influxdata.com/telegraf/releases/telegraf-1.14.3-1.x86_64.rpmsudo yum localinstall telegraf-1.14.3-1.x86_64.rpm 安装完成后,先要修改Telegraf的配置文件,再启动。在配置文件中主要配置两个部分:inputs和outputs。 inputs表示监控数据从哪里来,outputs表示监控要发送到哪里去。 打开/etc/telegraf/telegraf.conf,找到[[outputs.influxdb]]部分,所有配置项默认都被注释了。我们需要删除注释并配置一些项。主要是Influxdb的地址,用户名、密码和数据库名等。 [[outputs.influxdb]] ## The full HTTP or UDP URL for your InfluxDB instance. ## ## Multiple URLs can be specified for a single cluster, only ONE of the ## urls will be written to each interval. # urls = ["unix:///var/run/influxdb.sock"] # urls = ["udp://127.0.0.1:8089"] urls = ["http://172.17.2.4:8086"] ## The target database for metrics; will be created as needed. ## For UDP url endpoint database needs to be configured on server side. database = "telegraf" ## The value of this tag will be used to determine the database. If this ## tag is not set the 'database' option is used as the default. # database_tag = "" ## If true, the 'database_tag' will not be included in the written metric. # exclude_database_tag = false ## If true, no CREATE DATABASE queries will be sent. Set to true when using ## Telegraf with a user without permissions to create databases or when the ## database already exists. skip_database_creation = true ## Name of existing retention policy to write to. Empty string writes to ## the default retention policy. Only takes effect when using HTTP. retention_policy = "" ## The value of this tag will be used to determine the retention policy. If this ## tag is not set the 'retention_policy' option is used as the default. # retention_policy_tag = "" ## If true, the 'retention_policy_tag' will not be included in the written metric. # exclude_retention_policy_tag = false ## Write consistency (clusters only), can be: "any", "one", "quorum", "all". ## Only takes effect when using HTTP. write_consistency = "any" ## Timeout for HTTP messages. timeout = "5s" ## HTTP Basic Auth username = "admin" password = ""找到[[inputs.sqlserver]]部分,取消相关配置项的注释,servers部分连接到本地实例。Telegraf默认的Plugin中包括了对SQL Server的实现, 这个Plugin还包括了对Azure SQL PaaS的实现 Read metrics from Microsoft SQL Server [[inputs.sqlserver]] Specify instances to monitor with a list of connection strings. All connection parameters are optional. By default, the host is localhost, listening on default port, TCP 1433. for Windows, the user is the currently running AD user (SSO). See https://github.com/denisenkom/go-mssqldb for detailed connection parameters, in particular, tls connections can be created like so: "encrypt=true;certificate=;hostNameInCertificate=" servers = [ "Server=localhost;Port=1433;User Id=telegraf;Password=<yourPassword>;app name=telegraf;log=1;" ] Optional parameter, setting this to 2 will use a new version of the collection queries that break compatibility with the original dashboards. query_version = 2 If you are using AzureDB, setting this to true will gather resource utilization metrics azuredb = false Possible queries: - PerformanceCounters - WaitStatsCategorized - DatabaseIO - DatabaseProperties - CPUHistory - DatabaseSize - DatabaseStats - MemoryClerk - VolumeSpace - PerformanceMetrics - Schedulers - AzureDBResourceStats - AzureDBResourceGovernance - SqlRequests - ServerProperties A list of queries to include. If not specified, all the above listed queries are used. include_query = [] A list of queries to explicitly ignore. exclude_query = [ 'Schedulers' , 'SqlRequests'] 启动Telegraf之后,可以看到时已经加载的inputs和收集间隔 [root@SQL19N1 log]# systemctl status telegraf ● telegraf.service - The plugin-driven server agent for reporting metrics into InfluxDB Loaded: loaded (/usr/lib/systemd/system/telegraf.service; enabled; vendor preset: disabled) Active: active (running) since Tue 2020-05-26 14:19:07 UTC; 19min ago Docs: https://github.com/influxdata/telegraf Main PID: 12359 (telegraf) CGroup: /system.slice/telegraf.service └─12359 /usr/bin/telegraf -config /etc/telegraf/telegraf.conf -config-directory /etc/telegraf/telegraf.d May 26 14:19:07 SQL19N1 systemd[1]: Started The plugin-driven server agent for reporting metrics into InfluxDB.May 26 14:19:07 SQL19N1 telegraf[12359]: 2020-05-26T14:19:07Z I! Starting Telegraf 1.14.3May 26 14:19:07 SQL19N1 telegraf[12359]: 2020-05-26T14:19:07Z I! Loaded inputs: system cpu disk diskio kernel mem processes swap sqlserverMay 26 14:19:07 SQL19N1 telegraf[12359]: 2020-05-26T14:19:07Z I! Loaded aggregators:May 26 14:19:07 SQL19N1 telegraf[12359]: 2020-05-26T14:19:07Z I! Loaded processors:May 26 14:19:07 SQL19N1 telegraf[12359]: 2020-05-26T14:19:07Z I! Loaded outputs: influxdbMay 26 14:19:07 SQL19N1 telegraf[12359]: 2020-05-26T14:19:07Z I! Tags enabled: host=SQL19N1May 26 14:19:07 SQL19N1 telegraf[12359]: 2020-05-26T14:19:07Z I! [agent] Config: Interval:20s, Quiet:false, Hostname:"SQL19N1", Flush Interval:10sTelegraf on Windows以管理员身份执行如下PowerShell命令 下载软件 wget https://dl.influxdata.com/telegraf/releases/telegraf-1.14.3_windows_amd64.zip · -OutFile "c:\temp\telegraf-1.14.3_windows_amd64.zip" 解压缩到C:Program FilesTelegraf Expand-Archive "c:temptelegraf-1.14.3_windows_amd64.zip", "C:Program Files" 将telegraf安装为windows服务 C:"Program Files"Telegraftelegraf.exe --service install修改telegraf.conf中outputs.influxdb和添加inputs.sqlserver部分,这些内容和在Linux上的配置一样,就不赘述了。 conf修改完成后,可以先测试一下telegraf是否能正常启动,没问题的话就启动telegraf服务。 测试 C:"Program Files"Telegraftelegraf.exe --config C:"Program Files"Telegraftelegraf.conf --test 启动服务 C:"Program Files"Telegraftelegraf.exe --service start配置Grafana的数据源和Dashboard登录Grafana后,在左侧的Configuration->Data Source中配置InfluxDB数据源,填写地址、账号、密码并设置为默认数据源,如下图 Dashboard,可以自己创建,也可以在采用公开社区的(感谢热心无私的大佬们)。这里,我采用SQL Servers by Jonathan Rioux。这个Dashboard中使用的Piechart不是Grafana预置的,所以还需要安装: Grafana所在Host安装,重启服务生效 grafana-cli plugins install grafana-piechart-panelsystemctl restart grafana-server.service然后在Grafana界面,选择左侧的Dashboard->Import->填入Dashboard ID->Import,如下图: 配置完成后的,可以看这个Dashboard提供的信息还比较丰富的,您也可以根据自己的需要修改和添加相关内容. 总结实际情况中,自带的数据收集和报表不能完全满足业务需求,自定义的数据收集和自定义的Dashboard,也是非常容易实现的,下次再写如果已经在使用Zabbix了,Grafana可以直接对接到Zabbix的数据输出。Telegraf能非常好的支持Cloud环境,下次说说对Azure SQL PaaS的监控 本文内容仅代表个人观点,与任何公司和组织无关 作者:Joe.TJ 原文地址https://www.cnblogs.com/Joe-T/p/12973362.html
看了这篇,我确定你已经彻底搞懂Java的继承了 遇到认真的读者是作者的一种幸运,真的,上一篇接口推送后,有好几个读者留言说,“二哥,你有一处内容需要修正,应该是接口中不能有 private 和 protected 修饰的方法。”说实话,看到这样的留言,我内心是非常欣慰的,因为你投出去的一块石头在水面上激起了一串美丽的涟漪。 在 Java 中,一个类可以继承另外一个类或者实现多个接口,我想这一点,大部分的读者应该都知道了。还有一点,我不确定大家是否知道,就是一个接口也可以继承另外一个接口,就像下面这样: public interface OneInterface extends Cloneable {}这样做有什么好处呢?我想有一部分读者应该已经猜出来了,就是实现了 OneInterface 接口的类,也可以使用 Object.clone() 方法了。 public class TestInterface implements OneInterface { public static void main(String[] args) throws CloneNotSupportedException { TestInterface c1 = new TestInterface(); TestInterface c2 = (TestInterface) c1.clone(); }}除此之外,我们还可以在 OneInterface 接口中定义其他一些抽象方法(比如说深拷贝),使该接口拥有 Cloneable 所不具有的功能。 public interface OneInterface extends Cloneable { void deepClone();}看到了吧?这就是继承的好处:子接口拥有了父接口的方法,使得子接口具有了父接口相同的行为;同时,子接口还可以在此基础上自由发挥,添加属于自己的行为。 以上,把“接口”换成“类”,结论同样成立。让我们来定义一个普通的父类 Wanger: public class Wanger { int age; String name; void write() { System.out.println("我写了本《基督山伯爵》"); }}然后,我们再来定义一个子类 Wangxiaoer,使用关键字 extends 来继承父类 Wanger: public class Wangxiaoer extends Wanger{ @Override void write() { System.out.println("我写了本《茶花女》"); }}我们可以将通用的方法和成员变量放在父类中,达到代码复用的目的;然后将特殊的方法和成员变量放在子类中,除此之外,子类还可以覆盖父类的方法(比如write() 方法)。这样,子类也就焕发出了新的生命力。 Java 只支持单一继承,这一点,我在上一篇接口的文章中已经提到过了。如果一个类在定义的时候没有使用 extends 关键字,那么它隐式地继承了 java.lang.Object 类——在我看来,这恐怕就是 Java 号称万物皆对象的真正原因了。 那究竟子类继承了父类的什么呢? 子类可以继承父类的非 private 成员变量,为了验证这一点,我们来看下面这个示例。 public class Wanger { String defaultName; private String privateName; public String publicName; protected String protectedName;}父类 Wanger 定义了四种类型的成员变量,缺省的 defaultName、私有的 privateName、共有的 publicName、受保护的 protectedName。 在子类 Wangxiaoer 中定义一个测试方法 testVariable(): 可以确认,除了私有的 privateName,其他三种类型的成员变量都可以继承到。 同理,子类可以继承父类的非 private 方法,为了验证这一点,我们来看下面这个示例。 public class Wanger { void write() { } private void privateWrite() { } public void publicWrite() { } protected void protectedWrite() { }}父类 Wanger 定义了四种类型的方法,缺省的 write、私有的 privateWrite()、共有的 publicWrite()、受保护的 protectedWrite()。 在子类 Wangxiaoer 中定义一个 main 方法,并使用 new 关键字新建一个子类对象: 可以确认,除了私有的 privateWrite(),其他三种类型的方法都可以继承到。 不过,子类无法继承父类的构造方法。如果父类的构造方法是带有参数的,代码如下所示: public class Wanger { int age; String name; public Wanger(int age, String name) { this.age = age; this.name = name; }}则必须在子类的构造器中显式地通过 super 关键字进行调用,否则编译器将提示以下错误: 修复后的代码如下所示: public class Wangxiaoer extends Wanger{ public Wangxiaoer(int age, String name) { super(age, name); }}is-a 是继承的一个明显特征,就是说子类的对象引用类型可以是一个父类类型。 public class Wangxiaoer extends Wanger{ public static void main(String[] args) { Wanger wangxiaoer = new Wangxiaoer(); }}同理,子接口的实现类的对象引用类型也可以是一个父接口类型。 public interface OneInterface extends Cloneable {}public class TestInterface implements OneInterface { public static void main(String[] args) { Cloneable c1 = new TestInterface(); }}尽管一个类只能继承一个类,但一个类却可以实现多个接口,这一点,我在上一篇文章也提到过了。另外,还有一点我也提到了,就是 Java 8 之后,接口中可以定义 default 方法,这很方便,但也带来了新的问题: 如果一个类实现了多个接口,而这些接口中定义了相同签名的 default 方法,那么这个类就要重写该方法,否则编译无法通过。 FlyInterface 是一个会飞的接口,里面有一个签名为 sleep() 的默认方法: public interface FlyInterface { void fly(); default void sleep() { System.out.println("睡着飞"); }}RunInterface 是一个会跑的接口,里面也有一个签名为 sleep() 的默认方法: public interface RunInterface { void run(); default void sleep() { System.out.println("睡着跑"); }}Pig 类实现了 FlyInterface 和 RunInterface 两个接口,但这时候编译出错了。 原本,default 方法就是为实现该接口而不覆盖该方法的类提供默认实现的,现在,相同方法签名的 sleep() 方法把编译器搞懵逼了,只能重写了。 public class Pig implements FlyInterface, RunInterface { @Override public void fly() { System.out.println("会飞的猪"); } @Override public void sleep() { System.out.println("只能重写了"); } @Override public void run() { System.out.println("会跑的猪"); }}类虽然不能继承多个类,但接口却可以继承多个接口,这一点,我不知道有没有触及到一些读者的知识盲区。 public interface WalkInterface extends FlyInterface,RunInterface{ void walk();}学到了吧?学到就是赚到。 原文地址https://www.cnblogs.com/qing-gee/p/12990735.html
Unity 离线建造系统 很多游戏,特别是养成类手游,都会有自己独特的建造系统,一个建造装置的状态循环或者说生命周期一般是这样的: 1.准备建造,设置各项资源的投入等 2.等待一段倒计时,正在建造中 3.建造结束,选择是否收取资源 大体上,可以将建造盒子分为以下三种状态,每一个状态的逻辑和显示的页面不同: 1 public enum BuildBoxState2 {3 Start,4 Doing,5 Complete6 } 1 private void ShiftState(BuildBoxState state) 2 { 3 switch (state) 4 { 5 case BuildBoxState.Start: 6 Start.SetActive(true); 7 Doing.SetActive(false); 8 Complete.SetActive(false); 9 10 ResetResCount();11 break;12 case BuildBoxState.Doing:13 Start.SetActive(false);14 Doing.SetActive(true);15 Complete.SetActive(false);16 17 StartCoroutine(ShowBuildTime());18 break;19 case BuildBoxState.Complete:20 Start.SetActive(false);21 Doing.SetActive(false);22 Complete.SetActive(true);23 24 break;25 }26 CurState = state;27 } 这里值得思考的并非是状态的切换或者基础的按钮侦听,视图资源更新等。 如何在离线一段时间后重新获取目前对应建造盒子所处的状态才是重点;并且如果处于建造中状态的话,还应该能正确的显示剩余时间的倒计时。 一个非常常见的想法是,在建造开始时记录一份开始建造的时间数据给服务器或存在本地离线数据中,当下一次再登录时读取当前系统的时间,并通过总共需要的建造时长来计算剩余时间。 但假如总共需要的建造时长与当时投入的资源类型和量都有关系,这时就需要至少额外记载一类数据来进行计算。那么,有没有方法仅通过一个数据得到剩余时长呢? 答案是,不记录开始建造的时刻,改为记录拟定建造完成的时刻。 如此一来,每次离线登录后,只需要干两件事既可以判断出所有状态视图: 1.是否存在该建造盒子ID对应的拟定建造完成时刻的数据,如果不存在,一定是处于准备状态,即Start状态。 2.如果存在,对比当前系统时刻与拟定建造完成时刻的数据大小,大于等于则处于完成状态,小于则依然在建造中,并按秒显示差值更新。 记录的时刻如下: 1 public string BuildCompleteTime 2 { 3 get 4 { 5 if (PlayerPrefs.HasKey(ID.ToString())) 6 return PlayerPrefs.GetString(ID.ToString()); 7 return S_Null; 8 } 9 set10 {11 PlayerPrefs.SetString(ID.ToString(), value);12 PlayerPrefs.Save();13 }14 } 每次开始时,只需要判断这个数据是否存在: 1 protected override void InitState() 2 { 3 View = HudView as BuildBoxView; 4 if (BuildCompleteTime == S_Null) 5 { 6 ShiftState(BuildBoxState.Start); 7 } 8 else 9 {10 ShiftState(BuildBoxState.Doing);11 }12 } 通过建造中的时刻关系自动判断是否完成: 1 IEnumerator ShowBuildTime() 2 { 3 var ct = GetCompleteTime(); 4 if (CheckBuildCompleted(ct)) 5 { 6 ShiftState(BuildBoxState.Complete); 7 yield break; 8 } 9 else10 {11 for (; ; )12 {13 View.SetTime(CalNeedTime(ct));14 yield return new WaitForSeconds(1);15 }16 }17 } 当建造完成点击收取资源时,切换为准备状态的同时,自动清空拟定建造完成时刻的数据记录: 1 private void OnClickGet()2 {3 Canvas.SendEvent(new GetItemEvent());4 ClearCompleteTime();5 ShiftState(BuildBoxState.Start);6 }这里有一个问题是,为什么不在建造完成时就清理数据呢,因为有一种情况是,建造完成后,玩家还没来得及点击收取,就直接进入了离线状态,如果此时再次登录时数据已经清空,那他将做了一场无用功。 说不定直接垃圾游戏毁我青春败我前程了,为了避免这种情况发生,我们只有确保玩家真正收取到资源的那一刻才能清理数据。 到此,整个建造的基础逻辑已经梳理完毕。如果要实现快速建造的话,也只不过是将拟定的完成时间直接设置为此刻即可。如果之前记录的是开始建造的时刻,此时又会进行更多额外计算。 接下来,关于时间的坑这里也略提一二吧,一开始我以为记录时刻只需要记录时分秒即可,因为最多的建造时长也不超过10小时一般,游戏要保证玩家每天登陆,不可能动用海量的时间去建造一个资源。 如若如此,策划很可能会马上被抓出来祭天,并被玩家评论区冰冷的口水淹没。 但后来写着写着就发现了一个问题,那就是好多天没登录的玩家怎么办,只记录时分秒根本没办法判断时间的早晚,后来想一会还是把日期也记录下来吧。 1 public struct TimeData2 {3 public int DayOfYear;4 public int Hour;5 public int Minute;6 public int Second;7 } 要是你问,那一年以上没登录怎么办,那只能说,你建造的资源已经被时光的齿轮碾碎了(允悲...)。后来突然想起来如果是某一年的最后一天呢...emm,还是老实写全吧: 1 public struct TimeData 2 { 3 public int Year; 4 public int DayOfYear; 5 public int Hour; 6 public int Minute; 7 public int Second; 8 9 public TimeData(int year,int dayOfYear,int hour,int minute,int second)10 {11 Year = year;12 DayOfYear = dayOfYear;13 Hour = hour;14 Minute = minute;15 Second = second;16 }17 } 完整时间数据管理脚本: View Code完整建造脚本: View Code View Code这里用到的UI基础类可详见之前写过的随笔: https://www.cnblogs.com/koshio0219/p/12808063.html 单例模式: https://www.cnblogs.com/koshio0219/p/11203631.html 补充: 通用确认弹窗: View Code 效果: 原文地址https://www.cnblogs.com/koshio0219/p/12988884.html
使用Docker发布blazor wasm Blazor编译后的文件是静态文件,所以我们只需要一个支持静态页面的web server即可。根据不同项目,会用不同的容器编排,本文已无网关的情况下为例,一步一步展示如何打包进docker 需求HTTPS既然无网关,直接面向互联网,所以HTTPS显得尤为重要 HTTP/2 TLS3.0既然都是静态资源,使用H2和TLS3.0的目的是进一步加快加载速度 Compress对静态资源的压缩的目的依然是进一步加快加载速度。压缩选型为Brotli和 Gzip 压缩 Dockerfile官方的Nginx镜像,默认不支持Brotli所以需要自己准备一个具有Brotli支持的镜像,这里推荐使用自卖自夸的Nginx镜像,不仅使用最新的openssl编译(避免漏洞),还支持TLS1.3 http2 brotli和默认东八时区,且配置文件里还有配置示例。欢迎访问Docker Hub rsnow/nginx,了解更多。 发布blazor wasm截至2020.05.26,VS还不能把blazor wasm直接发布到Docker镜像仓库,所以只能自己打包 首先发布Release,不再赘述在项目根目录创建DockerfileFROM rsnow/nginx:amd64-1.18.0RUN rm /usr/share/nginx/html/index.html && \ echo -e 'server { \n\ listen 443 ssl http2; \n\ server_name localhost; \n\ brotli on; \n\ brotli_comp_level 6; \n\ brotli_types application/wasm application/octet-stream text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/svg+xml; \n\ gzip on; \n\ gzip_vary on; \n\ gzip_proxied any; \n\ gzip_comp_level 6; \n\ gzip_types application/wasm application/octet-stream text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; \n\ ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; \n\ ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; \n\ ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem; \n\ ssl_dhparam /etc/certs/dhparam.pem; \n\ ssl_session_timeout 1d; \n\ ssl_session_cache shared:SSL:10m; about 40000 sessions \n\ ssl_session_tickets off; \n\ ssl_protocols TLSv1.3; \n\ ssl_prefer_server_ciphers off; \n\ ssl_stapling on; \n\ ssl_stapling_verify on; \n\ resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=60s; \n\ resolver_timeout 2s; \n\ add_header X-Frame-Options "SAMEORIGIN" always; \n\ add_header X-XSS-Protection "1; mode=block" always; \n\ add_header X-Content-Type-Options "nosniff" always; \n\ add_header Referrer-Policy "no-referrer-when-downgrade" always; \n\ add_header Content-Security-Policy "default-src \'self\' http: https: data: blob: \'unsafe-inline\'" always; \n\ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; \n\ location / { \n\ root /usr/share/nginx/html; \n\ try_files $uri $uri/ /index.html =404; \n\ } \n\ } n\' > /etc/nginx/conf.d/default.confCOPY ./bin/Release/netstandard2.1/publish/wwwroot /usr/share/nginx/html/我们使用自定义的conf覆盖掉默认的conf(最简单的情况) 整个Nginx Container只有一个虚拟主机,所以server_name无论为何值,请求都会指向这个唯一的虚拟主机如果有网关,可以关闭跨域,HTTPS之类的配置,因为这些内容网关会涵盖因本示例展示的是最简单的情况,所以并没有使用 volume ,如果有容器互联,加载外部证书,配置文件的情况可根据实际情况创建挂载点。无论是gz或br都增加了对application/wasm application/octet-stream的支持Images有个Dockerfile,就开始生成镜像吧 docker build -t example.com/testnginxblazor:latest .如果需要上传到仓库 docker push example.com/testnginxblazor:latestRun根据创建的镜像,创建并运行容器 docker run \ -d \ --name nginxblazor \ -p 443:443 \ -v /test/fullchain.pem : /etc/letsencrypt/live/example.com/fullchain.pem \ -v /test/privkey.pem : /etc/letsencrypt/live/example.com/privkey.pem \ -v /test/chain.pem : /etc/letsencrypt/live/example.com/chain.pem \ -v /test/dhparam.pem : /etc/certs/dhparam.pem \ example.com/testnginxblazor 声明本文采用知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议进行许可,发表在CSDN和博客园,欢迎读者转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接!请读者/爬虫们尊重版权 原文地址https://www.cnblogs.com/chasingdreams2017/p/12984025.html
Java高级特性之集合 Java集合框架 一、Java集合框架概述 1、数组与集合的区别: 1)数组长度不可变化而且无法保存具有映射关系的数据;集合类用于保存数量不确定的数据,以及保存具有映射关系的数据。 2)数组元素既可以是基本类型的值,也可以是对象;集合只能保存对象。 2、Java集合类主要由两个根接口Collection和Map派生出来的,Collection派生出了三个子接口:List、Set、Queue(Java5新增的队列),因此Java集合大致也可分成List(有序、可重复集合、可直接根据元素的索引来访问)、Set(无序、不可重复集合、只能根据元素本身来访问)、Queue(队列集合)、Map(存储key-value对的集合,可根据元素的key来访问value)这四种。 二、Java集合常见接口及实现类 1、Collection接口常见方法 2、List集合 实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack。 它们都可以容纳所有类型的对象,包括 null,允许重复,并且都保证元素的存储顺序。(可重复,有顺序) 1)ArrayList ArrayList 对数组进行了封装,实现了长度可变的数组(动态数组),和数组采用相同存储方式,在内存中分配连续的空间,它的优点在于遍历元素和随即访问元素的效率比较高。 ArrayList特点:线程不安全,查询速度快;底层数据结构是数组结构;扩容增量:原容量的 1.5倍, 如 ArrayList的容量为10,一次扩容后是容量为15。 2)LinkedList LinkedList是List接口的另一个实现,除了可以根据索引访问集合元素外,LinkedList还实现了Deque接口,可以当作双端队列来使用,也就是说,既可以当作“栈”使用,又可以当作队列使用。 LinkedList 采用链表存储方式,优点在于插入、删除元素时效率比较高,它提供了额外的 addFirst()、addLast()、removeFirst()和 removeLast()等方法,可以在LinkedList 的首部或尾部进行插入或者删除操作。 3)Vector 与ArrayList相似,但是Vector是同步的。所以说Vector是线程安全的动态数组。它的操作与ArrayList几乎一样。 3、Set集合 实现Set集合的接口主要有:HashSet、TreeSet、LinkedHashSet; Set集合与Collection的方法相同,由于Set集合不允许存储相同的元素,所以如果把两个相同元素添加到同一个Set集合,则添加操作失败,新元素不会被加入,add()方法返回false。(无序、不重复) 1)HashSet HashSet是按照hash算法来存储元素的,因此具有很好的存取和查找性能。 HashSet存储原理如下: 当向HashSet集合存储一个元素时,HashSet会调用该对象的hashCode()方法得到其hashCode值,然后根据hashCode值决定该对象的存储位置。HashSet集合判断两个元素相等的标准是(1)两个对象通过equals()方法比较返回true;(2)两个对象的hashCode()方法返回值相等。因此,如果(1)和(2)有一个不满足条件,则认为这两个对象不相等,可以添加成功。如果两个对象的hashCode()方法返回值相等,但是两个对象通过equals()方法比较返回false,HashSet会以链式结构将两个对象保存在同一位置,这将导致性能下降,因此在编码时应避免出现这种情况。 HashSet查找原理如下: 基于HashSet以上的存储原理,在查找元素时,HashSet先计算元素的HashCode值(也就是调用对象的hashCode方法的返回值),然后直接到hashCode值对应的位置去取出元素即可,这就是HashSet速度很快的原因。 HashSet特点: 不能保证元素的顺序;集合元素值可以是null;线程不安全,存取速度快;底层实现是一个HashMap(保存数据),实现Set接口;默认初始容量为16;加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容;扩容增量:原容量的 1 倍;如 HashSet的容量为16,一次扩容后是容量为32。 2)LinkedHashSet LinkedHashSet是HashSet的一个子类,具有HashSet的特性,也是根据元素的hashCode值来决定元素的存储位置。但它使用链表维护元素的次序,元素的顺序与添加顺序一致。由于LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet,但在迭代访问Set里的全部元素时由很好的性能。 3)TreeSet TreeSet可以保证元素处于排序状态,它采用红黑树的数据结构来存储集合元素。TreeSet支持两种排序方法:自然排序和定制排序,默认采用自然排序。 ♦ 自然排序 TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素的大小关系,然后将元素按照升序排列,这就是自然排序。如果试图将一个对象添加到TreeSet集合中,则该对象必须实现Comparable接口,否则会抛出异常。当一个对象调用方法与另一个对象比较时,例如obj1.compareTo(obj2),如果该方法返回0,则两个对象相等;如果返回一个正数,则obj1大于obj2;如果返回一个负数,则obj1小于obj2。 ♦ 定制排序 想要实现定制排序,需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由Comparator对象负责集合元素的排序逻辑。 综上:自然排序实现的是Comparable接口,定制排序实现的是Comparator接口。 4、Map集合 Map接口采用键值对Map的存储方式,保存具有映射关系的数据,因此,Map集合里保存两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value,key和value可以是任意引用类型的数据。key值不允许重复,可以为null。如果添加key-value对时Map中已经有重复的key,则新添加的value会覆盖该key原来对应的value。 常用实现类主要有HashMap、LinkedHashMap、TreeMap。 1)HashMap 对于HashMap而言,key是唯一的,不可以重复的。所以,以相同的key 把不同的value插入到 Map中会导致旧元素被覆盖,只留下最后插入的元素。不过,同一个对象可以作为值插入到map中,只要对应的key不一样 。 HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。 HashMap工作原理如下: HashMap基于hashing原理,通过put()和get()方法存储和获取对象。当我们将键值对传递给put()方法时,它调用建对象的hashCode()方法来计算hashCode值,然后找到bucket位置来储存值对象。当获取对象时,通过建对象的equals()方法找到正确的键值对,然后返回对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会存储在链表的下一个节点中。 HashMap特点: Map提供了一种映射关系,元素是以键值对(key-value)的形式存储的,能根据key快速查找value;Map中的键值对以Entry类型的对象实例形式存在;key值不能重复,value值可以重复;key对value是多(一)对一的关系;Map接口提供了返回key值集合、value值集合、Entry值集合,的方法;Map支持泛型,形式如:Map;默认初始容量为16;加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容;扩容增量:原容量的 1 倍。2)LinkedHashMap LinkedHashMap使用双向链表来维护key-value对的次序(其实只需要考虑key的次序即可),该链表负责维护Map的迭代顺序,与插入顺序一致,因此性能比HashMap低,但在迭代访问Map里的全部元素时有较好的性能。 大多数情况下,只要不涉及线程安全问题,Map基本都可以使用HashMap,不过HashMap有一个问题,是迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序。HashMap的这一缺点往往会带来困扰,因为有些场景,我们期待一个有序的Map。这个时候,LinkedHashMap就闪亮登场了,它虽然增加了时间和空间上的开销,但是通过维护一个运行于所有条目的双向链表,LinkedHashMap保证了元素迭代的顺序。该迭代顺序可以是插入顺序或者是访问顺序。 LinkedHashMap可以认为是HashMap+LinkedList,即它既使用HashMap操作数据结构,又使用LinkedList维护插入元素的先后顺序。 LinkedHashMap的基本实现思想就是----多态。 3)TreeMap TreeMap是SortedMap的实现类,是一个红黑树的数据结构,每个key-value对作为红黑树的一个节点。TreeMap存储key-value对时,需要根据key对节点进行排序。TreeMap也有两种排序方式: ♦ 自然排序:TreeMap的所有key必须实现Comparable接口,而且所有的key应该是同一个类的对象,否则会抛出ClassCastException。 ♦ 定制排序:创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中的所有key进行排序。 TreeMap特点: ·TreeMap是非线程安全的; ·TreeMap是用键来进行升序顺序来排序的。通过Comparable 或 Comparator来排序; ·和HashMap一样,如果插入重复的元素,后面的元素会覆盖前面的;·键不可以为null(如果比较器对null做了处理,就可以为null),但是值可以为null。 5、遍历 第一种:for循环遍历 1 for (int i = 0; i < heros.size(); i++) {2 Hero h = heros.get(i);3 System.out.println(h);4 }第二种:迭代器遍历 1 System.out.println("--------使用while的iterator-------"); 2 Iterator it= heros.iterator(); 3 //从最开始的位置判断"下一个"位置是否有数据 4 //如果有就通过next取出来,并且把指针向下移动 5 //直到"下一个"位置没有数据 6 while(it.hasNext()){ 7 Hero h = it.next(); 8 System.out.println(h); 9 }10 //迭代器的for写法11 System.out.println("--------使用for的iterator-------");12 for (Iterator iterator = heros.iterator(); iterator.hasNext();) {13 Hero hero = (Hero) iterator.next();14 System.out.println(hero);15 } 第三种:增强for循环 1 System.out.println("--------增强型for循环-------");2 for (Hero h : heros) {3 System.out.println(h);4 }原文地址https://www.cnblogs.com/jacob-wuhan/p/12974025.html
关于Java两点需要更新的知识 HashMap的初始容量 背景 很多人可以把HashMap的原理描述的很溜。比如JDK1.7之前,底层数据结构是数组+链表。JDK1.8之后,出于效率上的考虑,在数组长度大于64,链表长度大于8的时候,会转换为红黑树。 甚至知道对于赋值了容量的都会做一个变成2的n次方的操作。它的hash方法为了防止高位变化大或者低位变化大将它本身hash值右移16位和自身原hash值做一个按位异或操作再与容量-1做按位与。还知道默认的负载因子是0.75,这个值是经过概率论统计出来的,最好不要改。 了解的这么清楚,我就想问一下为什么从数据库中取出来一个list,之后转换成hashmap。直接用的是Map map = new HashMap()或者是Map map = Maps.newHashMap(),为什么不赋初始容量呢? 分析 容量的大小会在put过程中发生resize操作。如果初始不赋值。默认容量是16。那比如从数据库中取出来1000个元素。put过程中会从16->32->64->128……,运行多次resize操作。resize操作数组,需要将所有元素进行复制和rehash,效率是很低的。 所以也有一些同学考虑到这个问题,代码是这么写的: Map map = new HashMap(list.size()); 这个写法也有问题,因为resize并不是到达容量上限才resize。为了尽量避免hash冲突,是超过阈值threshold就扩容。而这个threshold=容量*负载因子。 所以我更建议的写法是Map map = new HashMap(list.size()/负载因子)。 这样理论上可以比Map map = new HashMap(list.size())减少一次resize。 总结 在可以确定HashMap容量时,最好Map map = new HashMap(list.size()/负载因子)来初始化,避免自动扩容带来的性能损耗。 思考 ConcurrentHashMap怎么来更合理的初始化? JVM内存结构和Java内存模型 背景 前段时间偶然看到有篇文章批判很多人对「JVM内存模型」这个概念不清楚,说这个经典的图并不是内存模型而是内存结构。 而内存模型应该是JSR133规范里介绍的volatile、final和synchronized等关键字的内存语义。 分析 这个非常富有淘金式思维的作者却搞混了一个概念,看看下面JSR-133规范里是怎么说的:JSR133规范里讲的Java内存模型,并没有说是JVM的内存模型啊。 Java内存模型讲的是Java语言本身的规范,这个规范包含了各个Java标准关键字在JVM里是怎样运作的。而JVM内存模型描述的是Java虚拟机怎样运行字节码的。所以上面经典的图说是JVM内存模型也不为过。不过根据官网,叫JVM内存结构更为标准。证据如下: https://docs.oracle.com/javase/specs/jvms/se14/html/jvms-2.html#jvms-2.5在oracle官网里,介绍了这个概念 总结 Java内存模型和JVM内存模型是两个概念。 原文地址https://www.cnblogs.com/xiexj/p/12967273.html
Docker虚拟机配置手札(centos) 一、Docker只支持CentOS7及以上系统,不支持6.x系统二、yum安装Docker1、安装相关环境和设置仓库 yum install -y yum-utils device-mapper-persistent-data lvm2yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo2、安装Docker社区版 yum install docker-ce docker-ce-cli containerd.ioDocker 安装完默认未启动。并且已经创建好 docker 用户组,但该用户组下没有用户。 3、启动Docker systemctl start docker 4、运行hello-world docker run hello-world 三、修改Docker镜像存放目录docker 默认的存储路径在 /var/lib/docker ,但机子的数据盘挂载在/home目录下, 所以修改docker存储路径到 /home/docker 中。 1、查找 docker.service 配置文件,不知道配置文件在哪里可以用以下命令显示 systemctl disable dockersystemctl enable docker 显示结果 Created symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.2、修改 docker.service 配置文件 vi /usr/lib/systemd/system/docker.service3、在里面的EXECStart找到这样一行 ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock ,将其修改为 ExecStart=/usr/bin/dockerd --graph /home/docker -H fd:// --containerd=/run/containerd/containerd.sock 保存并退出 4、重载并重启docker服务 systemctl daemon-reload #重载配置文件systemctl restart docker #重启dockersystemctl enable docker #设为自启动5、查看docker运行信息 docker info 其中 Docker Root Dir: /home/docker 即表示已经成功修改运行目录了 6、重新下载和运行hello-world镜像 docker run hello-world 四、容器和镜像的导入导出镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统。 镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。 docker images 查看镜像列表; docker ps 查看容器列表; 1、保存容器为镜像 docker commit mycentos myos202005 mycentos是容器名,即现正运行的这个虚拟机的名;myos202005是导出的虚拟机镜像名,可用于发布、备份等操作; 2、导出镜像(转格式)为文件 docker save myos202005:latest > /home/myos202005.img 3、导出容器(正在运行的虚拟机)为文件 docker export mycentos > /home/myos202005.img 4、导入镜像, docker load --input /home/myos202005.img 5、导入容器, docker import /home/myos202005.img myos202005 区别:docker save保存的是镜像(image),docker export保存的是容器(container);docker load用来载入镜像包,docker import用来载入容器包,但两者都会恢复为镜像;docker load不能对载入的镜像重命名,而docker import可以为镜像指定新名称。 (如果VMware中的虚拟机centos空间不足,需要扩容,参考这篇文章) 五、运行虚拟机和进入虚拟机系统1、后台运行镜像(会新建一个容器) docker run -itd --privileged=true --name myos202005 myos202005:latest(注:以特权模式运行,容器中才能使用启动服务systemctl等系统命令) 2、如果有运行过,则应该启动容器 docker ps -a docker start 容器ID 2、进入容器(虚拟机),使用 docker ps 查看容器信息,然后 docker exec -it 容器ID /bin/bash 3、退出容器(虚拟机),不关闭容器 Ctrl + P + Q 4、关闭容器, docker stop 容器ID ;重启容器 docker restart 容器ID ;启动容器 docker start 容器ID 5、查看包括已退出的容器 docker ps -a ;删除容器 docker rm 容器ID ;删除镜像 docker rmi 镜像ID PS:容器ID 和 容器名是等效的,以上命令均可以用容器名替代容器ID。 六、虚拟机端口映射1、先分别查看本机和docker机的进程端口使用情况 netstat -nlpt 2、查看容器列表信息 docker ps 3、停止容器、停止docker服务 docker stop 容器IDsystemctl stop docker4、修改容器的配置文件,两个配置文件都要改 vi /var/lib/docker/containers/容器ID/hostconfig.jsonvi /var/lib/docker/containers/容器ID/config.v2.jsonPS:如果第三步修改过docker存放目录,如 /home/docker,则配置文件的位置也在相应新的位置 例如把docker中mysql 3306端口,映射到外面的13306端口,以便远程访问docker中的数据库 5、hostconfig.json修改 找到 PortBindings ,把那一段JSON修改为 "PortBindings":{"3306/tcp":[{"HostIp":"","HostPort":"13306"}]} 6、config.v2.json修改 修改值 config > ExposedPorts 和 NetworkSettings > Ports "Config": { .... "ExposedPorts": { "22/tcp": {}, "3306/tcp": {} }, .... },"NetworkSettings": { .... "Ports": { "22/tcp":null, "3306/tcp": [{ "HostIp": "", "HostPort": "13306" }], }, ... } 7、修改完成后先检查两个文件是否JSON格式正确 8、启动docker,启动容器, 查看容器列表信息,查看端口映射是否生效 systemctl start dockerdocker start 容器IDdocker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES容器ID myos202005:latest "/usr/sbin/init" 3 hours ago Up 5 seconds 22/tcp, 1300/tcp, 0.0.0.0:13306->3306/tcp cranky_franklin如上标红的 13306->3306,则表示端口映射成功 9、查看端口使用情况 netstat -nlpt ,可以看到 docker-proxy 进程使用13306端口 10、服务器防火墙开放相应的端口 查看开放的端口 firewall-cmd --list-port 添加端口(永久) firewall-cmd --zone=public --add-port=13306/tcp --permanent 重载防火墙 firewall-cmd --reload 如果是用的阿里云,还有“安全组”策略限制了端口访问,需要在阿里云后台操作添加端口 七、Nginx / Apache 反向代理部分域名到docker中实现效果:blog.batsing.com 正常定向在服务器中,blog.demo.batsing.com 定向到docker中。都是使用80端口。 实现过程: 1、docker机里有内网IP(默认172.17.0.2) 2、服务器可以通过此IP访问到docker中的nginx curl 172.17.0.2:80 3、服务器配置系统hosts,把demo域名指向docker内网IP cat /etc/hosts172.17.0.2 blog.demo.batsing.com4、Nginx中配置vhost,将demo域名转发到域名自身 server { listen 80; server_name blog.demo.batsing.com; location / { proxy_pass http://blog.demo.batsing.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-NginX-Proxy true; } } 转发到域名自身,因为服务器的hosts配置,所以会转发到docker机中,从而实现部分域名转入到docker中的功能。 服务器中其他站点域名,按nginx正常配置即可。 docker中的nginx配置,也是按正常配置即可,无需另行特殊配置。 原文地址https://www.cnblogs.com/batsing/p/docker.html
C# 数据操作系列 - 11 NHibernate 配置和结构介绍 前言今天是NHibernate的第二篇内容,通过上一篇的内容,我们初步了解了NHibernate的创建和使用。这一篇,我继续探索NHibernate背后的秘密。嗯,就是这样。 NHibernate结构先给小伙伴们放个图: 这是NHibernate的整体结构图。NHibernate通过ADO.NET 建立访问数据库的连接,然后封装了一个Transaction(事务)工厂和一个Session工厂。每次操作的时候,通过两个工厂获取对应的Session/Transaction示例操作数据对象。 ISessionFactory - NHibernate.ISessionFactory: 一个基于单数据库的已编译的映射缓存,它是持久不变的且线程安全(额,这句话是从它的文档翻译过来的)。是一个提供ISession的工厂类,同时也是一个 IConnectionProvider的客户端。可以设置一个在事务之间的进程级或集群级的二级缓存。 ISession - NHibernate.ISession: 一个单线程、短生命周期的对象,表示从应用程序和数据持久化之间一个连接。一个ADO.NET连接的封装,用来提供ITransaction的工厂。提供了一个通过主键检索对象和导航链接查询对象时的一级缓存。也就是EF Core中的导航属性。 Persistent Objects and Collections(持久化对象和集合): 一些单线程、短生命周期对象其中包含持久化状态和业务方法。它们可能只是一些普通的POCO,仅仅是与ISession中关联起来了。只要ISession关闭了,这些对象就可以被分离出来然后可以在应用层的任意地方使用。 Transient Objects and Collections(临时对象和集合): 表示临时的未被ISession托管的持久化对象,它们被应用层临时创建但直到ISession关闭都不会被持久化。 ITransaction - NHibernate.ITransaction: 这个是可选的。表示一个单线程、短生命周期的对象,被应用程序用来限制一个原子的工作单元,基于ADO.NET 的Transaction的抽象。一个ISession可能会开启多个事务,Transaction scopes may be used instead(原话是这个,大意是可以改用事务作用域)。 IConnectionProvider - NHibernate.Connection.IConnectionProvider: 也是可选的,是一个用来创建ADO.NET Connection和Command的工厂。基于DbConnection和DbCommand实现,并非直接暴露给应用程序,但是可以由开发者对其进行扩展或实现。 IDriver -NHibernate.Driver.IDriver: 可选的,驱动接口,用来封装隐藏不同ADO.NET 数据提供程序之间的不同。例如:参数化等。 ITransactionFactory - NHibernate.Transaction.ITransactionFactory: 可选的,事务实现工厂,不对应用程序公开,但开发者可以对其进行扩展或实现。 实例状态在NHibernate中,一个可持久化的对象有三种不同的状态,依据与持久化上下文之间的关系不同,其中ISession就是一个持久化上下文。状态分为以下三种: transient 暂存的、临时的 该状态的对象并没有被持久化上下文捕获到,简单来讲就是刚被创建,还没有从数据库/持久化上下文中获取到主键信息。persistent 持久化的 该状态的对象表示已经被上下文正确获取到了,持久化上下文能够监控到对象的变化。持久化上下文中持有一个指向该对象的引用。这种状态通常是从数据库中获取到数据或者新建的数据附加到了上下文中。detached 游离态 该状态的对象是从上下文中分离出来的,有了数据库主键,曾经或现在仍然有一条数据库记录与之对应。造成的原因可能有,上下文关闭了;该对象是在另一个上下文中持久化的,它对于当前上下文是游离态的。 配置项介绍在上一篇文章中,我们介绍了一下如何设置NHibernate的基本配置项,但是并未对配置项进行深入。这一节,将带领大家看一下NHibernate中我们常用的配置,因为配置项有很多,但一大部分通常情况都遇不到使用它的时候。 dialect数据库方言,表示NHibernate连接的数据库是什么,该用哪种格式解析关系映射到数据库SQL语句 default_schema默认的schema,用来设置连接字符串连接的数据库默认的schema。 connection.provider数据库连接的提供程序,默认是NHibernate.Connection.DriverConnectionProvider. 填继承自 IConnectionProvider 的实现类 connection.connection_string数据库连接字符串 connection.connection_string_name数据库连接字符串的名称,指的是配置在程序的配置文件中 connectionStrings节点的数据连接字符串。 max_fetch_depth最大递归深度,表示一次查询中直接加载的导航属性深度。默认是不直接加载导航属性,基于延迟加载的逻辑,由实际使用时才从数据库中加载数据。 show_sql是否在控制台中打印转换的SQL语句,一般在调试的过程中会设置为true,用来确认生成的SQL是否正确等。 hbm2ddl.auto该值表示每次ISessionFactory创建的时候,是否自动生成DDL语句并提交数据库执行。默认是空,表示不会强制更新数据库。有几个候选值:create或create-drop、update等。其中create表示每次只创建新增的;create-drop表示每次ISessionFactory创建时创建表 ,ISessionFactory关闭时,删除表;其中update表示每次都会将DDL SQL更新到数据库中。(我记得有update,但文档中没有这个选项) 以上是我们常用的一些配置内容,当然还有更多的配置,我并没有在这里一一讲明,留待以后吧。 总结这是一篇枯燥乏味的说明文,主要介绍了Nhibernate的基本内容。下一章我们来试试,如何创建Nhibernate的映射配置。 原文地址https://www.cnblogs.com/c7jie/p/12924503.html
换一种方式编写 Spring MVC 接口 前言通常我们编写 Spring MVC 接口的范式是这样的: @RestController@RequestMapping("/v1/userinfo")public class UserInfoController { @GetMapping("/foo") public String foo() { return "felord.cn"; } }这种我都写吐了,今天换个口味,使用 Spring 5 新引入的函数式端点(Functional Endpoints)来耍耍。 这种方式同样支持 Spring Webflux。 请注意可使用该特性的 Spring 版本不低于 Spring 5.2 依赖为了演示,这里极简化只引入 Spring MVC 的 starter : <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> RouterFunction在函数式端点的写法中,传统的请求映射(@RequestMapping)被路由函数(RouterFunction)所代替。上面的写法等同于: @Bean public RouterFunction<ServerResponse> fooFunction() { return RouterFunctions.route() .GET("/v1/userinfo/foo", request -> ServerResponse.ok() .body("felord.cn")) .build(); } 在该示例中,我使用了 RouterFunctions.route() 创建了一个RouterFunction,然后RouterFunction 提供了从请求到响应的细节操作。 ServerRequest/ServerResponseServerRequest 是对服务器端的HTTP请求的抽象,你可以通过该抽象获取请求的细节。对应的,ServerResponse 是对服务器端响应的抽象,你也可以通过该抽象构建响应的细节。这两个概念由下面的 HandlerFunction 接口进行 请求→ 响应 处理。 HandlerFunctionHandlerFunction 是一个函数式接口,它提供了从请求( ServerRequest)到响应(ServerResponse)的函数映射抽象。通常你的业务逻辑由该接口进行实现。从 ServerRequest 中获取请求的细节,然后根据业务构建一个 ServerResponse 响应。 HandlerFunction handlerFunction = request -> ServerResponse.ok().body("felord.cn"); RequestPredicateRequestPredicate 可以让你根据请求的一些细节,比如 请求方法、请求头、请求参数等等进行断言以决定是否路由。 这里举一个例子,假如我们希望请求接口/v1/userinfo/predicate时根据不同的参数处理不同的业务,当携带参数 plan时才能进行处理。我们可以这么写: @Bean public RouterFunction<ServerResponse> predicateFunction() { return RouterFunctions.route() .GET("/v1/userinfo/predicate", request -> request.param("plan").isPresent(), request -> ServerResponse.ok().body("felord.cn")) .build(); } 然后我们测试一下: 当携带参数 plan时: GET http://localhost:8080/v1/userinfo/predicate?plan= HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 9Date: Thu, 14 May 2020 07:57:35 GMTKeep-Alive: timeout=60Connection: keep-alive felord.cn不携带参数plan时: GET http://localhost:8080/v1/userinfo/predicate HTTP/1.1 404 Vary: OriginVary: Access-Control-Request-MethodVary: Access-Control-Request-HeadersContent-Type: application/jsonTransfer-Encoding: chunkedDate: Thu, 14 May 2020 08:00:15 GMTKeep-Alive: timeout=60Connection: keep-alive { "timestamp": "2020-05-14T08:00:15.659+0000", "status": 404, "error": "Not Found", "message": "No message available", "path": "/v1/userinfo/predicate"} 小结函数式端点是 Spring 5 提供的一个新的接口范式风格,对于 Spring MVC 来说 Spring 5.2 才进行了支持。也是顺应函数式编程的一个未来趋势。由于篇幅原因这里仅仅对其中的关键概念进行了讲解。下一篇我们会对这种接口范式进行进一步的讲解和实际使用。敬请关注:码农小胖哥 。 原文地址https://www.cnblogs.com/felordcn/p/12894127.html
Docker基础修炼3--Docker容器及常用命令 前文讲解了Docker镜像的原理和常用命令,本文继续通过官方的Apache镜像演示Docker容器相关的常用操作及命令。 我之前的Linux入门系列文章中“linux入门系列18--web服务之apache服务1”,演示了在CentOS7下安装和使用Apache服务,本次我们仍然采用官方的Apache镜像来进行演示,通过这个案例,大家自行对比这两种方式的不同,从而更加深刻理解Docke能干什么,以及Docker带来的好处。 接下来我们就来演示下容器如何创建、如何停止、如何删除等操作。 一、Docker容器简介容器是Docker中的另外一个核心概念,容器是镜像的一个运行实例。 Docker镜像是静态的,只有从Docker镜像创建容器并运行起来,容器内的程序会运行,从而完成特定的功能。 我们要完成业务功能的程序就是在容器中运行。镜像本身是静态的只读文件,而容器带有运行时所需的可写文件层,同时容器内的应用进程处于运行状态。 通过下边的演示,将会理解的更加深刻。 二、Docker容器常用命令2.1 apache镜像准备我们后续的演示是基于Docker Hub上官方提供的apache镜像进行。 镜像名称为httpd,该镜像并不包含php的环境只能运行静态的HTML页面,因此如果你是想运行php的动态网站则需要选择PHP镜像,而本文只是为了演示容器的相关操作,因此采用静态页面即可。 [root@docker ~]# docker pull httpd:2.42.4: Pulling from library/httpd68ced04f60ab: Pull complete 35d35f1e0dc9: Pull complete 8a918bf0ae55: Pull complete d7b9f2dbc195: Pull complete d56c468bde81: Pull complete Digest: sha256:946c54069130dbf136903fe658fe7d113bd8db8004de31282e20b262a3e106fbStatus: Downloaded newer image for httpd:2.4docker.io/library/httpd:2.4[root@docker ~]# docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEhttpd 2.4 c5a012f9cf45 2 days ago 165MB下载完成后我们可以看到httpd镜像大小为165M,里边究竟包含了什么内容呢?文章最后我们再来仔细分析一下。 2.2 创建容器并后台运行镜像下载完成后,就需要通过docker run命令创建容器。 语法: docker run [选项] 镜像名称 [命令] [参数] 选项很多,可以执行通过帮助命令进行查看,以下列出最常用的几个选项 选项 作用--name 指定容器的名称-d 容器后台运行,不在控制台打印消息-i 即--interactive,交互式运行-t 分配伪终端-p 指定宿主机与容器端口的映射,宿主机端口:容器内端口-P 指定宿主机与容器端口映射,宿主机端口随机指定其中-it参数和-d参数一般不同时使用,并且代表了两种启动容器的不同方式:交互式启动容器和守护式启动容器。 所谓交互式启动就是在容器启动后直接进入容器,并会自动分配一个伪终端,可以在容器内执行各种命令;而守护式启动容器则是容器启动后无需进入容器,容器在后台运行,默默的提供服务。 至于如何选择使用哪一种方式就看是否需要进入容器,如果要进入就选择交互式,如果只是希望容器后台运行提供服务那就选择守护式。 接下来通过-d参数,以守护进程方式创建基于httpd镜像的容器并后台运行 [root@docker ~]# docker run -d --name mywebsit -p 8888:80 httpd:2.4783b46f5cddcc3ea919329a99f83a783da98bce4abce05ccc9b3f27fda859b09[root@docker ~]# docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES783b46f5cddc httpd:2.4 "httpd-foreground" 6 seconds ago Up 4 seconds 0.0.0.0:8888->80/tcp mywebsit[root@docker ~]只需要做这一步操作,基于Apache的web环境就搭建好了,此时在CentOS7上就可以通过浏览器访问刚才搭建的基于apahce的网站了。 如果配置了防火墙,也可以在宿主机进行访问。 接下来就可以修改容器内网站目录里的数据,替换为自己的web页面即可。轻轻松松就搭建了一个apache的web静态网站,这是不是很方便呢?甚至都无须配置任何环境。迁移也非常方便,比如想换到另外一台主机运行该网站,也只需要下载该镜像,然后run起来即可。 这就是docker的强大之处,还有很多更强大的功能,后续慢慢演示。 说明一下我本地的环境,大家一定要理清楚物理机、虚拟机、容器之间的关系。我本地物理机系统是win10,在其上装了VMware虚拟机,在虚拟机中创建了一个Centos7的实例,然后在其中安装Docker,并运行mywebsite容器。因为我本地没有装Centos7,所以在虚拟机中虚拟一个来进行演示,当然你也可以直接在windows上装docker或直接在本地的centos上安装docker。 外部访问是访问在run容器是指定映射到的Centos7的宿主机端口8888,而非mywebsite容器的80端口。 特别注意:此处之所以采用-d在后台以守护进程的方式运行容器,原因就是当一个容器没有前台进程执行的时候,创建容器后立马就会停止。所以你可以用交互式的方式创建一个容器试试,docker run -it --name mywebsit1 -p 8888:80 httpd:2.4 ,创建后你用docker ps命令查看根本看不到容器,用docker ps -a命令查看,你就会发现容器创建后,短暂启动后立马就停止了,就是这个原因。 2.3 查看容器命令查看本地有哪些正在运行的容器,或者是曾经创建过的容器可以通过docker ps命令 语法 docker ps [选项] 常用参数 参数 作用-a --all,查看所有容器,默认情况下只显示正在运行的容器-l --latest,查看最近创建的容器-n 显示最后创建的n个容器--no-trunc 不截断显示案例: (1)查看正在运行的容器 [root@docker ~]# docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES783b46f5cddc httpd:2.4 "httpd-foreground" 4 hours ago Up 4 hours 0.0.0.0:8888->80/tcp mywebsit[root@docker ~]# 可以看到只有刚才创建的mywebsit容器在运行。 2.4 在容器外部查看容器信息2.4.1 查看容器日志命令语法 docker logs [选项] 容器名或id 常用参数 参数 作用-f 动态跟踪打印日志信息,一旦有新日志会继续打印-t 在每行日志前面打印出时间--tail 指定显示的行数案例 (1)查看mywebsit的日志,只显示2行 [root@docker ~]# docker logs -t --tail 2 mywebsit2020-02-29T03:09:57.654372509Z 192.168.78.1 - - [29/Feb/2020:03:09:57 +0000] "-" 408 -2020-02-29T03:09:57.854284777Z 192.168.78.1 - - [29/Feb/2020:03:09:57 +0000] "-" 408 -[root@docker ~]# 2.4.2 查看容器内进程命令语法 docker top 案例 (1)查看mywebsit内部的进程 [root@docker ~]# docker top mywebsitUID PID PPID C STIME TTY TIME CMDroot 3673 3655 0 10:29 pts/0 00:00:01 httpd -DFOREGROUNDbin 3708 3673 0 10:29 pts/0 00:00:00 httpd -DFOREGROUND...省略部分输出此命令类似于Linux下的top命令。 2.4.3 查看容器内部细节语法 docker inspect 容器名称或id 案例 (1)查看mywebsit容器内部信息 [root@docker ~]# docker inspect mywebsit...省略输出由于输出内容太多就不粘贴了。 执行命令后将得到一个详细描述容器信息的JSON字符串对象,该对象中包含了容器的详细信息,包含容器端口映射、挂载信息、卷信息、网络ip等信息。 2.5 退出容器命令如果进入了容器,退出方式有如下两种: exit命令 或 按快捷键:ctrl+p+q 2.6 进入容器内部与之交互进入正在运行的容器并以命令行交互 2.6.1 docker exec命令采用docker exec命令可以进入容器或不进入容器直接执行命令 语法 docker exec [选项] 容器名或id 命令 [参数...] 选项与docker run命令类似,也有-itd等参数。 用此命令进入容器后,用exit命令或快捷键退出容器后,容器不会停止。 案例 (1)不进入容器直接执行命令 [root@docker ~]# docker exec -it mywebsit pwd/usr/local/apache2[root@docker ~]# 该命令的含义查看mywebsit容器内的当前目录,可以看到命令执行结束后我们没没有进入到容器内部。 pwd是linux的命令,但此处为何能执行呢?原因是httpd镜像是由debian基镜像继承而来,大家知道debian也是linux的一个发行版本,因此该容器就有debian的功能。简单说就是你可以认为mywebsit容器就是跑在docker上的一个小linux系统。 (2)进入容器执行命令 [root@docker ~]# docker exec -it mywebsit /bin/bashroot@783b46f5cddc:/usr/local/apache2# pwd/usr/local/apache2root@783b46f5cddc:/usr/local/apache2# exitexit[root@docker ~]# 这个过程的作用给前面的案例是一样的,只不过这里是进入到容器内部,然后打印当前目录,然后在退出容器,返回到centos7宿主机。 通过这两个案例的对比,应该很清楚他们的区别了吧。 2.6.2 docker attach命令语法 docker attach [选项] 容器名或id 用此命令重新进入容器,进入容器后如果用exit命令退出,则容器会停止。 2.7 容器与宿主机之间数据拷贝docker cp命令可以实现宿主机与主机之间的数据拷贝,即使是容器停止的情况下也可以执行拷贝操作。 语法 docker cp 容器id:容器内路径 宿主机路径 案例 (1)修改容器内的首页内容 首先:httpd镜像网站的目录为:/usr/local/apache2/htdocs/index.html 其次:由于httpd镜像基于debian制作,内部并没有包含vi/vim等工具。 再次:正好可以用docker cp演示宿主机与容器之间文件的相互拷贝。 因此我们将网页文件拷贝到宿主机然后修改后在拷贝回容器网站目录下,已达到修改首页的目的。(在后续的讲解中也可以通过数据卷的方式将网站目录映射到宿主机上实现共享和修改) [root@docker ~]# docker cp mywebsit:/usr/local/apache2/htdocs/index.html /[root@docker ~]# ll /index.html -rw-r--r-- 1 root root 45 Jun 12 2007 /index.html[root@docker ~]# echo "my websit is updating">/index.html[root@docker ~]# cat /index.html my websit is updating[root@docker ~]# docker cp /index.html mywebsit:/usr/local/apache2/htdocs/index.html[root@docker ~]# 再次在浏览器中查看网页,就会发现已经被修改过了。 2.8 停止容器命令停止容器可以通过stop和kill两个命令 kill是强制停止容器,stop会稍微过一小会停止容器,二者都可以指定容器停止前等待的时间。 docker stop|kill 容器id或容器名 案例 (1)停止容器 [root@docker ~]# docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES783b46f5cddc httpd:2.4 "httpd-foreground" 6 hours ago Up 21 minutes 0.0.0.0:8888->80/tcp mywebsit[root@docker ~]# docker stop mywebsit mywebsit[root@docker ~]# docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES[root@docker ~]# 2.9 启动容器命令可以通过start或restart启动或重启容器 语法 docker start|restart 容器id或容器名 案例 (1)启动停止的容器 [root@docker ~]# docker ps -aCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES783b46f5cddc httpd:2.4 "httpd-foreground" 6 hours ago Exited (0) 2 minutes ago mywebsit[root@docker ~]# docker start mywebsit mywebsit[root@docker ~]# docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES783b46f5cddc httpd:2.4 "httpd-foreground" 6 hours ago Up 1 second 0.0.0.0:8888->80/tcp mywebsit[root@docker ~]# 可以看到刚停止的mywebsit容器为exited状态,重启后又重新运行起来。 2.10 删除容器命令语法 docker rm [选项] 容器名或id 常用选项 选项 作用-f --forece,强制删除,当容器在运行时只能强制删除-v --vomumes,删除数据卷案例 (1)删除正在运行容器 [root@docker ~]# docker rm mywebsitError response from daemon: You cannot remove a running container 783b46f5cddcc3ea919329a99f83a783da98bce4abce05ccc9b3f27fda859b09. Stop the container before attempting removal or force remove[root@docker ~]# docker rm -f mywebsitmywebsit[root@docker ~]# docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES[root@docker ~]# 可以看到当容器运行时不能删除,需要添加-f参数强制删除。 (2)强制删除所有容器 慎用,仅供演示,他会删除所有的容器,包括正在运行的和已经停止的。 [root@docker ~]# docker rm -f $(docker ps -aq) 3d228a470c53[root@docker ~]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES[root@docker ~]#上边这个删除语句也可以写为:docker ps -a -q | xargs docker rm 三、容器内部窥探以上基于httpd镜像创建了容器进行各种容器操作的演示,接下来我们在通过centos镜像,研究下镜像内部的结构和原理 [root@docker ~]# docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEcentos latest 470671670cac 6 weeks ago 237MB如果你本地还没有centos进行,先用docker pull命令下载到本地,接下来我们用它来创建一个名为mycentos的容器,然后进入容器查看结构 [root@docker ~]# docker run -it --name mycentos centos[root@bda9ff3abfd9 /]# lsbin etc lib lost+found mnt proc run srv tmp vardev home lib64 media opt root sbin sys usr[root@bda9ff3abfd9 /]# pwd/[root@bda9ff3abfd9 /]# cat /etc/redhat-release CentOS Linux release 8.1.1911 (Core) [root@bda9ff3abfd9 /]# 进入容器后,我们可以看到,其实它就是运行着的一个centos系统,里边可以执行各种centos命令,也可以查看到内核版本。 如果你想象力在扩展一下,那很容易想到,镜像其实就是把各种操作系统环境以及我们运行所需要的软件包打包在一起,然后上传到仓库中,需要的时候直接pull下来,在运行run命令创建容器即可。这也正是我们前二篇文章反复提到的内容,如果现在在返回去看之前的文章,应该会有更深刻的理解。 本文演示完容器相关操作命令后,docker三要素就还差仓库了,下一篇讲解仓库相关理论和操作,敬请期待。 作者:黑马腾云原文地址https://www.cnblogs.com/heimatengyun/p/12886651.html
.net core HttpClient 使用之掉坑解析(一) 一、前言在我们开发当中经常需要向特定URL地址发送Http请求操作,在.net core 中对httpClient使用不当会造成灾难性的问题,这篇文章主要来分享.net core中通过IHttpClientFactory 工厂来使用HttpClient的正确打开方式。 二、HttpClient使用中的那些坑2.1 错误使用using(var client = new HttpClient())我们可以先来做一个简单的测试,代码如下: public async Task GetBaiduListAsync(string url) { var html = ""; for (var i = 0; i < 10; i++) { using (var client = new System.Net.Http.HttpClient()) { var result=await client.GetStringAsync(url); html += result; } } return html; }运行项目输出结果后,通过netstate查看下TCP连接情况: 虽然项目已经运行结束,但是连接依然存在,状态为" TIME_WAIT"(继续等待看是否还有延迟的包会传输过来;默认在windows下,TIME_WAIT状态将会使系统将会保持该连接 240s。在高并发的情况下,连接来不及释放,socket被耗尽,耗尽之后就会出现喜闻乐见的一个错误: 错误原因:对象所占用资源应该确保及时被释放掉,但是,对于网络连接而言,这是错误的,原因有如下: 网络连接是需要耗费一定时间的,频繁开启与关闭连接,性能会受影响;开启网络连接时会占用底层socket资源,但在HttpClient调用其本身的Dispose方法时,并不能立刻释放该资源,这意味着你的程序可能会因为耗尽连接资源而产生灾难性的问题。对于上面的错误原因,大家可能会想到使用静态单例模式的HttpClient,如下: private static HttpClient Client = new HttpClient();静态单例模式虽然可以解决上面问题,但是会带来另外一个问题: DNS变更会导致不能解析,DNS不会重新加载,需要重启才能变更(有兴趣的大佬可以去尝试一下)三、正确使用及源码分析 HttpClientFactory 以模块化、可命名、可配置、弹性方式重建了 HttpClient 的使用方式: 由 DI 框架注入 IHttpClientFactory 工厂;由工厂创建 HttpClient 并从内部的 Handler 池分配请求 Handler。 .net core 2.1 开始引入了IHttpClientFactory 工厂类来自动管理IHttpClientFactory 类的创建和资源释放,可以通过Ioc 注入方式进行使用,代码如下: services.AddControllers();services.AddHttpClient();调用代码如下: private readonly IHttpClientFactory _clientFactory; public FirstController(IHttpClientFactory clientFactory){ _clientFactory = clientFactory; } /// /// /// /// /// public async Task GetBaiduAsync(string url){ var client = _clientFactory.CreateClient(); var result = await client.GetStringAsync(url); return result; }代码中通过IHttpClientFactory 中的CreateClient()方法进行创建一个HttpClient 对象,但是没有看到有释放资源的动作,那它是怎么释放的呢?我们来看看它的主要源代码 /// /// Creates a new using the default configuration./// /// The ./// An configured using the default configuration.public static HttpClient CreateClient(this IHttpClientFactory factory){ if (factory == null) { throw new ArgumentNullException(nameof(factory)); } return factory.CreateClient(Options.DefaultName); } public HttpClient CreateClient(string name){ if (name == null) { throw new ArgumentNullException(nameof(name)); } var handler = CreateHandler(name); var client = new HttpClient(handler, disposeHandler: false); var options = _optionsMonitor.Get(name); for (var i = 0; i < options.HttpClientActions.Count; i++) { options.HttpClientActions[i](client); } return client; } public HttpMessageHandler CreateHandler(string name){ if (name == null) { throw new ArgumentNullException(nameof(name)); } var entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value; StartHandlerEntryTimer(entry); return entry.Handler; }代码中可以看到创建HttpClent 时会先创建HttpMessageHandler对象,而CreateHandler 方法中调用了StartHandlerEntryTimer方法,该方法主要时启动清理释放定时器方法,核心代码如下: public DefaultHttpClientFactory( IServiceProvider services, IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory, IOptionsMonitor<HttpClientFactoryOptions> optionsMonitor, IEnumerable<IHttpMessageHandlerBuilderFilter> filters) { if (services == null) { throw new ArgumentNullException(nameof(services)); } if (scopeFactory == null) { throw new ArgumentNullException(nameof(scopeFactory)); } if (loggerFactory == null) { throw new ArgumentNullException(nameof(loggerFactory)); } if (optionsMonitor == null) { throw new ArgumentNullException(nameof(optionsMonitor)); } if (filters == null) { throw new ArgumentNullException(nameof(filters)); } _services = services; _scopeFactory = scopeFactory; _optionsMonitor = optionsMonitor; _filters = filters.ToArray(); _logger = loggerFactory.CreateLogger<DefaultHttpClientFactory>(); // case-sensitive because named options is. _activeHandlers = new ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>>(StringComparer.Ordinal); _entryFactory = (name) => { return new Lazy<ActiveHandlerTrackingEntry>(() => { return CreateHandlerEntry(name); }, LazyThreadSafetyMode.ExecutionAndPublication); }; _expiredHandlers = new ConcurrentQueue<ExpiredHandlerTrackingEntry>(); _expiryCallback = ExpiryTimer_Tick; _cleanupTimerLock = new object(); _cleanupActiveLock = new object(); } // Internal for tests internal void ExpiryTimer_Tick(object state) { var active = (ActiveHandlerTrackingEntry)state; // The timer callback should be the only one removing from the active collection. If we can't find // our entry in the collection, then this is a bug. var removed = _activeHandlers.TryRemove(active.Name, out var found); Debug.Assert(removed, "Entry not found. We should always be able to remove the entry"); Debug.Assert(object.ReferenceEquals(active, found.Value), "Different entry found. The entry should not have been replaced"); // At this point the handler is no longer 'active' and will not be handed out to any new clients. // However we haven't dropped our strong reference to the handler, so we can't yet determine if // there are still any other outstanding references (we know there is at least one). // // We use a different state object to track expired handlers. This allows any other thread that acquired // the 'active' entry to use it without safety problems. var expired = new ExpiredHandlerTrackingEntry(active); _expiredHandlers.Enqueue(expired); Log.HandlerExpired(_logger, active.Name, active.Lifetime); StartCleanupTimer(); } // Internal so it can be overridden in tests internal virtual void StartHandlerEntryTimer(ActiveHandlerTrackingEntry entry) { entry.StartExpiryTimer(_expiryCallback); } 从微软源码分析,HttpClient继承自HttpMessageInvoker,而HttpMessageInvoker实质就是HttpClientHandler。 HttpClientFactory 创建的HttpClient,也即是HttpClientHandler,只是这些个HttpClient被放到了“池子”中,工厂每次在create的时候会自动判断是新建还是复用。(默认生命周期为2min)。希望这篇文章对你有帮助,如果对你有帮助请点个推荐,感谢! 如果您认为这篇文章还不错或者有所收获,您可以点击右下角的【推荐】按钮精神支持,因为这种支持是我继续写作,分享的最大动力! 作者:Jlion原文地址https://www.cnblogs.com/jlion/p/12813692.html
自己动手在Linux系统实现一个everything程序 大家好,我是良许。 我们知道,在 Windows 下,有一款非常实用的神器,叫作 Everything ,它可以在极短的时间里,搜索出来你所想要的文件/目录,如下图示: Linux 下也有一些类似于 everything 的神器,比如:locate,Catfish,Tracker,等等。这些工具也十分强大,在此就不一一演示了,有兴趣的小伙伴可以自行去体验一下。 但是,其实我们自己也可以动手实现一个轻巧的 everything ,既可以满足自己的需求,也可以提高自己的技术,还能在程序媛面前秀一把~ 废话不多说,我们直接上脚本: !/bin/sh lazy find GNU All-Permissive License Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. This file is offered as-is, without any warranty. help function function helpu { echo " " echo "Fuzzy search for filename." echo "$0 [--match-case|--path] filename" echo " " exit } set variables MATCH="-iname"SEARCH="." parse options while [ True ]; doif [ "$1" = "--help" -o "$1" = "-h" ]; then helpu elif [ "$1" = "--match-case" -o "$1" = "-m" ]; then MATCH="-name" shift 1 elif [ "$1" = "--path" -o "$1" = "-p" ]; then SEARCH="${2}" shift 2 else break fidone sanitize input filenames create array, retain spaces ARG=( "${@}" ) set -e catch obvious input error if [ "X$ARG" = "X" ]; then helpu fi perform search for query in ${ARG[*]}; do /usr/bin/find "${SEARCH}" "${MATCH}" "*${ARG}*" done这段脚本不是良许的原创,而是国外一个小哥的作品。这个脚本写得还是非常简洁,并且使用了一些常用的 Shell 编程技术,好好去研究它也会提高自己的 Shell 编程水平。 那么这个脚本怎么用? 2020 精选 阿里/腾讯等一线大厂 面试、简历、进阶、电子书 公众号「良许Linux」后台回复「资料」免费获取 第一步,创建一个脚本文件。比如创建的脚本文件是 lazy_find.sh 。 $ vim lazy_find.sh然后,我们再把上面的脚本内容复制进去,再按 :wq 保存并退出。 接着,我们再用 chmod 命令赋予这个脚本可执行权限: $ chmod +x lazy_find.sh第二步,使用脚本搜索文件。最基本的用法,就是在脚本名称后面直接跟上要搜索的文件名称。不需要跟上完整的名称,只需要其中一部分就好,比如: $ ./lazy_find.sh scr运行结果如下: 可以看到,这个脚本不仅可以搜索当前路径下的文件,还可以搜索当前目录下的子目录及孙目录,也就是具有递归搜索的功能。 上面这种用法是不匹配大小写的效果,也就是说,既可以匹配到 scr ,也可以匹配到 Scr 。那么,如果想要区分大小写,需要怎么做? 只需加上 -m 选项即可。 $ ./lazy_find.sh -m scr运行结果如下: 可以看到,Scr 已经没有再被匹配到了。 如果我们不想搜索当前目录,而是想要搜索指定目录,则只需加上 --path ,并指定想要搜索的路径即可。 比如,我们想在家目录下搜索包含有 hello 关键字的文件/目录,可以这么运行命令: $ ./lazy_find.sh --path ~ hello运行结果如下: 2020 精选 阿里/腾讯等一线大厂 面试、简历、进阶、电子书 公众号「良许Linux」后台回复「资料」免费获取 第三步,自定义命令调用脚本在上面的使用方法中,我们需要自己手动去运行那个脚本文件,这样肯定是非常不方便的。我们可以自定义一个命令,比如 lf ,当运行这个命令时,就自动调用这个脚本,从而大大提高效率。 有 Linux 基础的小伙伴应该想到了,我们可以使用 alias 命令来实现这个效果。为了能够在系统重启之后 lf 命令依然能使用,我们直接在 .bashrc 里进行修改。 $ vim ~/.bashrc在 .bashrc 文件的末尾增加这么一行语句即可: alias lf=~/bin/lazy_find.sh #路径别照抄,写你自己的脚本路径!!增加完语句之后,按 :wq 保存并退出。然后,再使能我们的修改: $ . ~/.bashrcOK,大功告成了,我们在任意地方都可以使用这个命令了。 小结本文我们通过一段脚本来实现 everything 的一些基本功能,通过学习这个脚本,我们可以提高自己的 Shell 编程能力,同时也可以学会如何自定义命令来调用自己写的脚本。 本文比较基础,适合小白入门。 原文地址https://www.cnblogs.com/yychuyu/p/12878089.html
C# 数据操作系列 - 2. ADO.NET操作 0.前言在上一篇中初略的介绍了一下SQL的基本写法,这一篇开始我们正式步入C#操作数据库的范围。通过这一系列的内容,我想大家能对于数据库交互有了一定的认识和基础。闲话不多说,先给大家介绍一个C#操作数据库的方式。 ADO.NET的介绍在ADO.NET出现之前,C#连接数据库有很多种方式,各种框架琳琅满目。用户们饱受困扰,再加上乱七八糟的连接方式对语言的发展也是一种强有力的阻挠。所以微软决定搞一套标准化出来,之后ADO.NET诞生了。 ADO.NET定义了一系列操作数据库的接口和基类,而数据库厂商只需要根据自己的实际情况开发对应的实现类就可以了。 使用ADO.NET 操作需要的步骤使用ADO.NET操作数据库,先需要一个连接也就是 IDbConnection实例,然后使用IDbCommand执行,通过 IDataReader读取数据。 先来简单介绍一下上面提到的接口: a.IDbConnection 表示一个与数据源的开放连接,并由访问关系数据库的.NET 数据提供程序实现,也就是说这只是一个接口,具体的实现得看具体的数据库。 我们先看一下,具体的属性和方法吧: public string ConnectionString { get; set; }// 获取或设置用于打开数据库的连接字符串public string Database { get; }//获取当前连接或即将连接的数据库名称public System.Data.ConnectionState State { get; }//获取当前连接的状态public System.Data.IDbTransaction BeginTransaction ();//开启一项数据库事务public void ChangeDatabase (string databaseName);//修改已打开连接的当前数据库public void Close ();//关闭当前连接public System.Data.IDbCommand CreateCommand ();//创建并获取与该连接关联的命令对象public void Open ();//开启与数据库的连接 IDbCommand 表示连接到数据源时执行的SQL命令,并由访问关系数据库的.NET数据提供程序实现。与IDbConnection一致,也是一个接口。 这个接口的属性和方法如下: public string CommandText { get; set; } //获取或设置要对数据源运行的文本命令,也就是SQL语句public int CommandTimeout { get; set; } //获取或设置在终止尝试执行命令并生成错误之前的等待时间public System.Data.CommandType CommandType { get; set; }//指定或者获取解释CommandText属性的方式public System.Data.IDbConnection Connection { get; set; }//获取或设置执行该命令的连接public System.Data.IDataParameterCollection Parameters { get; }//获取命令的参数化列表public System.Data.IDbTransaction Transaction { get; set; }//获取或设置该命令关联的事务public void Cancel ();//尝试取消执行命令public System.Data.IDbDataParameter CreateParameter ();//创建一个参数public int ExecuteNonQuery ();//执行一个语句,并返回受影响的行数public System.Data.IDataReader ExecuteReader ();//在Connect上执行CommandText,并返回一个IDataReaderpublic object ExecuteScalar ();//执行查询,并返回第一行第一列,其他数据忽略 IDataReader 提供一种读取结果集(通过对数据源执行命令获取)的一个或多个只进流的方法,具体实现由访问关系数据库的.NET 数据提供程序实现。 这里的只进流的意思类似于只读流,也就是说它是一种单向的流,从数据库传向程序的流。 这个接口的属性和方法如下: public int Depth { get; }//获取一个值,该值指示当前行的嵌套深度public bool IsClosed { get; }// 获取该读取器的是否关闭public int RecordsAffected { get; }//获取由执行 SQL 语句而更改、插入或删除的行数public void Close ();//关闭IDataReader对象public System.Data.DataTable GetSchemaTable ();//获取一个描述该读取器关联的列元数据public bool NextResult ();//显示是否有下一行,如果有则在下次读取的时候,读取下一行的数据public bool Read ();//与NextResult类似同时,IDataReader 继承了接口 IDataRecord,也就是说IDataReader也能直接返回当前行的数据。 来,我们看看它从IDataRecord继承了哪些吧(也就是IDataRecord有的属性和方法)。 public int FieldCount { get; }//获取当前行中的列数public object this[int i] { get; }//获取位于指定索引处的列public object this[string name] { get; }//获取具有指定名称的列 public System.Data.IDataReader GetData (int i);//获取指定列序号的DataReaderpublic Type GetFieldType (int i);//获取对应于会从 Type 返回的 Object 类型的 GetValue(Int32) 信息public string GetDataTypeName (int i);//获取指定字段的数据类型信息public string GetName (int i);//获取要查找的字段的名称public int GetOrdinal (string name);//返回命名字段的索引 public bool GetBoolean (int i);//作为布尔值获取指定列的值public byte GetByte (int i);//获取指定列的 8 位无符号整数值public char GetChar (int i);//获取指定列的字符值public DateTime GetDateTime (int i);//获取指定字段的日期和时间数据值public decimal GetDecimal (int i);//获取指定字段的数值public double GetDouble (int i);//获取指定字段的双精度浮点数public float GetFloat (int i);//获取指定字段的单精度浮点数public Guid GetGuid (int i);//获取指定字段的GUID值public short GetInt16 (int i);//获取指定字段的 16 位带符号整数值public long GetInt64 (int i);//获取指定字段的 64 位带符号整数值public string GetString (int i);//获取指定字段的字符串值public object GetValue (int i);//返回指定字段的值public int GetValues (object[] values);// 将当前记录的值按顺序填充到数组中,并返回实际的数目public bool IsDBNull (int i);//返回指定字段是否设置为 null特别补充说明 ConnectionState 是一个枚举状态,表示数据连接状态,其属性值如下: 字段 值 说明Broken 16 与数据源的连接中断。 只有在连接打开之后才可能发生这种情况。 可以关闭处于这种状态的连接,然后重新打开。 (该值是为此产品的未来版本保留的。)Closed 0 连接已关闭。Executing 4 连接对象正在执行命令。 (该值是为此产品的未来版本保留的。)Fetching 8 连接对象正在检索数据。 (该值是为此产品的未来版本保留的。)Open 1 连接处于打开状态。CommandType 用来指定如何解释命令字符串,属性值如下: 字段 值 说明StoredProcedure 4 存储过程的名称。TableDirect 512 表的名称。Text 1 SQL 文本命令。 (默认。) 如何使用ADO.NET操作数据库以SQLServer为例,创建一个Connection: using System.Data;using System.Data.SqlClient;//Sql Server的命名空间string connectStr = "";IDbConnection connection = new SqlConnection(connectStr);在.NET Framework中,以上代码是正确的,因为.NET Framework内置了SQL Server的数据访问程序,也就是数据驱动。但是在.NET Core中,需要为项目添加如下包的引用: System.Data.SqlClient 这里简单介绍一下如何使用Visual Studio安装包: 如图所示,在【工具】->【NuGet 包管理器】下找到 管理解决方案的NuGet程序包,点击。然后会出现类似于下图: 然后点击浏览,输入:System.Data.SqlClient 选中第一项,然后在右侧勾选要添加包的项目,然后点击安装。 OK,现在假设你们都已经安装好了。然后继续下一步操作: 执行一个查询语句: select name from demo;创建一个Command: 这时候创建Command有这样几种方式: IDbCommand command = connection.CreateCommand();// 通过 connection创建一个命令IDbCommand command = new SqlCommand();//简单创建一个命令对象IDbCommand command = new SqlCommand(sql,connection);//在初始化的时候,指定要执行的SQL和连接的Connection如果在创建Command的时候,没有指定连接和要执行的SQL语句,那么必须在获取Reader之前,手动设置。 现在获取一个Reader: IDataReader reader = command.ExecuteReader();如果只是想执行SQL,不关心返回内容的话,可以调用以下方法: int lines = command.ExecuteNonQuery();// 获取受影响的行数该方法适合于SQL是DML类型的SQL语句或者增删改的SQL语句。 如果是查询语句,则需要获取Reader,然后通过Reader获取对应的值。 实践在大概讲解了SQL,我们通过实践练习把之前了解到的内容串联起来。 a.创建一个表 var connectStr = "";var sql = @"create table demo( [key] int identity primary key, [name] varchar(20) )";IDbConnection connection = new SqlConnection(connectStr);IDbCommand command = connection.CreateCommand();connection.Open();//开启链接command.CommandText = sql;//赋值sqlvar result = command.ExecuteNonQuery();//获取返回的int值,是-1connection.Close();//用完了记得把链接关闭对于 ExcuteNonQuery的返回值,微软在官方文档中给出了这样的描述: 对于 UPDATE、INSERT 和 DELETE 语句,返回值为该命令所影响的行数。 对于所有其他类型的语句,返回值是 -1。 所以这里的返回值是-1。 b.添加一条数据 与创建表类似,区别在于使用的SQL语句不同。在C#中,使用ADO.NET 向数据库添加值,需要手动拼接SQL语句来操作。 代码如下:(假设使用在上一个示例里创建的表) 拼接SQL: var value1 = "测试";var sql = @$"insert into demo(name) values('{value1}');";//====或者var sql = string.Format("insert into demo(name) values('{0}');", "测试");注意SQL语句拼接过程中的单引号,这在SQL中表示中间是字符串。SQL有很强的将字符串转换成对应字段类型的能力,所以可以统一传给数据库字符串。 执行SQL: var connectStr = "";IDbConnection connection = new SqlConnection(connectStr);IDbCommand command = connection.CreateCommand();connection.Open();command.CommandText = sql;var result = command.ExecuteNonQuery();connection.Close();这次如果没有执行错误的话,会返回一个1。 c.修改记录 例如修改demo里name为测试的数据,将其name修改为demo: var connectStr = "";var sql = "update demo set name = 'demo' where name = ‘测试’";IDbConnection connection = new SqlConnection(connectStr);IDbCommand command = connection.CreateCommand();connection.Open();command.CommandText = sql;var result = command.ExecuteNonQuery();Console.WriteLine(result);connection.Close();其中result表示SQL影响的表中数据行数。 删除记录 var sql = "delete table demo where name = ‘测试’";IDbConnection connection = new SqlConnection(connectStr);IDbCommand command = connection.CreateCommand();connection.Open();command.CommandText = sql;var result = command.ExecuteNonQuery();Console.WriteLine(result);connection.Close(); 查询 这里就先容我卖个关子,不过大家可以自己试试ADO.NET的查询 说明在第四小节里提到了连接字符串,对于C#来说,不同数据库应当有不同的连接字符串。因为这是C#连接数据库的一种指令或者是密钥。 简单介绍一下连接字符串,它是用分号隔开的键值对列表。格式如下: keyword1=value; keyword2=value;以下是一个标准的SQL Server连接字符串: Persist Security Info=False;User ID=;Password=;Initial Catalog=AdventureWorks;Server=MySqlServer其中: Persist Security Info=False 表示使用账户密码连接数据库User ID 表示用户名Password 表示密码Initial Catalog=AdventureWorks 表示连接的数据库是 AdventureWorks ,可根据自己需要修改Server=MySqlServer 表示数据库在 MySqlServer 这个服务器上,可以是IP地址或者域名等之所以留下了查询没有说,因为在ADO.NET中还有一种更棒的方式操作数据库。这就是下篇内容要讲的。如果有需要的小伙伴,别忘了关注评论哈。 原文地址https://www.cnblogs.com/c7jie/p/12868458.html
一分钟明白MySQL聚簇索引和非聚簇索引 MySQL的InnoDB索引数据结构是B+树,主键索引叶子节点的值存储的就是MySQL的数据行,普通索引的叶子节点的值存储的是主键值,这是了解聚簇索引和非聚簇索引的前提 什么是聚簇索引?很简单记住一句话:找到了索引就找到了需要的数据,那么这个索引就是聚簇索引,所以主键就是聚簇索引,修改聚簇索引其实就是修改主键。 什么是非聚簇索引?索引的存储和数据的存储是分离的,也就是说找到了索引但没找到数据,需要根据索引上的值(主键)再次回表查询,非聚簇索引也叫做辅助索引。 clustered index(MySQL官方对聚簇索引的解释)The InnoDB term for a primary key index. InnoDB table storage is organized based on the values of the primary key columns, to speed up queries and sorts involving the primary key columns. For best performance, choose the primary key columns carefully based on the most performance-critical queries. Because modifying the columns of the clustered index is an expensive operation, choose primary columns that are rarely or never updated.注意标黑的那段话,聚簇索引就是主键的一种术语 一个例子下面我们创建了一个学生表,做三种查询,来说明什么情况下是聚簇索引,什么情况下不是。 create table student ( id bigint, no varchar(20) , name varchar(20) , address varchar(20) , PRIMARY KEY (`branch_id`) USING BTREE, UNIQUE KEY `idx_no` (`no`) USING BTREE )ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;第一种,直接根据主键查询获去所有字段数据,此时主键时聚簇索引,因为主键对应的索引叶子节点存储了id=1的所有字段的值。 select * from student where id = 1第二种,根据编号查询编号和名称,编号本身是一个唯一索引,但查询的列包含了学生编号和学生名称,当命中编号索引时,该索引的节点的数据存储的是主键ID,需要根据主键ID重新查询一次,所以这种查询下no不是聚簇索引 select no,name from student where no = 'test'第三种,我们根据编号查询编号(有人会问知道编号了还要查询?要,你可能需要验证该编号在数据库中是否存在),这种查询命中编号索引时,直接返回编号,因为所需要的数据就是该索引,不需要回表查询,这种场景下no是聚簇索引 select no from student where no = 'test'总结主键一定是聚簇索引,MySQL的InnoDB中一定有主键,即便研发人员不手动设置,则会使用unique索引,没有unique索引,则会使用数据库内部的一个行的id来当作主键索引,其它普通索引需要区分SQL场景,当SQL查询的列就是索引本身时,我们称这种场景下该普通索引也可以叫做聚簇索引,MyisAM引擎没有聚簇索引。 原文地址https://www.cnblogs.com/sy270321/p/12864357.html
你离高薪 offer 只差一个Redis入门,我是认真的 说起来,可能有些小伙伴会不相信,我是第一次用 Redis,真的。因为公司小,业务量小,Redis 根本派不上用场。不过,最近打算把系统升级一下,顺带把当下时髦的技术入个门,“与时俱进”嘛,虽然进的有“一点点”晚(注意双引号)。 作为一名富有责任心的技术博主,我觉得有必要把我入门 Redis 的过程分享出来,供一些小伙伴作为参考。要是我哪里写错了,别客气,过来给我一巴掌,就行了(温柔点,别打肿,影响颜值就不好了)。 01、Redis 是什么Redis 是互联网技术领域中使用最广泛的存储中间件,它是 Remote Dictionary Service 三个单词中加粗字母的组合。你别说,组合起来后念着挺自然的。 Redis 以超高的性能、完美的文档、简洁的源码著称,国内外很多大型互联网公司都在用,比如说阿里、腾讯、GitHub、Stack Overflow 等等。它的版本更新非常的快,功能也越来越强大,最初只是用来作为缓存数据库,现在已经可以用它来实现消息队列了。 可以这么说吧,掌握 Redis 已经变成了一项后端工程师必须具备的基础技能。 Redis 的作者是一名意大利人,网名 Antirez,长相还是过得去的,感兴趣的小伙伴可以 Google 一下。知道为什么 Redis 的默认端口是 6379 吗? 据说是手机键盘上“MERZ”的位置决定的,小伙伴们可以打开自己手机上九宫格键盘感受一下。“MERZ”是什么意思呢?据说是“愚蠢”的意思。这?是不是感觉程序员的生活中还是有蛮多神秘色彩的? 02、安装 RedisRedis 针对不同的操作系统有不同的安装方式,我们这篇入门的文章就以 Windows 为例吧。 下载地址如下: https://github.com/MicrosoftArchive/redis/releases Windows 最新的版本是 3.2.100。从下图中可以看得出,Redis 的体积非常的轻量级,还不到 6 M。体积越小,让我感觉 Redis 越牛逼,你是不是也有这种感觉? 有两种安装方式,第一种是 msi 的方式,双击运行后安装;第二种是免安装,绿色版,只需要把 zip 包解压就可以了。 里面有一份英文版的文档——Windows Service Documentation.docx,教我们如何安装 Redis 服务、如何启动、如何关闭,以及如何使用自定义端口启动服务。 打开命令行,进入到当前解压后的目录,输入启动命令: redis-server redis.windows.conf然后你就会看到 Redis 启动后的欢迎画面,左边这个盒子感觉好有艺术感啊!有小伙伴知道是怎么生成的吗? 还有一些其他的提示信息: Redis 当前的版本号为 3.2.100端口是 6379进程 ID,也就是 PID 为 12636Redis 官方地址为:http://redis.io那如何停止服务呢?可以直接按下 Ctrl+C 组合键——粗暴、壁咚(当然可以直接点右上角的叉号)。 PS:本来想用 Linux 版或者 OSX 版的,怕入门的小伙伴没有环境。后面可以整一个。 03、Redis 的数据结构Redis 有 5 种基础数据结构,String、Hash、List、Set、SortedSet,也是学 Redis 必须掌握的。除此之外,还有 HyperLogLog、Geo、Pub/Sub,算是高级数据结构了。我们这篇入门的文章就以 String 为例吧。 String 结构使用非常广泛,比如说把用户的登陆信息转成 JSON 字符串后缓存起来,等需要取出的时候再反序列化一次。 小伙伴们应该都知道,Java 的 String 是不可变的,无法修改。Redis 的 String 是动态的,可以修改的,两者不同哦。关于 Redis 的 String 结构,我觉得老钱的 Redis 教程上讲得非常明白,大家一起拜读下。 Redis 的 String 在内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。如上图所示,当前字符串实际分配的空间为 capacity,一般高于实际的字符串长度 len。当字符串长度小于 1M 时,扩容是对现有空间的成倍增长;如果长度超过 1M 时,扩容一次只会多增加 1M 的空间。最大长度为 512M。 04、实操 Redis好了好了,我估计很多小伙伴们已经整装待发,准备实操一把了。这就来。 Redis 的解压目录下有一个名叫 redis-cli.exe 的文件,这是 Redis 自带的一个客户端工具,可以用来连接之前我们启动好的 Redis 服务。双击启动它。 这个客户端还是非常智能的,当键入命令的时候,会跳出对应的提示 当按下空格跟进关键字的时候,对应位置上的提示会自动消失。 以下是完整的键值对测试命令,小伙伴们可以按照格式动手实操一把。 set name cmowerOK get name "cmower" exists name (integer) 1 del name (integer) 1 get name (nil)1)set 命令用来存储一个键值对,在本例中,name 为 key,cmower 为 值。 2)get 命令用来获取一个键值对。 3)exists 命令用来测试一个键值对是否存在,(integer) 1 表示存在,(integer) 0 表示不存在。 4)del 命令用来删除一个键值对,(integer) 1 表示执行成功,(integer) 0 表示执行失败。 5)当键值对删除后,再通过 get 命令获取时,结果就为 (nil) 。 可能有小伙伴会好奇,nil 是什么意思?它是 Objective-C、Swift、Ruby、Lua 等编程语言中的一个关键字,更详细的解释可以看一下《Programming in Lua 程序设计第二版》: nil 是一种类型,它只有一个值 nil,它的主要功能是用于区别其他任何值,就像之前所说的,一个全局变量在第一次赋值前的默认值就是 nil,将 nil 赋予一个全局变量等同于删除它,Lua 将 nil 用于表示一种“无效值(non-value)”的情况,即没有任何有效值的情况。 想了解 Redis 命令的具体使用方法,可以参考以下链接: http://redisdoc.com/index.html 是 Redis Command Reference 和 Redis Documentation 的中文翻译版,良心吧? 05、在 Java 中使用 Redis有些小伙伴可能会问,“二哥,我是一名 Java 程序员,我该如何在 Java 中使用 Redis 呢?”这个问题问得好,这就来,这就来。 第一步,在项目中添加 Jedis(Java 和 Redis 的混拼) 依赖: redis.clients jedis 3.2.0第二步,新建 UserInfo(用户信息)类: public class UserInfo { private String name; private int age; public UserInfo(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "UserInfo{" + "name='" + name + ''' + ", age=" + age + '}'; } // getter / setter}第三步,在项目中添加 Gson(用于序列化和反序列化用户信息) 依赖: com.google.code.gson gson 2.8.6 compile第四步,新建测试类 RedisTest: public class RedisTest { private static final String REDIS_KEY = "user"; public static void main(String[] args) { Jedis jedis = new Jedis("localhost", 6379); Gson gson = new Gson(); UserInfo userInfo = new UserInfo("沉默王二", 18); jedis.set(REDIS_KEY, gson.toJson(userInfo)); UserInfo getUserInfoFromRedis = gson.fromJson(jedis.get(REDIS_KEY),UserInfo.class); System.out.println("get:" + getUserInfoFromRedis); System.out.println("exists:" + jedis.exists(REDIS_KEY)); System.out.println("del:" + jedis.del(REDIS_KEY)); System.out.println("get:" + jedis.get(REDIS_KEY)); }}1)REDIS_KEY 常量为存储用户信息到 Redis 的 key。 2)在 Jedis 的帮助下,Java 连接 Redis 服务变得非常简单,只需要一行代码: Jedis jedis = new Jedis("localhost", 6379);参数分别是主机名,端口号。 存储键值对用 set() 方法,获取键值对用 get() 方法,判断键值对是否存在用 exists() 方法,删除键值对用 del() 方法。 3)Gson 是谷歌提供的一个开源库,可以将 Java 对象序列化为 JSON 字符串,同样可以将 JSON 字符串反序列化(解析)为匹配的 Java 对象。 使用起来也非常简单,toJson() 方法将对象转成 JSON 字符串,fromJson() 方法将 JSON 字符串反序列化对象。 好了,来看一下程序的输出结果: get:UserInfo{name='沉默王二', age=18}exists:truedel:1get:null完全符合我们的预期,perfect! 06、鸣谢好了,我亲爱的小伙伴们,以上就是本文的全部内容了,是不是看完后很想实操一把 Redis,赶快行动吧!如果你在学习的过程中遇到了问题,欢迎随时和我交流,虽然我也是个菜鸟,但我有热情啊。 另外,如果你想写入门级别的文章,这篇就是最好的范例。 原文地址https://www.cnblogs.com/qing-gee/p/12855763.html
C# 基础知识系列- 17 小工具优化 前言不知道有没有动手能力强的小伙伴照着上一篇的内容写过程序呢?如果有的话,应该会在使用的时候发现以下几个问题: 每次启动都需要经过漫长的时间去遍历磁盘里的文件目录因为数据是用的字典保存的,所以会消耗大量的内存空间不能多次查询现在我们就针对这些问题,让我们的小工具实用起来。 分析与实现在动手之前,我们先分析一下问题。在实际开发之前,无论是接到什么需求都要先仔细分析一下,确定好方案再动手方为开发的正道。嗯,没毛病。因为开发过程中跟产品对线、跟客户对线要占整个项目的一半左右时间。好了,不废话了。继续: 遍历文件目录的时间过长,那么我们是不是可以用异步并发去遍历呢? 数据用字典保存会消耗内存空间,那么我们是不是可以用其他的方式保存呢? 不能多次查询,是不是可以使用循环,然后设置一个退出条件? 1.1 C#的异步/并发实现在C#里,异步和并发的实现是依据线程、任务来实现的。在之前《C# 基础知识系列- 12 任务和多线程》里大概介绍了一下线程和任务,我们知道线程本身是没法返回数据的,它与主线程进行数据交互的过程十分需要注意线程安全。而任务可以返回数据,不需要像线程一样小心翼翼地与主线程进行数据交互。任务有一个优点,它比线程更轻量,所以在当前环境下我们可以试试任务。 当然,线程也有优点,那就是线程的运行环境相对更封闭一点,它能完成一个长的大型运算。 那么继续上一篇的内容,先引用 : using System.Threading.Tasks;先提取一组根据可枚举目录集合创建任务组并取得结果的方法: public static Dictionary> OverDirectories(IEnumerable directories){ var tasks = directories.Select(dir => Task.Run(()=>OverDirectories(dir))).ToArray(); Task.WaitAll(tasks);// 这行的意思是等待所有任务完成 return Concat(tasks.Select(t=>t.Result).ToArray()); }然后改造原有的OverDirectories方法: public static Dictionary> OverDirectories(DirectoryInfo rootDirectory){ Console.WriteLine($"正在遍历目录:{rootDirectory.FullName}"); var dict = new Dictionary<string, List<string>>(); IEnumerable<FileInfo> files = new List<FileInfo>(); try { files = rootDirectory.EnumerateFiles(); } catch(Exception e) { Console.WriteLine($"错误信息:{e}");//打印错误信息 } foreach(var file in files) { var key = Path.GetFileNameWithoutExtension(file.Name); if(!dict.ContainsKey(key)) { dict[key] = new List<string>(); } dict[key].Add(file.FullName); } try { var dirs = rootDirectory.EnumerateDirectories(); return Concat(dict, OverDirectories(dirs));// 采用线程版的方法进行遍历 } catch (System.Exception e) { Console.WriteLine($"错误信息:{e}");//打印错误信息 } return dict; }1.2 数据复用理想状态下,我们的数据应该是保存在数据库的,但因为数据库的操作是在下一系列的教程中,所以目前只能舍弃这个设想。 那么,利用现有方式,我们可以使用文件作为缓存的方式,也就是说把数据保存在文件里,在需要的时候从文件中读取出来。这时候就需要一组操作文件的方法。 首先,声明一个静态变量: public static readonly string TempFile = "temp.txt";然后编写读取、存放数据的方法: public static void WriteLinesToTemp(List lines){ File.AppendAllLines(TempFile, lines); } public static List Search(string file){ var lines = File.ReadLines(file); var results = lines.Where(line=>Path.GetFileNameWithoutExtension(line).Contains(file)); return results.ToList(); }这时候在文件中存放的都是路径文件,所以需要重新修改遍历文件路径的方法,只保留路径: public static List OverDirectories(DirectoryInfo rootDirectory){ Console.WriteLine($"正在遍历目录:{rootDirectory.FullName}"); List<string> files = new List<string>(); try { files.AddRange(rootDirectory.GetFiles().Select(f=>f.FullName).ToList()); Console.WriteLine($"在目录:{rootDirectory.FullName} 下 找到 文件:{files.Count} 个"); } catch(Exception e) { Console.WriteLine($"加载目录:{rootDirectory.FullName} 中\t错误信息:{e}");//打印错误信息 } try { var dirs = rootDirectory.GetDirectories(); OverDirectories(dirs); } catch (System.Exception e) { Console.WriteLine($"在下探目录{rootDirectory.FullName}时发生错误:{e}"); } return files; } public static void OverDirectories(IEnumerable directories){ var tasks =new List<Task<List<string>>>( directories.Select(dir => Task.Run(()=>OverDirectories(dir)))); while(tasks.Any()) { var completeds = tasks.Where(t=>t.IsCompleted).ToList(); // 提取所有已完成的任务 foreach(var t in completeds) { WriteLinesToTemp(t.Result);// 保存文件列表 tasks.Remove(t);//移除已处理的任务 } } }最后修改主方法,设置启动时遍历路径的规则: static void Main(string[] args){ if(!File.Exists(TempFile))// 缓存文件存在,则认为上次已经遍历成功了 { var drivers = GetDrivers(); OverDirectories(drivers); } Console.WriteLine("请输入要查询的文件名:"); var search = Console.ReadLine().Trim(); }1.3 循环使用并设置退出条件设置用户输入q或Q的时候退出程序,这时候就需要改造Main方法了: static void Main(string[] args){ Console.WriteLine("文件查询小工具启动了……"); if(!File.Exists(TempFile)) { Console.WriteLine("尚未加载缓存记录,数据加载中……"); var drivers = GetDrivers(); OverDirectories(drivers); Console.WriteLine("数据加载完成"); Thread.Sleep(500); Console.Clear();// 清除控制台 } while(true) { Console.WriteLine("请输入要查询的文件名(输入q/Q 退出):"); var search = Console.ReadLine().Trim();// 去除多余的空白字符 if(search == "q" || search == "Q")//添加退出条件 { break; } Console.WriteLine("查询中……"); var results = Search(search); Console.WriteLine("查询结果:"); foreach(var r in results) { Console.WriteLine(r); } } Console.WriteLine("程序已退出!"); }在main 方法里加了很多提示语句,以方便使用。 总结以上是第一次实战课的所有内容。欢迎各位小伙伴们踊跃讨论。这个小工具并不完善,但是随着我们对.net core的了解和深入就会写的得心应手了。 原文地址https://www.cnblogs.com/c7jie/p/12849942.html
Redis 6.0 新特性-多线程连环13问! Redis 6.0 来了 在全国一片祥和IT民工欢度五一节假日的时候,Redis 6.0不声不响地于5 月 2 日正式发布了,吓得我赶紧从床上爬起来,学无止境!学无止境!对于6.0版本,Redis之父Antirez在RC1版本发布时(2019-12-19)在他的博客上连续用了几个“EST”词语来评价: the most “enterprise” Redis version to date // 最”企业级”的the largest release of Redis ever as far as I can tell // 最大的the one where the biggest amount of people participated // 参与人数最多的 这个版本提供了诸多令人心动的新特性及功能改进,比如新网络协议RESP3,新的集群代理,ACL等,其中关注度最高的应该是“多线程”了,笔者也第一时间体验了一下,带着众多疑问,我们来一起开始“Redis 6.0 新特性-多线程连环13问”。 Redis 6.0 多线程连环13问 Redis6.0之前的版本真的是单线程吗? Redis在处理客户端的请求时,包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。但如果严格来讲从Redis4.0之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 key 的删除等等。 Redis6.0之前为什么一直不使用多线程? 官方曾做过类似问题的回复:使用Redis时,几乎不存在CPU成为瓶颈的情况, Redis主要受限于内存和网络。例如在一个普通的Linux系统上,Redis通过使用pipelining每秒可以处理100万个请求,所以如果应用程序主要使用O(N)或O(log(N))的命令,它几乎不会占用太多CPU。 使用了单线程后,可维护性高。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。Redis通过AE事件模型以及IO多路复用等技术,处理性能非常高,因此没有必要使用多线程。单线程机制使得 Redis 内部实现的复杂度大大降低,Hash 的惰性 Rehash、Lpush 等等 “线程不安全” 的命令都可以无锁进行。 3.Redis6.0为什么要引入多线程呢? Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,对于小数据包,Redis服务器可以处理80,000到100,000 QPS,这也是Redis处理的极限了,对于80%的公司来说,单线程的Redis已经足够使用了。 但随着越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要更大的QPS。常见的解决方案是在分布式架构中对数据进行分区并采用多个服务器,但该方案有非常大的缺点,例如要管理的Redis服务器太多,维护代价大;某些适用于单个Redis服务器的命令不适用于数据分区;数据分区无法解决热点读/写问题;数据偏斜,重新分配和放大/缩小变得更加复杂等等。 从Redis自身角度来说,因为读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶颈主要在于网络的 IO 消耗, 优化主要有两个方向: • 提高网络 IO 性能,典型的实现比如使用 DPDK 来替代内核网络栈的方式• 使用多线程充分利用多核,典型的实现比如 Memcached。 协议栈优化的这种方式跟 Redis 关系不大,支持多线程是一种最有效最便捷的操作方式。所以总结起来,redis支持多线程主要就是两个原因: • 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核• 多线程任务可以分摊 Redis 同步 IO 读写负荷 4.Redis6.0默认是否开启了多线程? Redis6.0的多线程默认是禁用的,只使用主线程。如需开启需要修改redis.conf配置文件:io-threads-do-reads yes 5.Redis6.0多线程开启时,线程数如何设置? 开启多线程后,还需要设置线程数,否则是不生效的。同样修改redis.conf配置文件 关于线程数的设置,官方有一个建议:4核的机器建议设置为2或3个线程,8核的建议设置为6个线程,线程数一定要小于机器核数。还需要注意的是,线程数并不是越大越好,官方认为超过了8个基本就没什么意义了。 6.Redis6.0采用多线程后,性能的提升效果如何? Redis 作者 antirez 在 RedisConf 2019分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。国内也有大牛曾使用unstable版本在阿里云esc进行过测试,GET/SET 命令在4线程 IO时性能相比单线程是几乎是翻倍了。 测试环境: Redis Server: 阿里云 Ubuntu 18.04,8 CPU 2.5 GHZ, 8G 内存,主机型号 ecs.ic5.2xlargeRedis Benchmark Client: 阿里云 Ubuntu 18.04,8 2.5 GHZ CPU, 8G 内存,主机型号 ecs.ic5.2xlarge 测试结果: 详见:https://zhuanlan.zhihu.com/p/76788470 说明1:这些性能验证的测试并没有针对严谨的延时控制和不同并发的场景进行压测。数据仅供验证参考而不能作为线上指标。 说明2:如果开启多线程,至少要4核的机器,且Redis实例已经占用相当大的CPU耗时的时候才建议采用,否则使用多线程没有意义。所以估计80%的公司开发人员看看就好。 7.Redis6.0多线程的实现机制? 流程简述如下: 1、主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列2、主线程处理完读事件之后,通过 RR(Round Robin) 将这些连接分配给这些 IO 线程3、主线程阻塞等待 IO 线程读取 socket 完毕4、主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行5、主线程阻塞等待 IO 线程将数据回写 socket 完毕6、解除绑定,清空等待队列 (图片来源:https://ruby-china.org/topics/38957) 该设计有如下特点:1、IO 线程要么同时在读 socket,要么同时在写,不会同时读或写2、IO 线程只负责读写 socket 解析命令,不负责命令处理 8.开启多线程后,是否会存在线程并发安全问题? 从上面的实现机制可以看出,Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。 9.Linux环境上如何安装Redis6.0.1(6.0的正式版是6.0.1)? 这个和安装其他版本的redis没有任何区别,整个流程跑下来也没有任何的坑,所以这里就不做描述了。唯一要注意的就是配置多线程数一定要小于cpu的核心数,查看核心数量命令: [root@centos7.5 ~]# lscpuArchitecture: x86_64CPU op-mode(s): 32-bit, 64-bitByte Order: Little EndianCPU(s): 4On-line CPU(s) list: 0-310.Redis6.0的多线程和Memcached多线程模型进行对比 前些年memcached 是各大互联网公司常用的缓存方案,因此redis 和 memcached 的区别基本成了面试官缓存方面必问的面试题,最近几年memcached用的少了,基本都是 redis。不过随着Redis6.0加入了多线程特性,类似的问题可能还会出现,接下来我们只针对多线程模型来简单比较一下。 如上图所示:Memcached 服务器采用 master-woker 模式进行工作,服务端采用 socket 与客户端通讯。主线程、工作线程 采用 pipe管道进行通讯。主线程采用 libevent 监听 listen、accept 的读事件,事件响应后将连接信息的数据结构封装起来,根据算法选择合适的工作线程,将连接任务携带连接信息分发出去,相应的线程利用连接描述符建立与客户端的socket连接 并进行后续的存取数据操作。 Redis6.0与Memcached多线程模型对比:相同点:都采用了 master线程-worker 线程的模型不同点:Memcached 执行主逻辑也是在 worker 线程里,模型更加简单,实现了真正的线程隔离,符合我们对线程隔离的常规理解。而 Redis 把处理逻辑交还给 master 线程,虽然一定程度上增加了模型复杂度,但也解决了线程并发安全等问题。 11.Redis作者是如何点评 “多线程”这个新特性的? 关于多线程这个特性,在6.0 RC1时,Antirez曾做过说明: Redis支持多线程有2种可行的方式:第一种就是像“memcached”那样,一个Redis实例开启多个线程,从而提升GET/SET等简单命令中每秒可以执行的操作。这涉及到I/O、命令解析等多线程处理,因此,我们将其称之为“I/O threading”。另一种就是允许在不同的线程中执行较耗时较慢的命令,以确保其它客户端不被阻塞,我们将这种线程模型称为“Slow commands threading”。 经过深思熟虑,Redis不会采用“I/O threading”,redis在运行时主要受制于网络和内存,所以提升redis性能主要是通过在多个redis实例,特别是redis集群。接下来我们主要会考虑改进两个方面:1.Redis集群的多个实例通过编排能够合理地使用本地实例的磁盘,避免同时重写AOF。2.提供一个Redis集群代理,便于用户在没有较好的集群协议客户端时抽象出一个集群。 补充说明一下,Redis和memcached一样是一个内存系统,但不同于Memcached。多线程是复杂的,必须考虑使用简单的数据模型,执行LPUSH的线程需要服务其他执行LPOP的线程。 我真正期望的实际是“slow operations threading”,在redis6或redis7中,将提供“key-level locking”,使得线程可以完全获得对键的控制以处理缓慢的操作。 详见:http://antirez.com/news/126 12.Redis线程中经常提到IO多路复用,如何理解? 这是IO模型的一种,即经典的Reactor设计模式,有时也称为异步阻塞IO。 多路指的是多个socket连接,复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。 13.你知道Redis的彩蛋LOLWUT吗? 这个其实从Redis5.0就开始有了,但是原谅我刚刚知道。作者是这么描述这个功能的《LOLWUT: a piece of art inside a database command》,“数据库命令中的一件艺术品”。你可以把它称之为情怀,也可以称之为彩蛋,具体是什么,我就不透露了。和我一样不清楚是什么的小伙伴可以参见:http://antirez.com/news/123,每次运行都会随机生成的噢。 | 参考、致谢 Rdis作者Antirez的博客:http://antirez.comhttps://www.zhihu.com/question/26943938/answer/68773398https://zhuanlan.zhihu.com/p/76788470http://www.web-lovers.com/redis-source-6-rc-mult-thread.htmlhttps://ruby-china.org/topics/38957https://redis.io/topics/faq#redis-is-single-threaded-how-can-i-exploit-multiple-cpu--coreshttps://juejin.im/post/5e9ae485f265da47b04d95d2https://www.cnblogs.com/gattaca/p/6929361.html本文图片来自互联网,版权归原作者所有 公众号:码大叔 原文地址https://www.cnblogs.com/madashu/p/12832766.html
C# 基础知识系列- 14 IO篇 文件的操作 @ 目录 前言 文件、目录和路径1.1 File和FileInfo 1.1.1 File工具类1.1.2 FileInfo 对象类1.2 Directory和DirectoryInfo1.2.1 Directory1.2.2 DirectoryInfo 前言本章节是IO篇的第二集,我们在上一篇中介绍了C#中IO的基本概念和一些基本方法,接下来我们介绍一下操作文件的方法。在编程的世界中,操作文件是一个很重要的技能。 文件、目录和路径在开始操作之前,先大概讲解一下基本概念。在计算机系统中,文件是以硬盘为载体存储在计算机上的信息集合。文件通常会有一个后缀名,表示文件格式(当然,通常的另一个含义就是可能没有)。我们最常见到的图片文件,后缀有jpg/png/gif这些常见的;文本文件为txt等。 目录,不严谨的来讲可以用文件夹代替。不过严格来说,目录指的是文件所在的文件夹以及文件夹的位置这些信息的集合。 路径是指文件或文件夹所在的位置的字符串表示,有相对路径和绝对路径,有物理路径和网络路径等一系列这些划分。 相对路径指的是,相对程序所在目录目标文件所在的目录路径绝对路径指的是从系统或者网站的目录起点开始文件所在的位置,也就是说无论程序在哪都能通过绝对路径访问到对应文件物理路径是指文件在磁盘的路径,划分依据与之前的两种并不一致,所以不是并列关系网络路径是指网络或文件是在网络服务上部署的,通过URI访问的路径信息好了,基本概念介绍到这里,让我们来看看如何实现C#操作文件吧。 1.1 File和FileInfoC# 提供了两个访问文件的入口,File和FileInfo这两个类。有人可能要迷惑了,为啥要提供两个呢,这两个又有啥子不一样的呢?别急,让我们来一起看一看吧。 我们先来观察一下两个类的声明方式有什么不一样的: public static class File;public sealed class FileInfo : System.IO.FileSystemInfo;我们忽略突然冒出来的FileSystemInfo,只需要明白它是FileInfo的基类即可。 通过两个类的声明方式,可以看出File是一个工具类,而FileInfo则是文件对象。所以,File更多的用在快速操作文件并不需要长时间多次使用同一个文件的场景,而FileInfo则适合同一个文件的多次使用。 1.1.1 File工具类我们先来看下File支持哪些操作: a.文件读取 public static byte[] ReadAllBytes (string path);public static string[] ReadAllLines (string path);public static string[] ReadAllLines (string path, System.Text.Encoding encoding);public static string ReadAllText (string path);public static string ReadAllText (string path, System.Text.Encoding encoding);public static System.Collections.Generic.IEnumerable ReadLines (string path);先从名称上分析方法应该是什么,应该具有哪些功能? ReadAllBytes以二进制的形式一次性把文件全部读出来ReadAllLines打开文本文件,将文件内容一行一行的全部读出来并返回ReadAllText打开文件,并将文件所有内容一次性读出来ReadLines 这是一个新的方法,根据返回值和方法名称,可以判断它应该与ReadAllLines有着类似的行为ReadLInes和ReadAllLines的区别: ReadAllLines返回的是字符串数组,所以该方法会一次性将文件内容全部读出ReadLines返回的是一个可枚举对象,根据之前在Linq系列和集合系列的知识,我们能判断出,这个方法不会立即返回数据所以我们很轻易的就能得出,ReadAllLines不会过久的持有文件对象,但是不适合操作大文件;ReadLines对于大文件的操作更擅长一些,但是可能会更久的持有文件 b.写入文件 public static void AppendAllLines (string path, System.Collections.Generic.IEnumerable contents);public static void AppendAllLines (string path, System.Collections.Generic.IEnumerable contents, System.Text.Encoding encoding);public static void AppendAllText (string path, string contents);public static void AppendAllText (string path, string contents, System.Text.Encoding encoding);public static void WriteAllBytes (string path, byte[] bytes);public static void WriteAllLines (string path, string[] contents, System.Text.Encoding encoding);public static void WriteAllText (string path, string contents);public static void WriteAllText (string path, string contents, System.Text.Encoding encoding);来,我们简单看一下这几个方法具体作用: AppendAllLines:追加行到文件末尾AppendAllText :将字符串内容追加到文件末尾WriteBytes:将字节数组写到文件里,如果文件有内容就覆盖原有内容WriteAllLines:按行写入文件中,如果文件有内容则覆盖原有内容WriteAllText:将内容写入文件,如果文件有内容则覆盖原有内容在使用File写入文件的时候,如果文件不存在则会自动创建文件。 复制文件 File类提供了简单易用的复制文件功能,只需要指定源文件和新文件即可: public static void Copy (string sourceFileName, string destFileName);public static void Copy (string sourceFileName, string destFileName, bool overwrite);这两个方法对的作用就是将 sourceFileName复制为destFileName。第一个方法不允许复制为已存在的文件,也就是说如果destFileName已存在则报错。第二个方法则通过overwrite指定是否覆盖。 d.移动文件 与复制文件相同的使用方式,File提供了移动文件的方法: public static void Move (string sourceFileName, string destFileName);public static void Move (string sourceFileName, string destFileName, bool overwrite);注意事项与复制文件一致。 e.删除文件 public static void Delete (string path);1.1.2 FileInfo 对象类FileInfo提供了文件的创建、复制、删除、移动和打开等属性和实例方法。我们先来看看,如果创建一个FileInfo: public FileInfo (string fileName);通过指定文件路径,来换取一个FileInfo对象,如果fileName指定的是目录则会提示错误。 好,现在我们已经可以获取一个FileInfo对象实例了,那么一起来看看FileInfo支持哪些内容吧: 先来看看文件的基本属性 public override bool Exists { get; }文件是否存在,等效于File.Existss(string path)。 public string DirectoryName { get; }获取文件所在目录的完整路径(绝对路径)。 public System.IO.DirectoryInfo Directory { get; }获取文件所在目录的目录类型实例。 public long Length { get; }获取文件的大小,单位是字节。 public override string Name { get; }获取文件名,包括文件的扩展名。 文件的操作 对于FileInfo实例来说,对于文件的操作大多都是基于流来完成的(这部分请留意下一篇内容),这里先看一下它的实例方法: public System.IO.StreamWriter AppendText ();//创建一个流适配器,在适配器里追加文本到文件中public System.IO.FileInfo CopyTo (string destFileName);//将现有文件复制到新文件,并返回新文件的实例,不支持覆盖public System.IO.FileInfo CopyTo (string destFileName, bool overwrite);//根据orverwrite确定是否覆盖public System.IO.FileStream Create ();//创建当前对象代表的文件,并返回一个文件流public System.IO.StreamWriter CreateText ();//与AppendText类似,但会覆盖文件原有内容public override void Delete ();//删除文件public void MoveTo (string destFileName);// 将文件移动到新文件,不支持覆盖已存在文件public void MoveTo (string destFileName, bool overwrite);// 根据overwrite确定是否覆盖public System.IO.FileStream Open (System.IO.FileMode mode);// 根据模式打开文件public System.IO.FileStream Open (System.IO.FileMode mode, System.IO.FileAccess access);//指定权限和模式,打开文件public System.IO.FileStream OpenRead ();//打开一个只能读取的文件流public System.IO.StreamReader OpenText ();//打开一个读流适配器public System.IO.FileStream OpenWrite ();// 打开一个只能写的流最新版C#的API,取消了通过FileInfo获取文件的格式名的属性以及其他的很多属性,只保留了文中提到的几个属性。 1.2 Directory和DirectoryInfo与之前的类似,Directory也是个工具类,DirectoryInfo则代表目录实例。 1.2.1 Directory先来个简单的: 创建目录: public static System.IO.DirectoryInfo CreateDirectory (string path);如果目录已存在,则跳过创建,直接返回指定路径的DirectoryInfo实例 b.是否存在: public static bool Exists (string path);返回是否存在这个目录。 c.返回目录下的所有文件 public static string[] GetFiles (string path); 返回目录下的所有子目录: public static string[] GetDirectories (string path);public static string[] GetDirectories (string path, string searchPattern);public static string[] GetDirectories (string path, string searchPattern, System.IO.EnumerationOptions enumerationOptions);public static string[] GetDirectories (string path, string searchPattern, System.IO.SearchOption searchOption);除了上文提到的 GetDirectories 方法可以直接返回目录下所有子目录以外,还有一组方法也可以枚举出当前目录下的子目录: public static System.Collections.Generic.IEnumerable EnumerateDirectories (string path);枚举 path 目录下的所有子目录。 public static System.Collections.Generic.IEnumerable EnumerateDirectories (string path, string searchPattern);searchPattern,搜索名称字符串,可以包含有效文本路径和通配符(* 和 ?)的组合,但不支持正则表达式。 public static System.Collections.Generic.IEnumerable EnumerateDirectories (string path, string searchPattern, System.IO.EnumerationOptions enumerationOptions);public static System.Collections.Generic.IEnumerable EnumerateDirectories (string path, string searchPattern, System.IO.SearchOption searchOption);这两个方法放在一起讲,这两个是对上一个方法的增强和补充。其中 EnumerationOptions 是类,可以配置查询的条件;SearchOption 是个枚举,选择只查询当前目录的子目录名称还是继续深入查询子孙目录。 e.查看目录下的所有文件-补充 与子目录查询相同,Directory也支持这么几组查询方法: public static string[] GetFiles (string path);public static string[] GetFiles (string path, string searchPattern);public static string[] GetFiles (string path, string searchPattern, System.IO.EnumerationOptions enumerationOptions);public static string[] GetFiles (string path, string searchPattern, System.IO.SearchOption searchOption);从参数上看,可以看出来这是返回子目录下的文件列表。其中使用 searchPattern查询名称,enumerationOptions 作为查询条件,searchOption 作为查询的深度。 同样,查询文件也可以使用枚举方法: public static System.Collections.Generic.IEnumerable EnumerateFiles (string path);public static System.Collections.Generic.IEnumerable EnumerateFiles (string path, string searchPattern);public static System.Collections.Generic.IEnumerable EnumerateFiles (string path, string searchPattern, System.IO.EnumerationOptions enumerationOptions);public static System.Collections.Generic.IEnumerable EnumerateFiles (string path, string searchPattern, System.IO.SearchOption searchOption);f.获取当前目录 public static string GetCurrentDirectory ();在程序中调用这个方法可以获取程序执行时的目录,如果是在调试阶段,目录是指程序的主方法所在目录;如果在发布之后,也就是运行阶段,该目录指程序所在目录。 g.获取上级目录 public static System.IO.DirectoryInfo GetParent (string path);获取传入目录的上级目录信息。 h.目录移动 public static void Move (string sourceDirName, string destDirName);sourceDirName 移动到 destDirName,其中destDirName所代表的目录不能纯在。这个方法有个很有意思的特点,它也支持移动文件。也就是说,如果sourceDirNanme指向的是一个文件,那么destDirName也必须是一个文件类型的路径字符串。 i.删除目录 public static void Delete (string path);//删除 path所代表的目录,如果目录非空则提示无法删除public static void Delete (string path, bool recursive);// recursive指示是否同时删除子目录和文件以上是Directory类的一些常用方法,当然还有更多的内容留待小伙伴一起发掘。传送门==>https://docs.microsoft.com/zh-cn/dotnet/api/system.io.directory?view=netcore-3.1 1.2.2 DirectoryInfo之前的篇幅我们介绍了Directory的工具类所支持的方法,接下来我们看一下 DirectoryInfo有哪些属性和方法吧。 public DirectoryInfo (string path);初始化的方式很简单,直接传递一个目录的路径字符串,就可以获取一个目录信息类了。 接下来看看,DirectoryInfo支持的属性: public override bool Exists { get; }// 目录是否存在public override string Name { get; }// 目录名称,不是路径public System.IO.DirectoryInfo Parent { get; }//如果有上级目录,则返回上级目录,如果没有则返回 nullpublic System.IO.DirectoryInfo Root { get; }//获取目录的根目录我们路过了DirectoryInfo的属性,看到了它一部分特点,那么我们该怎么使用呢? public void Create ();创建目录信息所代表的目录,如果目录已存在,则不会有任何变化 。如果这个目录的父目录也不存在,则自动创建父目录 public System.IO.DirectoryInfo CreateSubdirectory (string path);创建 pathi指定的子目录。 public override void Delete ();如果当前目录是空目录,调用可直接删除,如果非空则会提示错误。 public void Delete (bool recursive);根据参数 recursive指定是否删除当前目录的子目录。 public System.IO.DirectoryInfo[] GetDirectories ();public System.IO.DirectoryInfo[] GetDirectories (string searchPattern);public System.IO.DirectoryInfo[] GetDirectories (string searchPattern, System.IO.EnumerationOptions enumerationOptions);public System.IO.DirectoryInfo[] GetDirectories (string searchPattern, System.IO.SearchOption searchOption);获取子目录的数组,参数与 Directory 的同名方法一致。 public System.Collections.Generic.IEnumerable EnumerateDirectories ();public System.Collections.Generic.IEnumerable EnumerateDirectories (string searchPattern);public System.Collections.Generic.IEnumerable EnumerateDirectories (string searchPattern, System.IO.EnumerationOptions enumerationOptions);public System.Collections.Generic.IEnumerable EnumerateDirectories (string searchPattern, System.IO.SearchOption searchOption);返回一个子目录信息的可枚举集合。 public System.IO.FileInfo[] GetFiles ();public System.IO.FileInfo[] GetFiles (string searchPattern);public System.IO.FileInfo[] GetFiles (string searchPattern, System.IO.EnumerationOptions enumerationOptions);public System.IO.FileInfo[] GetFiles (string searchPattern, System.IO.SearchOption searchOption);嗯,依旧类似的写法,获取文件信息的数组 public System.Collections.Generic.IEnumerable EnumerateFiles ();public System.Collections.Generic.IEnumerable EnumerateFiles (string searchPattern);public System.Collections.Generic.IEnumerable EnumerateFiles (string searchPattern, System.IO.EnumerationOptions enumerationOptions);public System.Collections.Generic.IEnumerable EnumerateFiles (string searchPattern, System.IO.SearchOption searchOption);返回文件的可枚举集合。 public void MoveTo (string destDirName);把当前目录移动到对应的目录。 依旧未完待续,下一篇将为大家介绍一下 Path类和FileInfo与DirectoryInfo的父类 FileSystemInfo 这两个类的API,然后演示一下如何使用流来读写文件。在文件和目录这块内容里,我故意忽略了权限的介绍,这部分我将会放在进阶篇中介绍。 API的介绍总是这么枯燥乏味,不过请期待一下,在IO篇完成后,我会演示一下如何做一个简单的文件查找工具。 简单介绍一下这个工具的内容:它会遍历系统里所有文件的路径信息,然后记录到一个缓存文件中,用户输入一个要查询的文件名时,我们可以通过读取缓存文件确认文件所在目录。 原文地址https://www.cnblogs.com/c7jie/p/12812830.html
Springboot以Tomcat为容器实现http重定向到https的两种方式 1 简介本文将介绍在Springboot中如何通过代码实现Http到Https的重定向,本文仅讲解Tomcat作为容器的情况,其它容器将在以后一一道来。 建议阅读之前的相关文章: (1) Springboot整合https原来这么简单 (2)HTTPS之密钥知识与密钥工具Keytool和Keystore-Explorer 2 相关概念2.1 什么叫重定向所谓重定向,就是本来你想浏览地址A的,但是到达服务端后,服务端认为地址A的界面不在了或者你没权限访问等原因,不想你访问地址A;就告诉你另一个地址B,然后你再去访问地址B。 对于重定向一般有两个返回码: 301:永久性重定向;302:暂时性重定向。通过Chrome查看网络详情,记录了几个网站的重定向情况: 网站 域名 重定向代码 重定向后的网址南瓜慢说 www.pkslow.com 301 https://www.pkslow.comGoogle www.google.com 307 https://www.google.comApple www.apple.com 307 https://www.apple.com支付宝 www.alipay.com 301 https://www.alipay.comQQ www.qq.com 302 https://www.qq.com百度 www.baidu.com 307 https://www.baidu.com注:307也是重定向的一种,是新的状态码。 2.2 为什么要重定向结合我上面特意列的表格,是不是大概想到了为何要做这种重定向?不难发现上面的重定向都在做一件事,就是把http重定向为https。原因如下: (1)http是不安全的,应该使用安全的https网址; (2)但不能要求用户每次输入网站都输入https:// 吧,这也太麻烦了,所以大家都是习惯于只输入域名,甚至连www. 都不愿意输入。因此,用户的输入其实都是访问http的网页,就需要重定向到https以达到安全访问的要求。 2.3 如何做到重定向首先,服务器必须要同时支持http和https,不然也就没有重定向一说了。因为https是必须提供支持的,那为何还要提供http的服务呢?直接访问https不就行了,不是多此一举吗?原因之前已经讲过了,大家是习惯于只输入简单域名访问的,这时到达的就是http,如果不提供http的支持,用户还以为你的网站已经挂了呢。 两种协议都提供支持,所以是需要打开两个Socket端口的,一般http为80,而https为443。然后就需要把所有访问http的请求,重定向到https即可。不同的服务器有不同的实现,现在介绍Springboot+Tomcat的实现。 3 Springboot Tomcat实现重定向Springboot以Tomcat作为Servlet容器时,有两种方式可以实现重定向,一种是没有使用Spring Security的,另一种是使用了Spring Security的。代码结构如下: 主类的代码如下: package com.pkslow.ssl; import com.pkslow.ssl.config.containerfactory.HttpToHttpsContainerFactoryConfig;import com.pkslow.ssl.config.security.EnableHttpWithHttpsConfig;import com.pkslow.ssl.config.security.HttpToHttpsWebSecurityConfig;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Import; @SpringBootApplication@Import({EnableHttpWithHttpsConfig.class, HttpToHttpsWebSecurityConfig.class})//@Import(HttpToHttpsContainerFactoryConfig.class)@ComponentScan(basePackages = "com.pkslow.ssl.controller")public class SpringbootSslApplication { public static void main(String[] args) { SpringApplication.run(SpringbootSslApplication.class, args); } }@ComponentScan(basePackages = "com.pkslow.ssl.controller"):没有把config包扫描进来,是因为想通过@Import来控制使用哪种方式来进行重定向。当然还可以使用其它方式来控制,如@ConditionalOnProperty,这里就不展开讲了。 当没有使用Spring Security时,使用@Import(HttpToHttpsContainerFactoryConfig.class); 当使用Spring Security时,使用@Import({EnableHttpWithHttpsConfig.class, HttpToHttpsWebSecurityConfig.class})。 配置文件application.properties内容如下: server.port=443http.port=80 server.ssl.enabled=trueserver.ssl.key-store-type=jksserver.ssl.key-store=classpath:localhost.jksserver.ssl.key-store-password=changeitserver.ssl.key-alias=localhost需要指定两个端口,server.port为https端口;http.port为http端口。注意在没有https的情况下,server.port指的是http端口。 3.1 配置Container Factory实现重定向配置的类为HttpToHttpsContainerFactoryConfig,代码如下: package com.pkslow.ssl.config.containerfactory; import org.apache.catalina.Context;import org.apache.tomcat.util.descriptor.web.SecurityCollection;import org.apache.tomcat.util.descriptor.web.SecurityConstraint;import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;import org.springframework.context.annotation.Bean;import org.apache.catalina.connector.Connector;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Configuration; @Configurationpublic class HttpToHttpsContainerFactoryConfig { @Value("${server.port}") private int httpsPort; @Value("${http.port}") private int httpPort; @Bean public TomcatServletWebServerFactory servletContainer() { TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() { @Override protected void postProcessContext(Context context) { SecurityConstraint securityConstraint = new SecurityConstraint(); securityConstraint.setUserConstraint("CONFIDENTIAL"); SecurityCollection collection = new SecurityCollection(); collection.addPattern("/*"); securityConstraint.addCollection(collection); context.addConstraint(securityConstraint); } }; tomcat.addAdditionalTomcatConnectors(createHttpConnector()); return tomcat; } private Connector createHttpConnector() { Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); connector.setScheme("http"); connector.setSecure(false); connector.setPort(httpPort); connector.setRedirectPort(httpsPort); return connector; } }createHttpConnector():这个方法主要是实现了在有https前提下,打开http的功能,并配置重定向的https的端口。 3.2 配置Spring security实现重定向有两个配置类,一个为打开http服务,一个为实现重定向。 EnableHttpWithHttpsConfig主要作用是在已经有https的前提下,还要打开http服务。 package com.pkslow.ssl.config.security; import org.apache.catalina.connector.Connector;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;import org.springframework.boot.web.server.WebServerFactoryCustomizer;import org.springframework.context.annotation.Configuration;import org.springframework.stereotype.Component; @Configurationpublic class EnableHttpWithHttpsConfig { @Value("${http.port}") private int httpPort; @Component public class CustomContainer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> { @Override public void customize(TomcatServletWebServerFactory factory) { Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); connector.setPort(httpPort); connector.setScheme("http"); connector.setSecure(false); factory.addAdditionalTomcatConnectors(connector); } } }HttpToHttpsWebSecurityConfig主要是针对Spring Security的配置,众所周知,Spring Security是功能十分强大,但又很复杂的。代码中已经写了关键的注释: package com.pkslow.ssl.config.security; import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configurationpublic class HttpToHttpsWebSecurityConfig extends WebSecurityConfigurerAdapter { @Value("${server.port}") private int httpsPort; @Value("${http.port}") private int httpPort; @Override protected void configure(HttpSecurity http) throws Exception { //redirect to https - 用spring security实现 http.portMapper().http(httpPort).mapsTo(httpsPort); http.requiresChannel( channel -> channel.anyRequest().requiresSecure() ); //访问路径/hello不用登陆获得权限 http.authorizeRequests() .antMatchers("/hello").permitAll() .anyRequest().authenticated().and(); } @Override public void configure(WebSecurity web) throws Exception { //过滤了actuator后,不会重定向,也不用权限校验,这个功能非常有用 web.ignoring() .antMatchers("/actuator") .antMatchers("/actuator/**"); } }4 总结最后实现了重定向,结果展示: 本文详细代码可在南瓜慢说公众号回复获取。 参考链接: Spring Security: https://docs.spring.io/spring-security/site/docs/5.3.2.BUILD-SNAPSHOT/reference/html5/#servlet-http-redirect Springboot 1.4重定向:https://jonaspfeifer.de/redirect-http-https-spring-boot/ 原文地址https://www.cnblogs.com/larrydpk/p/12806699.html
python实现线性回归之简单回归 代码来源:https://github.com/eriklindernoren/ML-From-Scratch 首先定义一个基本的回归类,作为各种回归方法的基类: class Regression(object): """ Base regression model. Models the relationship between a scalar dependent variable y and the independent variables X. Parameters: ----------- n_iterations: float The number of training iterations the algorithm will tune the weights for. learning_rate: float The step length that will be used when updating the weights. """ def __init__(self, n_iterations, learning_rate): self.n_iterations = n_iterations self.learning_rate = learning_rate def initialize_wights(self, n_features): """ Initialize weights randomly [-1/N, 1/N] """ limit = 1 / math.sqrt(n_features) self.w = np.random.uniform(-limit, limit, (n_features, )) def fit(self, X, y): # Insert constant ones for bias weights X = np.insert(X, 0, 1, axis=1) self.training_errors = [] self.initialize_weights(n_features=X.shape[1]) # Do gradient descent for n_iterations for i in range(self.n_iterations): y_pred = X.dot(self.w) # Calculate l2 loss mse = np.mean(0.5 * (y - y_pred)**2 + self.regularization(self.w)) self.training_errors.append(mse) # Gradient of l2 loss w.r.t w grad_w = -(y - y_pred).dot(X) + self.regularization.grad(self.w) # Update the weights self.w -= self.learning_rate * grad_w def predict(self, X): # Insert constant ones for bias weights X = np.insert(X, 0, 1, axis=1) y_pred = X.dot(self.w) return y_pred 说明:初始化时传入两个参数,一个是迭代次数,另一个是学习率。initialize_weights()用于初始化权重。fit()用于训练。需要注意的是,对于原始的输入X,需要将其最前面添加一项为偏置项。predict()用于输出预测值。 接下来是简单线性回归,继承上面的基类: class LinearRegression(Regression): """Linear model. Parameters: ----------- n_iterations: float The number of training iterations the algorithm will tune the weights for. learning_rate: float The step length that will be used when updating the weights. gradient_descent: boolean True or false depending if gradient descent should be used when training. If false then we use batch optimization by least squares. """ def __init__(self, n_iterations=100, learning_rate=0.001, gradient_descent=True): self.gradient_descent = gradient_descent # No regularization self.regularization = lambda x: 0 self.regularization.grad = lambda x: 0 super(LinearRegression, self).__init__(n_iterations=n_iterations, learning_rate=learning_rate) def fit(self, X, y): # If not gradient descent => Least squares approximation of w if not self.gradient_descent: # Insert constant ones for bias weights X = np.insert(X, 0, 1, axis=1) # Calculate weights by least squares (using Moore-Penrose pseudoinverse) U, S, V = np.linalg.svd(X.T.dot(X)) S = np.diag(S) X_sq_reg_inv = V.dot(np.linalg.pinv(S)).dot(U.T) self.w = X_sq_reg_inv.dot(X.T).dot(y) else: super(LinearRegression, self).fit(X, y) 这里使用两种方式进行计算。如果规定gradient_descent=True,那么使用随机梯度下降算法进行训练,否则使用标准方程法进行训练。 最后是使用: import numpy as npimport pandas as pdimport matplotlib.pyplot as pltfrom sklearn.datasets import make_regressionimport syssys.path.append("/content/drive/My Drive/learn/ML-From-Scratch/") from mlfromscratch.utils import train_test_split, polynomial_featuresfrom mlfromscratch.utils import mean_squared_error, Plotfrom mlfromscratch.supervised_learning import LinearRegression def main(): X, y = make_regression(n_samples=100, n_features=1, noise=20) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4) n_samples, n_features = np.shape(X) model = LinearRegression(n_iterations=100) model.fit(X_train, y_train) # Training error plot n = len(model.training_errors) training, = plt.plot(range(n), model.training_errors, label="Training Error") plt.legend(handles=[training]) plt.title("Error Plot") plt.ylabel('Mean Squared Error') plt.xlabel('Iterations') plt.savefig("test1.png") plt.show() y_pred = model.predict(X_test) mse = mean_squared_error(y_test, y_pred) print ("Mean squared error: %s" % (mse)) y_pred_line = model.predict(X) # Color map cmap = plt.get_cmap('viridis') # Plot the results m1 = plt.scatter(366 * X_train, y_train, color=cmap(0.9), s=10) m2 = plt.scatter(366 * X_test, y_test, color=cmap(0.5), s=10) plt.plot(366 * X, y_pred_line, color='black', linewidth=2, label="Prediction") plt.suptitle("Linear Regression") plt.title("MSE: %.2f" % mse, fontsize=10) plt.xlabel('Day') plt.ylabel('Temperature in Celcius') plt.legend((m1, m2), ("Training data", "Test data"), loc='lower right') plt.savefig("test2.png") plt.show() if name == "__main__": main() 利用sklearn库生成线性回归数据,然后将其拆分为训练集和测试集。 utils下的mean_squared_error(): def mean_squared_error(y_true, y_pred): """ Returns the mean squared error between y_true and y_pred """ mse = np.mean(np.power(y_true - y_pred, 2)) return mse 结果: Mean squared error: 532.3321383700828 原文地址https://www.cnblogs.com/xiximayou/p/12802118.html
C语言指定初始化器解析及其应用 指定初始化器的概念C90 标准要求初始化程序中的元素以固定的顺序出现,与要初始化的数组或结构体中的元素顺序相同。但是在新标准 C99 中,增加了一个新的特性:指定初始化器。利用该特性可以初始化指定的数组或者结构体元素。 数组的指定初始化器#一维数组的指定初始化器#利用指定初始化器的特性,我们可以这样定义并初始化一个数组: Copyint a[6] = {[4] = 10,[2] = 25};上述的初始化就等同于如下方式: Copyint a[6] = {0,0,25,0,10,0};可以看到通过这种方式能够不按照顺序,且指定具体的元素进行初始化。除了上述这样的用法,我们也能够初始化数组内一段范围内的用元素,比如这样: Copyint a[5] = {[4] = 10,[0 ... 3] = 23};上面这段程序的初始化也就等同于如下初始化: Copyint a[5] = {23,23,23,23,10};那如果数组初始化里有指定的元素初始化又有未指定的元素又是如何分析呢?比如这样: Copyint a[5] = {11,[3] = 44,55,[1] = 22,33};那它等同于下面的代码: Copyint a[5] = {11,22,33,44,55};如果定义数组时没有指定数组的大小,那么数组实际的大小又是多少呢?比如这样: Copyint main(void){ int number[] = {[20] = 1,[10] = 8,9}; int n = sizeof(number)/sizeof(number[0]); printf("The Value of n is:%d\n",n); }输出结果是这样的: CopyThe Value of n is:21也就是说,如果未给出数组的大小,则最大的初始化位置确定数组的大小 二维数组的指定初始化器#二维数组同样可以采用指定初始化器的方法,下面是一个二维数组的初始化: Copyint array2 = { [0] = {[0] = 11}, [1] = {[1] = 22}, };这样的初始化也就等同于下述代码: Copyint array12 = { {11,00}, {00,22} };通过上述代码,我们也可以知道,二维数组的指定初始化器的方法中,第一个[]里的数字表示的是初始化的二维数组的行数,而在{}内的则是对当前行的元素进行初始化,实际也就是说{}内的初始化方法也就和一维数组的一样了,一维数组可行的方法,二维数组也是可行的。 应用#在讲述了数组指定初始化器的基本概念之后,我们来看一个具体的例子,下面这个例子是基于状态机的编程方法实现的 ATM 机器,首先 ATM 具有如下几种状态; 我们就可以使用状态机的思路来编写这个程序,首先使用枚举的方式来定义各个状态和相应的操作: Copytypedef enum{ Idle_State, Card_Inserted_State, Pin_Entered_State, Option_Selected_State, Amount_Entered_State, last_State }eSysyemState; typedef enum{ Card_Insert_Event, Pin_Enter_Event, Option_Selection_Event, Amount_Enter_Event, Amount_Dispatch_Event, last_Event }eSystemEvent;然后是对应操作的具体实现: CopyeSysyemState AmountDispatchHandler(void){ return Idle_State; } eSysyemState EnterAmountHandler(void){ return Amount_Entered_State; } eSysyemState OptionSelectionHandler(void){ return Option_Selected_State; } eSysyemState InsertCardHandle(void){ return Card_Inserted_State; } eSysyemState EnterPinHandler(void){ return Pin_Entered_State; }为了使得状态机的实现看起来不是那么的冗长,我们这里采用查表的方式,首先重定义一个函数指针二维数组类型: Copytypedef eSysyemState (* const afEventHandlerlast_State)(void);简单说一个这是一个二维数组,二维数组里面存放的是函数指针,这个函数指针指向的是返回值为 eSysyemState,形参为 void 的函数。在重定义了这个类型之后,我们就可以用其定义新的变量了,在这之前,补充一点数组相关的内容,比如有如下代码: Copytypedef int array[3];array data;那么上述代码也就等同于如下代码: Copyint data[3];有了上述代码之后,我们就可以实现我们的查找表了,具体代码如下: Copy static afEventHandler StateMachine = { [Idle_State] = {[Card_Insert_Event] = InsertCardHandle}, [Card_Inserted_State] = {[Pin_Enter_Event] = EnterPinHandler }, [Pin_Entered_State] = {[Option_Selection_Event] = OptionSelectionHandler}, [Option_Selected_State] = {[Amount_Entered_Event] = EnterAmountHandler}, [Amount_Entered_State] = {[Amount_Dispatch_Event] = AmountDispatchHandler}, }; 现在再来看到这个初始化的方法也就比较清楚了,这实际上也就是一个二维数组使用指定初始化器解析的方法,最后,也就是我们的状态机运行代码: Copy include int main(void){ eSysyemState eNextState = Idle_State; eSystemEvent eNewEvent; while(1) { eNewEvent = ReadEvent(); /*省略相关判断*/ eNextState = (*StateMachine[eNextState][eNewEvent])(); } return 0; }结构体的指定初始化器#定义了如下结构体: Copystruct point{ int x,y; }那么对于结构体变量的初始化可以采用以下的方式: Copystruct point p = { .y = 2, .x = 3 };上述代码也就等价于如下代码: Copystruct point p = {3,2};那这样的初始化有什么作用呢?下面是 linux 内核的一段代码: Copyconst struct file_operations eeprom_fops ={ .llseek = eeprom_lseek, .read = eeprom_read, .write = eeprom_write, .open = eeprom_open, .release = eeprom_close};上述就是通过指定初始化器的方法来进行初始化的,其中 file_operations 这个结构体中的成员有很多,上述初始化的成员只是其中一部分, Copystruct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); /*还有很多,省略*/ } 采用这种指定初始化器的方法,使用灵活,而且代码易于维护。因为如果按照固定顺序赋值,当我们的 file_operations 结构体类型发生改变时,比如添加成员、减少成员、调整成员顺序,那么使用该结构体类型定义变量的大量 C 文件都需要重新调整初始化顺序,那将导致程序大幅度地更改。 结构体数组的指定初始化器#在叙述了上面关于结构体和数组的指定初始化器之后,我们也可以以这种方式来来初始化结构体数组,比如这样: Copy include int main(void){ struct point {int x,y;}; struct point pts[5] = { [2].y = 5, [2].x = 6, [0].x = 2 }; int i; for(i = 0;i < 5;i++) printf("%d %d\n",pts[i].x,pts[i].y); }输出结果如下: Copy2 00 06 50 00 0总结以上便是指定初始化器所包含的大致内容,这也是自己之前的知识盲点,通过这次总结学习,也能够很好的掌握了,不积跬步,无以至千里~ 参考资料:[1] https://blog.51cto.com/zhaixue/2346825[2] https://www.geeksforgeeks.org/designated-initializers-c/[3] https://aticleworld.com/state-machine-using-c/ 作者: wenzid 出处:https://www.cnblogs.com/wenziw5/p/12792740.html
HTTPS之密钥知识与密钥工具Keytool和Keystore-Explorer 1 简介之前文章《Springboot整合https原来这么简单》讲解过一些基础的密码学知识和Springboot整合HTTPS。本文将更深入讲解密钥知识和密钥工具。 2 密钥知识-非对称加密这部分知识非常重要,理解了关键的密钥知识,才能更好地在工作中去使用。需要注意的是,讲的主要是非对称加密的知识,对称加密比较好理解,就不讲述了。 (1)对于非对称加密,密钥分为私钥(private key)和公钥(public key),它们是成对出现的,缺一不可,少一个都无法正常工作。 (2)如果用公钥加密,就必须使用对应的私钥解密,反之亦然。 (3)通常,私钥是自己才拥有,公钥是对外公开发布。因此,在服务器端一般我们使用的是私钥,而在客户端我们使用的则为公钥。所以我们是在服务端使用私钥对来自客户端的用公钥加密的数据进行解密,哈哈,这句话很绕。 (4)即使把公钥公开了,只要密钥长度合理(一般是1024或2048),目前的技术是无法通过公钥来计算出私钥的,所以我们认为它是安全的。 (5)那么我们通过什么来加密、什么来解密呢?从功能上来说,既可以用公钥加密、私钥解密,也可以用私钥加密,公钥解密。但实际上,通常采用的是公钥加密、私钥解密,因为只想密文只有我知道,不希望别人也能获得我的信息,所以要使用私钥来加密。试想想,如果把用私钥加密后的信息暴露出去,而大家都可以拥有公钥,也就意味着大家都可以解密,那我的加密还有什么意义呢? (6)对于签名功能,我不希望有人来冒充我来签名,所以是私钥签名,公钥验证,这样才能确保签名只属于一个人呀,如果用公钥来签名,那不是大家都可以是南瓜慢说呢? (7)非对称加密是运算效率比较低的,为了提高效率,可以采用这样的方案:使用非对称加密来交换密钥Key,然后用密钥Key来进行对称加密。不难理解,这个密钥是由客户端生成的,如果服务端来生成,那大家都知道了这个密钥,就失去了意义。 (8)密钥文件是用于存放私钥和公钥的文件,我们可以把私钥和公钥放一起,也可以分开放。而密钥文件有许多格式:jks、p12、pem、cert等,要根据不用的服务器和客户端选取。 (9)一个密钥文件是可以同时存放多个私钥和公钥的,如.jks文件可以同时存放localhost和www.pkslow.com的私钥。但要注意别名和CN名是不同的。 (10)生成cert文件的两种方式: 第一种:自己生成密钥和CSR(Certificate Signing Request,证书签名请求文件),把CSR给CA机构,机构会生成一个cert文件给你,然后要把该cert文件导入到自己的密钥文件里。 第二种:直接在CA生成所有,然后会给回private key和cert文件,配置到服务器端使用即可。 (11)如何更新将要过期的cert? Private Key没有过期时间,可以通过自己的private key生成新的CSR,然后给CA重新生成cert;或者private key也重新生成,参加上面一条。 (12)更新了cert,客户端要不要做什么? 如果是通过CA来验证的,是不需要的。如果是Self-Signed的,需要把cert给客户端然后导入到客户端的密钥文件里。 (13)作为文本格式的时候,密钥的格式如下: Private Key文件: -----BEGIN ENCRYPTED PRIVATE KEY-----XXX-----END ENCRYPTED PRIVATE KEY-----CSR文件: -----BEGIN CERTIFICATE REQUEST-----XXX-----END CERTIFICATE REQUEST-----Public key文件: -----BEGIN PUBLIC KEY-----XXX-----END PUBLIC KEY-----Cert文件: -----BEGIN CERTIFICATE-----XXX-----END CERTIFICATE-----3 密钥工具密钥工具有许多,常用的有openssl和keytool。 3.1 keytool常用命令keytool是JDK提供的密钥命令行工具,功能强大,语义清晰明了。常用的命令有: 生成一个密钥对 keytool -genkey -alias localhost -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore localhost.jks -dname CN=localhost,OU=Test,O=pkslow,L=Guangzhou,C=CN -validity 731 -storepass changeit -keypass changeit 生成CSR文件 keytool -certreq -alias localhost -file localhost.csr -keystore localhost.jks -storepass changeit 导出一个cert文件 keytool -export -alias xxx -file xxx.cer -keystore xxx.jks 导入一个cert文件 keytool -import -alais xxx -file xxx.cer -keystore xxx.jks 查看cert列表详情 keytool -list -v -keystore xxx.p12 -storepass changeit -storetype PKCS12keytool -list -v -keystore xxx.jks -storepass changeit -storetype JKS 转换JKS格式为P12 keytool -importkeystore -srckeystore xxx.jks -destkeystore xxx.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass changeit -deststorepassword changeit -srckeypass changeit-destkeypass changeit -srcalias xxx -destalias xxx -noprompt 更多命令请参考帮助文档或网上吧。 3.2 可视化工具Keystore Explorer在Linux终端等只能通过命令行方式来操作密钥,但命令和参数这么多,记住是不太可能的,那总不能每次都要上网查吧,万一连不上网了呢? 还好有优秀免费的可视化工具Keystore Explorer,官方网址为https://keystore-explorer.org/index.html ,可以在Windows或Mac开发使用,最后再把文件上传到服务器即可。界面如下: 非常简单易用又功能强大,大大减少了记忆劳动。不管是生成Key、导入导出、转换格式、生成CSR等,都很容易。所以,就不一一截图介绍了。 4 总结本文主要讲解了密钥知识和密钥工具。密钥知识比较重要,暂时就想到这些,以后再慢慢补充,最新文章可以到(www.pkslow.com) 访问。密钥工具有命令行工具和可视化工具,大家按自己需求选择。 原文地址https://www.cnblogs.com/larrydpk/p/12785977.html
机器学习5- 对数几率回归+Python实现 目录 对数几率回归1.1 求解 ω 和 b 对数几率回归进行垃圾邮件分类2.1 垃圾邮件分类 2.2 模型评估混淆举证精度交叉验证精度准确率召回率F1 度量ROC AUC 对数几率回归考虑二分类任务,其输出标记 y∈{0,1}y∈{0,1},记线性回归模型产生的预测值 z=wTx+bz=wTx+b 是实值,于是我们需要一个将实值 zz 转换为 0/10/1 的 g−(⋅)g−(⋅)。 最理想的单位阶跃函数(unit-step function) y=⎧⎩⎨0,0.5,1,z<0z=0z>0(1.1)(1.1)y={0,z<00.5,z=01,z>0并不是连续函数,因此不能作为 g−(⋅)g−(⋅) 。于是我们选用对数几率函数(logistics function)作为单位阶跃函数的替代函数(surrogate function): y=11+e−z(1.2)(1.2)y=11+e−z如下图所示: 对数几率函数是 Sigmoid 函数(即形似 S 的函数)的一种。 将对数几率函数作为 g−(⋅)g−(⋅) 得到 y=11+e−(wTx+b)(1.3)(1.3)y=11+e−(wTx+b)lny1−y=wTx+b(1.4)(1.4)lny1−y=wTx+b若将 yy 视为样本 xx 为正例的可能性,则 1−y1−y 是其为反例的可能性,两者的比值为 y1−y(1.5)(1.5)y1−y称为几率(odds),反映了 xx 作为正例的相对可能性。对几率取对数得到对数几率(log odds,或 logit): lny1−y(1.6)(1.6)lny1−y所以,式 (1.3) 实际上是用线性回归模型的预测结果取逼近真实标记的对数几率,因此其对应的模型又称为对数几率回归(logistic regression, 或 logit regression)。 这种分类学习方法直接对分类可能性进行建模,无需事先假设数据分布,避免了假设分布不准确带来的问题;它能得到近似概率预测,这对需要利用概率辅助决策的任务很有用;对率函数是任意阶可导的凸函数,有很好的数学性质,许多数值优化算法都可直接用于求解最优解。 1.1 求解 ω 和 b将式 (1.3) 中的 yy 视为类后验概率估计 p(y=1|x)p(y=1|x),则式 (1.4) 可重写为 lnp(y=1|x)p(y=0|x)=wTx+b(1.7)(1.7)lnp(y=1|x)p(y=0|x)=wTx+b有 p(y=1|x)=ewTx+b1+ewTx+b(1.8)(1.8)p(y=1|x)=ewTx+b1+ewTx+bp(y=0|x)=11+ewTx+b(1.9)(1.9)p(y=0|x)=11+ewTx+b通过极大似然法(maximum likelihood method)来估计 ww 和 bb 。 给定数据集 {(xi,yi)}mi=1{(xi,yi)}i=1m,对率回归模型最大化对数似然(log-likelihood): ℓ(w,b)=∑i=1mlnp(yi|xi;w,b)(1.10)(1.10)ℓ(w,b)=∑i=1mlnp(yi|xi;w,b)即令每个样本属于其真实标记的概率越大越好。 令 β=(w;b)β=(w;b),x^=(x;1)x^=(x;1),则 wTx+bwTx+b 可简写为 βTx^βTx^。再令 p1(x^;β)=p(y=1|x^;β)p1(x^;β)=p(y=1|x^;β),p0(x^;β)=p(y=0|x^;β)=1−p1(x^;β)p0(x^;β)=p(y=0|x^;β)=1−p1(x^;β) 。则式 (1.10) 可简写为: p(yi|xi;w,b)=yip1(x^;β)+(1−yi)p0(x^;β)(1.11)(1.11)p(yi|xi;w,b)=yip1(x^;β)+(1−yi)p0(x^;β)将式 (1.11) 带入 (1.10),并根据式 (1.8) 和 (1.9) 可知,最大化式 (1.10) 等价于最小化 ℓ(β)=∑i=1m(−yiβTx^i+ln(1+eβT+x^i))(1.12)(1.12)ℓ(β)=∑i=1m(−yiβTx^i+ln(1+eβT+x^i))式 (1.12) 是关于 ββ 的高阶可导凸函数,根据凸优化理论,经典的数值优化算法如梯度下降法(gradient descent method)、牛顿法(Newton method)等都可求得其最优解,于是得到: β∗=arg min βℓ(β)(1.13)(1.13)β∗=arg min βℓ(β)以牛顿法为例, 其第 t+1t+1 轮迭代解的更新公式为: βt+1=βt−(∂2ℓ(β)∂β ∂βT)−1∂ℓ(β)∂β(1.14)(1.14)βt+1=βt−(∂2ℓ(β)∂β ∂βT)−1∂ℓ(β)∂β其中关于 ββ 的一阶、二阶导数分别为: ∂ℓ(β)∂β=−∑i=1mx^i(yi−p1(x^i;β))(1.15)(1.15)∂ℓ(β)∂β=−∑i=1mx^i(yi−p1(x^i;β))∂2ℓ(β)∂β∂βT=∑i=1mx^ix^Tip1(x^i;β)(1−p1(x^i;β))(1.16)(1.16)∂2ℓ(β)∂β∂βT=∑i=1mx^ix^iTp1(x^i;β)(1−p1(x^i;β)) 对数几率回归进行垃圾邮件分类2.1 垃圾邮件分类 import pandas as pdimport numpy as npimport matplotlib.pyplot as pltfrom sklearn.linear_model.logistic import LogisticRegressionfrom sklearn.model_selection import train_test_split, cross_val_scorefrom sklearn.feature_extraction.text import TfidfVectorizerfrom matplotlib.font_manager import FontPropertiesdf = pd.read_csv("SMSSpamCollection", delimiter='t', header=None)df.head() print("spam 数量: ", df[df[0] == 'spam'][0].count())print("ham 数量: ", df[df[0] == 'ham'][0].count())spam 数量: 747ham 数量: 4825X_train_raw, X_test_raw, y_train, y_test = train_test_split(df[1], df[0]) 计算TF-IDF权重 vectorizer = TfidfVectorizer()X_train = vectorizer.fit_transform(X_train_raw)X_test = vectorizer.transform(X_test_raw) 建立模型 classifier = LogisticRegression()classifier.fit(X_train, y_train)y_preds = classifier.predict(X_test)for i, y_pred in enumerate(y_preds[-10:]): print("预测类型: %s -- 信息: %s" % (y_pred, X_test_raw.iloc[i])) 预测类型: ham -- 信息: Aight no rush, I'll ask jay预测类型: ham -- 信息: Sos! Any amount i can get pls.预测类型: ham -- 信息: You unbelievable faglord预测类型: ham -- 信息: Carlos'll be here in a minute if you still need to buy预测类型: spam -- 信息: Meet after lunch la...预测类型: ham -- 信息: Hey tmr maybe can meet you at yck预测类型: ham -- 信息: I'm on da bus going home...预测类型: ham -- 信息: O was not into fps then.预测类型: ham -- 信息: Yes..he is really great..bhaji told kallis best cricketer after sachin in world:).very tough to get out.预测类型: ham -- 信息: Did you show him and wot did he say or could u not c him 4 dust?2.2 模型评估混淆举证test = y_testtest[test == "ham"] = 0test[test == "spam"] = 1 pred = y_predspred[pred == "ham"] = 0pred[pred == "spam"] = 1from sklearn.metrics import confusion_matrixtest = test.astype('int')pred = pred.astype('int')confusion_matrix = confusion_matrix(test.values, pred)print(confusion_matrix)plt.matshow(confusion_matrix)font = FontProperties(fname=r"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc")plt.title(' 混淆矩阵',fontproperties=font)plt.colorbar()plt.ylabel(' 实际类型',fontproperties=font)plt.xlabel(' 预测类型',fontproperties=font)plt.show()[[1191 1] [ 50 151]] 精度from sklearn.metrics import accuracy_score print(accuracy_score(test.values, pred))0.9633883704235463交叉验证精度df = pd.read_csv("sms.csv")df.head() X_train_raw, X_test_raw, y_train, y_test = train_test_split(df['message'], df['label'])vectorizer = TfidfVectorizer()X_train = vectorizer.fit_transform(X_train_raw)X_test = vectorizer.transform(X_test_raw)classifier = LogisticRegression()classifier.fit(X_train, y_train)scores = cross_val_score(classifier, X_train, y_train, cv=5)print(' 精度:',np.mean(scores), scores) 精度: 0.9562200956937799 [0.94736842 0.95933014 0.95574163 0.95574163 0.96291866]准确率召回率precisions = cross_val_score(classifier, X_train, y_train, cv=5, scoring='precision')print('准确率:', np.mean(precisions), precisions)recalls = cross_val_score(classifier, X_train, y_train, cv=5, scoring='recall')print('召回率:', np.mean(recalls), recalls)准确率: 0.9920944081237428 [0.98550725 1. 1. 0.98701299 0.98795181]召回率: 0.6778796653796653 [0.61261261 0.69642857 0.66964286 0.67857143 0.73214286]F1 度量f1s = cross_val_score(classifier, X_train, y_train, cv=5, scoring='f1')print(' 综合评价指标:', np.mean(f1s), f1s) 综合评价指标: 0.8048011339652206 [0.75555556 0.82105263 0.80213904 0.8042328 0.84102564]ROC AUCfrom sklearn.metrics import roc_curve, aucpredictions = classifier.predict_proba(X_test)false_positive_rate, recall, thresholds = roc_curve(y_test, predictions[:, 1])roc_auc = auc(false_positive_rate, recall)plt.title('Receiver Operating Characteristic')plt.plot(false_positive_rate, recall, 'b', label='AUC = %0.2f' % roc_auc)plt.legend(loc='lower right')plt.plot([0, 1], [0, 1], 'r--')plt.xlim([0.0, 1.0])plt.ylim([0.0, 1.0])plt.ylabel('Recall')plt.xlabel('Fall-out')plt.show() 原文地址https://www.cnblogs.com/raina/p/12777725.html
SOUL ANDROID APP 悬浮VIEW以及帖子中VIEW的联动刷新逆向分析 Soul app是我司的竞品,对它的语音音乐播放同步联动的逻辑很感兴趣,于是就开启了一波逆向分析。 下面看代码,以及技术分析,直接步入正轨,哈哈。 我们根据https://github.com/xingstarx/ActivityTracker 这个工具,找到某一个页面,比如cn.soulapp.android/.ui.post.detail.PostDetailActivity 这个页面,然后我们用反编译工具AndroidToolPlus反编译soul 的Android apk, 然后搜索下PostDetailActivity这个类。然后找到这个类之后,我们在根据代码经验猜测,这个语音音乐封装的控件可能在哪,肯定是在PostDetailActivity里面或者是他内容的某个成员变量里面,一不小心,我们就找到了PostDetailHeaderProvider。在这个类里面找到了MusicStoryPlayView, AudioPostView这两个view类,他们就是封装好的音频view,音乐view。(就不截图了。有人感兴趣可以按照我说的实践一番就能得到结论了) 关键代码找到了。那就看看他们内部实现吧。 public class MusicStoryPlayView extends FrameLayout implements SoulMusicPlayer.MusicPlayListener类结构上,实现了核心播放器的listener逻辑,那就说明,他的刷新逻辑,都是通过播放器自身的播放状态回调到view自身上,然后view自身实现了对应的刷新机制就可以更改view的状态了 我们选取几个回调的逻辑看看。不做仔细分析。 public void onPause(cn.soulapp.android.lib.common.c.i parami) { d(); } public void onPlay(cn.soulapp.android.lib.common.c.i parami) { LoveBellingManager.e().d(); } public void onPrepare(cn.soulapp.android.lib.common.c.i parami) { if (this.e == null) { return; } if (parami.b().equals(this.e.songMId)) { e(); } } 那么我们还得思考一个问题,这个listener是什么时候被添加进来的呢。关键点在于view自身的两个方法 protected void onAttachedToWindow() { super.onAttachedToWindow(); SoulMusicPlayer.k().a(this); } protected void onDetachedFromWindow() { super.onDetachedFromWindow(); SoulMusicPlayer.k().b(this); } 所以很明显,在view被添加到window上(也就是在页面上显示出来)的时候,添加入listener里面,从页面消失,就移除出去。 接着我们在看看核心播放器的逻辑里面,是怎么调度的? 根据代码相关联的逻辑,我们很容易找到核心播放器类SoulMusicPlayer public void a(cn.soulapp.android.lib.common.c.i parami) { y0.d().a(); LoveBellingManager.e().d(); MusicPlayer.i().f(); if (TextUtils.isEmpty(parami.f())) { return; } Object localObject1 = this.d; if (localObject1 != null) { if (!((cn.soulapp.android.lib.common.c.i)localObject1).equals(parami)) { i(); } else { if (!f()) { this.a.setLooping(parami.g()); h(); } return; } } if (this.a == null) { this.a = new IjkMediaPlayer(); this.a.setOnErrorListener(this); this.a.setOnCompletionListener(this); this.a.setOnPreparedListener(this); } this.a.setLooping(parami.g()); try { if (l0.e(parami.f())) { SoulApp localSoulApp; Object localObject2; if (parami.a() != null) { localObject1 = this.a; localSoulApp = SoulApp.e(); localObject2 = new java/io/File; ((File)localObject2).<init>(parami.f()); ((IjkMediaPlayer)localObject1).setDataSource(localSoulApp, Uri.fromFile((File)localObject2), parami.a()); } else { localObject2 = this.a; localSoulApp = SoulApp.e(); localObject1 = new java/io/File; ((File)localObject1).<init>(parami.f()); ((IjkMediaPlayer)localObject2).setDataSource(localSoulApp, Uri.fromFile((File)localObject1)); } } else { localObject1 = parami.a(); if (localObject1 != null) { this.a.setDataSource(SoulApp.e(), Uri.parse(parami.f().replace("https", "http")), parami.a()); } else { this.a.setDataSource(SoulApp.e(), Uri.parse(parami.f().replace("https", "http"))); } } this.a.prepareAsync(); this.d = parami; this.b = true; } catch (IOException parami) { parami.printStackTrace(); } } public void g() { if (f()) { Object localObject = this.a; if (localObject != null) { this.b = false; ((IjkMediaPlayer)localObject).pause(); localObject = this.e.iterator(); while (((Iterator)localObject).hasNext()) { ((MusicPlayListener)((Iterator)localObject).next()).onPause(this.d); } this.c.removeCallbacksAndMessages(null); } } } 仔细观察分析这两个方法体,大致可以猜测出,他们是start逻辑,以及暂停播放的逻辑。可以分析出,核心播放器执行完播放,暂停,停止等逻辑后,都会调用List里面的listener,遍历listener,然后触发对应的回调逻辑。 恩,大体的思路有了,就是这么搞,哈哈。 那么我用于我自己项目中,是这么用的么,还是有一些细微差异的,整体方案是参考的soul。细微不同之处在于我是将MusicStoryPlayView放在xml里面,不是像soul那样,直接new的。所以MusicStoryPlayView会被添加很多次,比如在列表中有很多个的话,后面需要判断播放的媒体资源,跟MusicStoryPlayView存放的媒体资源的主键是否一致。 此外出了view类,我对于一些特殊的逻辑,比如Activity或者是悬浮view等等,都实现了PlayListener。通过他们可以实现一些棘手的问题。 原文地址https://www.cnblogs.com/xing-star/p/12768379.html
Mybatis源码详解系列(三)--从Mapper接口开始看Mybatis的执行逻辑 简介Mybatis 是一个持久层框架,它对 JDBC 进行了高级封装,使我们的代码中不会出现任何的 JDBC 代码,另外,它还通过 xml 或注解的方式将 sql 从 DAO/Repository 层中解耦出来,除了这些基本功能外,它还提供了动态 sql、延迟加载、缓存等功能。 相比 Hibernate,Mybatis 更面向数据库,可以灵活地对 sql 语句进行优化。 本文继续分析 Mybatis 的源码,第1点内容上一篇博客已经讲过,本文将针对 2 和 3 点继续分析: 加载配置、初始化SqlSessionFactory;获取SqlSession和Mapper;执行Mapper方法。除了源码分析,本系列还包含 Mybatis 的详细使用方法、高级特性、生成器等,相关内容可以我的专栏 Mybatis。 注意,考虑可读性,文中部分源码经过删减。 隐藏在Mapper背后的东西从使用者的角度来看,项目中使用 Mybatis 时,我们只需要定义Mapper接口和编写 xml,除此之外,不需要去使用 Mybatis 的其他东西。当我们调用了 Mapper 接口的方法,Mybatis 悄无声息地为我们完成参数设置、语句执行、结果映射等等工作,这真的是相当优秀的设计。 既然是分析源码,就必须搞清楚隐藏 Mapper 接口背后都是什么东西。这里我画了一张 UML 图,通过这张图,应该可以对 Mybatis 的架构及 Mapper 方法的执行过程形成比较宏观的了解。 针对上图,我再简单梳理下: Mapper和SqlSession可以认为是用户的入口(项目中也可以不用Mapper接口,直接使用SqlSession),Mybatis 为我们生产的Mapper实现类最终都会去调用SqlSession的方法;Executor作为整个执行流程的调度者,它依赖StatementHandler来完成参数设置、语句执行和结果映射,使用Transaction来管理事务。StatementHandler调用ParameterHandler为语句设置参数,调用ResultSetHandler将结果集映射为所需对象。那么,我们开始看源码吧。 Mapper代理类的获取一般情况下,我们会先拿到SqlSession对象,然后再利用SqlSession获取Mapper对象,这部分的源码也是按这个顺序开展。 复制代码// 获取 SqlSessionSqlSession sqlSession = sqlSessionFactory.openSession();// 获取 MapperEmployeeMapper baseMapper = sqlSession.getMapper(EmployeeMapper.class);先拿到SqlSession对象SqlSession的获取过程上一篇博客讲了DefaultSqlSessionFactory的初始化,现在我们将利用DefaultSqlSessionFactory来创建SqlSession,这个过程也会创建出对应的Executor和Transaction,如下图所示。 图中的SqlSession创建时需要先创建Executor,而Executor又要依赖Transaction的创建,Transaction则需要依赖已经初始化好的TransactionFactory和DataSource。 进入到DefaultSqlSessionFactory.openSession()方法。默认情况下,SqlSession是线程不安全的,主要和Transaction对象有关,如果考虑复用SqlSession对象的话,需要重写Transaction的实现。 复制代码@Overridepublic SqlSession openSession() { // 默认会使用SimpltExecutors,以及autoCommit=false,事务隔离级别为空 // 当然我们也可以在入参指定 // 补充:SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement);BATCH 执行器不仅重用语句还会执行批量更新。 return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); } private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { // 获取Environment中的TransactionFactory和DataSource,用来创建事务对象 final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 创建执行器,这里也会给执行器安装插件 final Executor executor = configuration.newExecutor(tx, execType); // 创建DefaultSqlSession return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }如何给执行器安装插件上面的代码比较简单,这里重点说下安装插件的过程。我们进入到Configuration.newExecutor(Transaction, ExecutorType),可以看到创建完执行器后,还需要给执行器安装插件,接下来就是要分析下如何给执行器安装插件。 复制代码protected final InterceptorChain interceptorChain = new InterceptorChain();public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; // 根据executorType选择创建不同的执行器 if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } if (cacheEnabled) { executor = new CachingExecutor(executor); } // 安装插件 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }进入到InterceptorChain.pluginAll(Object)方法,这是一个相当通用的方法,不是只能给Executor安装插件,后面我们看到的StatementHandler、ResultSetHandler、ParameterHandler等都会被安装插件。 复制代码private final List interceptors = new ArrayList<>();public Object pluginAll(Object target) { // 遍历安装所有执行器 for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; }// 进入到Interceptor.plugin(Object)方法,这个是接口里的方法,使用 default 声明default Object plugin(Object target) { return Plugin.wrap(target, this); }在定义插件时,一般我们都会采用注解来指定需要拦截的接口及其方法,如下。安装插件的方法之所以能够通用,主要还是@Signature注解的功劳,注解中,我们已经明确了拦截哪个接口的哪个方法。注意,这里我也可以定义其他接口,例如StatementHandler。 复制代码@Intercepts( { @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), } )public class PageInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // do something } }上面这个插件将对Executor接口的以下两个方法进行拦截: 复制代码 List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException; List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;那么,Mybatis 是如何实现的呢?我们进入到Plugin这个类,它是一个InvocationHandler,也就是说 Mybatis 使用的是 JDK 的动态代理来实现插件功能,后面代码中, JDK 的动态代理也会经常出现。 复制代码public class Plugin implements InvocationHandler { // 需要被安装插件的类 private final Object target; // 需要安装的插件 private final Interceptor interceptor; // 存放插件拦截的接口接对应的方法 private final Map<Class<?>, Set<Method>> signatureMap; private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) { this.target = target; this.interceptor = interceptor; this.signatureMap = signatureMap; } public static Object wrap(Object target, Interceptor interceptor) { // 根据插件的注解获取插件拦截哪些接口哪些方法 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 获取目标类中需要被拦截的所有接口 Class<?> type = target.getClass(); Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { // 创建代理类 return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 以“当前方法的声明类”为key,查找需要被插件拦截的方法 Set<Method> methods = signatureMap.get(method.getDeclaringClass()); // 如果包含当前方法,那么会执行插件里的intercept方法 if (methods != null && methods.contains(method)) { return interceptor.intercept(new Invocation(target, method, args)); } // 如果并不包含当前方法,则直接执行该方法 return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } 以上就是获取SqlSession和安装插件的内容。默认情况下,SqlSession是线程不安全的,不断地创建SqlSession也不是很明智的做法,按道理,Mybatis 应该提供线程安全的一套SqlSession实现才对。 再获取Mapper代理类Mapper 代理类的获取过程比较简单,这里我们就不一步步看源码了,直接看图就行。我画了 UML 图,通过这个图基本可以梳理以下几个类的关系,继而明白获取 Mapper 代理类的方法调用过程,另外,我们也能知道,Mapper 代理类也是使用 JDK 的动态代理生成。 Mapper 作为一个用户接口,最终还是得调用SqlSession来进行增删改查,所以,代理类也必须持有对SqlSession的引用。通常情况下,这样的 Mapper代理类是线程不安全的,因为它持有的SqlSession实现类DefaultSqlSession也是线程不安全的,但是,如果实现类是SqlSessionManager就另当别论了。 Mapper方法的执行执行Mapper代理方法因为Mapper代理类是通过 JDK 的动态代理生成,当调用Mapper代理类的方法时,对应的InvocationHandler对象(即MapperProxy)将被调用,所以,这里就不展示Mapper代理类的代码了,直接从MapperProxy这个类开始分析。 同样地,还是先看看整个 UML 图,通过图示大致可以梳理出方法的调用过程。MethodSignature这个类可以重点看下,它的属性非常关键。 下面开始看源码,进入到MapperProxy.invoke(Object, Method, Object[])。这里的MapperMethodInvoker对象会被缓存起来,因为这个类是无状态的,不需要反复的创建。当缓存中没有对应的MapperMethodInvoker时,方法对应的MapperMethodInvoker实现类将被创建并放入缓存,同时MapperMethod、MethodSignature、sqlCommand等对象都会被创建好。 复制代码@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 如果是Object类声明的方法,直接调用 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else { // 先从缓存拿到MapperMethodInvoker对象,再调用它的方法 // 因为最终会调用SqlSession的方法,所以这里得传入SqlSession对象 return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } }private MapperMethodInvoker cachedInvoker(Method method) throws Throwable { try { // 缓存有就拿,没有需创建并放入缓存 return methodCache.computeIfAbsent(method, m -> { // 如果是接口中定义的default方法,创建MapperMethodInvoker实现类DefaultMethodInvoker // 这种情况我们不关注 if (m.isDefault()) { try { if (privateLookupInMethod == null) { return new DefaultMethodInvoker(getMethodHandleJava8(method)); } else { return new DefaultMethodInvoker(getMethodHandleJava9(method)); } } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException(e); } } else { // 如果不是接口中定义的default方法,创建MapperMethodInvoker实现类PlainMethodInvoker,在此之前也会创建MapperMethod return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); } }); } catch (RuntimeException re) { Throwable cause = re.getCause(); throw cause == null ? re : cause; } }由于我们不考虑DefaultMethodInvoker的情况,所以,这里直接进入到MapperProxy.PlainMethodInvoker.invoke(Object, Method, Object[], SqlSession)。 复制代码private final MapperMethod mapperMethod;@Overridepublic Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable { // 直接调用MapperMethod的方法,method和proxy的入参丢弃 return mapperMethod.execute(sqlSession, args); }进入到MapperMethod.execute(SqlSession, Object[])方法。前面提到过 Mapper 代理类必须依赖SqlSession对象来进行增删改查,在这个方法就可以看到,方法中会通过方法的类型来决定调用SqlSession的哪个方法。 在进行参数转换时有三种情况: 如果参数为空,则 param 为 null;如果参数只有一个且不包含Param注解,则 param 就是该入参对象;如果参数大于一个或包含了Param注解,则 param 是一个Map,key 为注解Param的值,value 为对应入参对象。另外,针对 insert|update|delete 方法,Mybatis 支持使用 void、Integer/int、Long/long、Boolean/boolean 的返回类型,而针对 select 方法,支持使用 Collection、Array、void、Map、Cursor、Optional 返回类型,并且支持入参 RowBounds 来进行分页,以及入参 ResultHandler 来处理返回结果。 复制代码public Object execute(SqlSession sqlSession, Object[] args) { Object result; // 判断属于哪种类型,来决定调用SqlSession的哪个方法 switch (command.getType()) { // 如果为insert类型方法 case INSERT: { // 参数转换 Object param = method.convertArgsToSqlCommandParam(args); // rowCountResult将根据返回类型来处理result,例如,当返回类型为boolean时影响的rowCount是否大于0,当返回类型为int时,直接返回rowCount result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } // 如果为update类型方法 case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } // 如果为delete类型方法 case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } // 如果为select类型方法 case SELECT: // 返回void,且入参有ResultHandler if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; // 返回类型为数组或List } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); // 返回类型为Map } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); // 返回类型为Cursor } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); // 这种一般是返回单个实体对象或者Optional对象 } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } break; // 如果为FLUSH类型方法,这种情况不关注 case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } // 当方法返回类型为基本类型,但是result却为空,这种情况会抛出异常 if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; }增删改的我们不继续看了,至于查的,只看method.returnsMany()的情况。进入到MapperMethod.executeForMany(SqlSession, Object[])。通过这个方法可以知道,当返回多个对象时,Mapper 中我们可以使用List接收,也可以使用数组或者Collection的其他子类来接收,但是处于性能考虑,如果不是必须,建议还是使用List比较好。 将RowBounds作为 Mapper 方法的入参,可以支持自动分页功能,但是,这种方式存在一个很大缺点,就是 Mybatis 会将所有结果查放入本地内存再进行分页,而不是查的时候嵌入分页参数。所以,这个分页入参,建议还是不要使用了。 复制代码private Object executeForMany(SqlSession sqlSession, Object[] args) { List<E> result; // 转换参数 Object param = method.convertArgsToSqlCommandParam(args); // 如果入参包含RowBounds对象,这个一般用于分页使用 if (method.hasRowBounds()) { RowBounds rowBounds = method.extractRowBounds(args); result = sqlSession.selectList(command.getName(), param, rowBounds); } else { // 不用分页的情况 result = sqlSession.selectList(command.getName(), param); } // 如果SqlSession方法的返回类型和Mapper方法的返回类型不一致 // 例如,mapper返回类型为数组、Collection的其他子类 if (!method.getReturnType().isAssignableFrom(result.getClass())) { // 如果mapper方法要求返回数组 if (method.getReturnType().isArray()) { return convertToArray(result); } else { // 如果要求返回Set等Collection子类,这个方法感兴趣的可以研究下,非常值得借鉴学习 return convertToDeclaredCollection(sqlSession.getConfiguration(), result); } } return result; }从Mapper进入到SqlSession接下来就需要进入SqlSession的方法了,这里选用实现类DefaultSqlSession进行分析。SqlSession作为用户入口,代码不会太多,主要工作还是通过执行器来完成。 在调用执行器方法之前,这里会对参数对象再次包装,一般针对入参只有一个参数且不包含Param注解的情况: 如果是Collection子类,将转换为放入"collection"=object键值对的 map,如果它是List的子类,还会再放入"list"=object的键值对如果是数组,将转换为放入"array"=object键值对的 map复制代码@Overridepublic List selectList(String statement, Object parameter) { // 这里还是给它传入了一个分页对象,这个对象默认分页参数为0,Integer.MAX_VALUE return this.selectList(statement, parameter, RowBounds.DEFAULT); } @Overridepublic List selectList(String statement, Object parameter, RowBounds rowBounds) { try { // 利用方法id从配置对象中拿到MappedStatement对象 MappedStatement ms = configuration.getMappedStatement(statement); // 接着执行器开始调度,传入resultHandler为空 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }执行器开始调度接下来就进入执行器的部分了。注意,由于本文不会涉及到 Mybatis 结果缓存的内容,所以,下面的代码都会删除缓存相关的部分。 那么,还是回到最开始的图,接下来将选择SimpleExecutor进行分析。 进入到BaseExecutor.query(MappedStatement, Object, RowBounds, ResultHandler)。这个方法中会根据入参将动态语句转换为静态语句,并生成对应的ParameterMapping。 例如, 复制代码and e.gender = {con.gender}将被转换为 and e.gender = ?,并且生成 复制代码ParameterMapping{property='con.gender', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}的ParameterMapping。 生成的ParameterMapping将根据?的索引放入集合中待使用。 这部分内容我就不展开了,感兴趣地可以自行研究。 复制代码@Overridepublic List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 将sql语句片段中的动态部分转换为静态,并生成对应的ParameterMapping BoundSql boundSql = ms.getBoundSql(parameter); // 生成缓存的key CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }接下来一大堆关于结果缓存的代码,前面说过,本文不讲,所以我们直接跳过进入到SimpleExecutor.doQuery(MappedStatement, Object, RowBounds, ResultHandler, BoundSql)。可以看到,接下来的任务都是由StatementHandler来完成,包括了参数设置、语句执行和结果集映射等。 复制代码@Overridepublic List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); // 获取StatementHandler对象,在上面的UML中可以看到,它非常重要 // 创建StatementHandler对象时,会根据StatementType自行判断选择SimpleStatementHandler、PreparedStatementHandler还是CallableStatementHandler实现类 // 另外,还会给它安装执行器的所有插件 StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // 获取Statement对象,并设置参数 stmt = prepareStatement(handler, ms.getStatementLog()); // 执行语句,并映射结果集 return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } }本文将对以下两行代码分别展开分析。其中,关于参数设置的内容不会细讲,更多地精力会放在结果集映射上面。 复制代码// 获取Statement对象,并设置参数stmt = prepareStatement(handler, ms.getStatementLog());// 执行语句,并映射结果集return handler.query(stmt, resultHandler);语句处理器开始处理语句在创建StatementHandler时,会通过MappedStatement.getStatementType()自动选择使用哪种语句处理器,有以下情况: 如果是 STATEMENT,则选择SimpleStatementHandler;如果是 PREPARED,则选择PreparedStatementHandler;如果是 CALLABLE,则选择CallableStatementHandler;其他情况抛出异常。本文将选用PreparedStatementHandler进行分析。 获取语句对象和设置参数进入到SimpleExecutor.prepareStatement(StatementHandler, Log)。这个方法将会获取当前语句的PreparedStatement对象,并给它设置参数。 复制代码protected Transaction transaction;private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; // 获取连接对象,通过Transaction获取 Connection connection = getConnection(statementLog); // 获取Statement对象,由于分析的是PreparedStatementHandler,所以会返回实现类PreparedStatement stmt = handler.prepare(connection, transaction.getTimeout()); // 设置参数 handler.parameterize(stmt); return stmt; }进入到PreparedStatementHandler.parameterize(Statement)。正如 UML 图中说到的,这里实际上是调用ParameterHandler来设置参数。 复制代码protected final ParameterHandler parameterHandler;@Overridepublic void parameterize(Statement statement) throws SQLException { parameterHandler.setParameters((PreparedStatement) statement); }进入到DefaultParameterHandler.setParameters(PreparedStatement)。前面讲过,在将动态语句转出静态语句时,生成了语句每个?对应的ParameterMapping,并且这些ParameterMapping会按照语句中对应的索引被放入集合中。在以下方法中,就是遍历这个集合,将参数设置到PreparedStatement中去。 复制代码private final TypeHandlerRegistry typeHandlerRegistry;private final MappedStatement mappedStatement;private final Object parameterObject;private final BoundSql boundSql;private final Configuration configuration; @Overridepublic void setParameters(PreparedStatement ps) { // 获得当前语句对应的ParameterMapping List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { // 遍历ParameterMapping for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); // 一般情况mode都是IN,至于OUT的情况,用于结果映射到入参,比较少用 if (parameterMapping.getMode() != ParameterMode.OUT) { Object value;// 用于设置到ps中的?的参数 // 这个propertyName对应mapper中#{value}的名字 String propertyName = parameterMapping.getProperty(); // 判断additionalParameters是否有这个propertyName,这种情况暂时不清楚 if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params value = boundSql.getAdditionalParameter(propertyName); // 如果入参为空 } else if (parameterObject == null) { value = null; // 如果有当前入参的类型处理器 } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; // 如果没有当前入参的类型处理器,这种一般是传入实体对象或传入Map的情况 } else { // 这个原理和前面说过的MetaClass差不多 MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); // 如果未指定jdbcType,且入参为空,没有在setting中配置jdbcTypeForNull的话,默认为OTHER if (value == null && jdbcType == null) { jdbcType = configuration.getJdbcTypeForNull(); } try { // 利用类型处理器给ps设置参数 typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (TypeException | SQLException e) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); } } } } }使用ParameterHandler设置参数的内容就不再多讲,接下来分析语句执行和结果集映射的代码。 语句执行和结果集映射进入到PreparedStatementHandler.query(Statement, ResultHandler)方法。语句执行就是普通的 JDBC,没必要多讲,重点看看如何使用ResultSetHandler完成结果集的映射。 复制代码protected final ResultSetHandler resultSetHandler;@Overridepublic List query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; // 直接执行 ps.execute(); // 映射结果集 return resultSetHandler.handleResultSets(ps); }为了满足多种需求,Mybatis 在处理结果集映射的逻辑非常复杂,这里先简单说下。 一般我们的 resultMap 是这样配置的: 复制代码 <association property="author" column="author_id" javaType="Author" select="selectAuthor"/> SELECT * FROM BLOG WHERE ID = #{id} 然而,Mybatis 竟然也支持多 resultSet 映射的情况,这里拿到的第二个结果集将使用resultSet="authors"的resultMap 进行映射,并将得到的Author设置进Blog的属性。 复制代码 <id property="id" column="id" /> <result property="title" column="title"/> <association property="author" javaType="Author" resultSet="authors" column="author_id" foreignColumn="id"> <id property="id" column="id"/> <result property="username" column="username"/> <result property="password" column="password"/> <result property="email" column="email"/> <result property="bio" column="bio"/> </association> {call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})} 还有一种更加奇葩的,多 resultSet、多 resultMap,如下。这种我暂时也不清楚怎么用。 复制代码 {call getTwoBlogs(#{id,jdbcType=INTEGER,mode=IN})} 接下来只考虑第一种情况。另外两种感兴趣的自己研究吧。 进入到DefaultResultSetHandler.handleResultSets(Statement)方法。 复制代码@Overridepublic List // 用于存放最终对象的集合 final List<Object> multipleResults = new ArrayList<>(); // resultSet索引 int resultSetCount = 0; // 获取第一个结果集 ResultSetWrapper rsw = getFirstResultSet(stmt); // 获取当前语句对应的所有ResultMap List<ResultMap> resultMaps = mappedStatement.getResultMaps(); // resultMap总数 int resultMapCount = resultMaps.size(); // 校验结果集非空时resultMapCount是否为空 validateResultMapsCount(rsw, resultMapCount); // 接下来结果集和resultMap会根据索引一对一地映射 while (rsw != null && resultMapCount > resultSetCount) { // 获取与当前结果集映射的resultMap ResultMap resultMap = resultMaps.get(resultSetCount); // 映射结果集,并将生成的对象放入multipleResults handleResultSet(rsw, resultMap, multipleResults, null); // 获取下一个结果集 rsw = getNextResultSet(stmt); // TODO cleanUpAfterHandlingResultSet(); // resultSet索引+1 resultSetCount++; } // 如果当前resultSet的索引小于resultSets中配置的resultSet数量,将继续映射 // 这就是前面说的第二种情况了,这个不讲 String[] resultSets = mappedStatement.getResultSets(); if (resultSets != null) { while (rsw != null && resultSetCount < resultSets.length) { // 获取指定resultSet对应的ResultMap ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); if (parentMapping != null) { // 获取嵌套ResultMap进行映射 String nestedResultMapId = parentMapping.getNestedResultMapId(); ResultMap resultMap = configuration.getResultMap(nestedResultMapId); handleResultSet(rsw, resultMap, null, parentMapping); } // 获取下一个结果集 rsw = getNextResultSet(stmt); // TODO cleanUpAfterHandlingResultSet(); // resultSet索引+1 resultSetCount++; } } // 如果multipleResults只有一个,返回multipleResults.get(0),否则整个multipleResults一起返回 return collapseSingleResultList(multipleResults); }进入DefaultResultSetHandler.handleResultSet(ResultSetWrapper, ResultMap, List 属性 resultHandler 一般为空,除非在 Mapper 方法的入参中传入,这个对象可以由用户自己实现,通过它我们可以对结果进行操作。在实际项目中,我们往往是拿到实体对象后才到 Web 层完成 VO 对象的转换,通过ResultHandler,我们在 DAO 层就能完成 VO 对象的转换,相比传统方式,这里可以减少一次集合遍历,而且,因为可以直接传入ResultHandler,而不是具体实现,所以转换过程不会渗透到 DAO层。注意,采用这种方式时,Mapper 的返回类型必须为 void。 复制代码private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List try { // 如果不是设置了多个resultSets,parentMapping一般为空 // 所以,这种情况不关注 if (parentMapping != null) { // 映射结果集 handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping); } else { // resultHandler一般为空 if (resultHandler == null) { // 创建defaultResultHandler DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory); // 映射结果集 handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null); // 将对象放入集合 multipleResults.add(defaultResultHandler.getResultList()); } else { // 映射结果集,如果传入了自定义的ResultHandler,则由用户自己处理映射好的对象 handleRowValues(rsw, resultMap, resultHandler, rowBounds, null); } } } finally { // issue #228 (close resultsets) closeResultSet(rsw.getResultSet()); } }进入到DefaultResultSetHandler.handleRowValues(ResultSetWrapper, ResultMap, ResultHandler<?>, RowBounds, ResultMapping)。这里会根据是否包含嵌套结果映射来判断调用哪个方法,如果是嵌套结果映射,需要判断是否允许分页,以及是否允许使用自定义ResultHandler。 复制代码public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { // 如果resultMap存在嵌套结果映射 if (resultMap.hasNestedResultMaps()) { // 如果设置了safeRowBoundsEnabled=true,需校验在嵌套语句中使用分页时抛出异常 ensureNoRowBounds(); // 如果设置了safeResultHandlerEnabled=false,需校验在嵌套语句中使用自定义结果处理器时抛出异常 checkResultHandler(); // 映射结果集 handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping); // 如果resultMap不存在嵌套结果映射 } else { // 映射结果集 handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping); } }这里我就不搞那么复杂了,就只看非嵌套结果的情况。进入DefaultResultSetHandler.handleRowValuesForSimpleResultMap(ResultSetWrapper, ResultMap, ResultHandler<?>, RowBounds, ResultMapping)。在这个方法中可以看到,使用RowBounds进行分页时,Mybatis 会查出所有数据到内存中,然后再分页,所以,不建议使用。 复制代码private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { // 创建DefaultResultContext对象,这个用来做标志判断使用,还可以作为ResultHandler处理结果的入参 DefaultResultContext<Object> resultContext = new DefaultResultContext<>(); // 获取当前结果集 ResultSet resultSet = rsw.getResultSet(); // 剔除分页offset以下数据 skipRows(resultSet, rowBounds); while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { // 如果存在discriminator,则根据结果集选择匹配的resultMap,否则直接返回当前resultMap ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); // 创建实体对象,并完成结果映射 Object rowValue = getRowValue(rsw, discriminatedResultMap, null); // 一般会回调ResultHandler的handleResult方法,让用户可以对映射好的结果进行处理 // 如果配置了resultSets的话,且当前在映射子结果集,那么会将子结果集映射到的对象设置到父对象的属性中 storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); } }进入DefaultResultSetHandler.getRowValue(ResultSetWrapper, ResultMap, String)方法。这个方法将创建对象,并完成结果集的映射。点到为止,有空再做补充了。 复制代码private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException { final ResultLoaderMap lazyLoader = new ResultLoaderMap(); // 创建实体对象,这里会完成构造方法中参数的映射,以及完成懒加载的代理 Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix); if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { // MetaObject可以方便完成实体对象的获取和设置属性 final MetaObject metaObject = configuration.newMetaObject(rowValue); // foundValues用于标识当前对象是否还有未映射完的属性 boolean foundValues = this.useConstructorMappings; // 映射列名和属性名一致的属性,如果设置了驼峰规则,那么这部分也会映射 if (shouldApplyAutomaticMappings(resultMap, false)) { foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues; } // 映射property的RsultMapping foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues; foundValues = lazyLoader.size() > 0 || foundValues; // 返回映射好的对象 rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null; } return rowValue; }以上,基本讲完 Mapper 的获取及方法执行相关源码的分析。 通过分析 Mybatis 的源码,希望读者能够更了解 Mybatis,从而在实际工作和学习中更好地使用,以及避免“被坑”。针对结果缓存、参数设置以及其他细节问题,本文没有继续展开,后续有空再补充吧。 参考资料Mybatis官方中文文档 相关源码请移步:mybatis-demo 本文为原创文章,转载请附上原文出处链接:https://www.cnblogs.com/ZhangZiSheng001/p/12761376.html 原文地址https://www.cnblogs.com/ZhangZiSheng001/p/12761376.html
代理模式是什么?如何在 C# 中实现代理模式 代理模式 并不是日常开发工作中常常用到的一种设计模式,也是一种不易被理解的一种设计模式。但是它会广泛的应用在系统框架、业务框架中。 定义它的 定义 就如其它同大部分 设计模式 的定义类似,即不通俗也不易懂,而且随便百度一下就能找到 : 为其他对象提供一种代理,以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介的作用。 每个字都认识,连在一起就看不懂了 by. 某个攻城狮 我们一个词一个词看就明白了。 其他对象所谓的 其它,其实就是你系统中 任意 一个类型,可以是 UserService、OrderRepository、DataDeletedEventListener、等等。 控制对这个对象的访问访问 其实就是调用这个对象上的方法、访问它的属性、设置它的属性等等,比如 User user = UserService.GetUserById(1); // 访问了 GetUserById 方法int id = user.Id; // 访问了 Id 属性Order order = OrderRepository.SelectByUserId(id); // 访问了 SelectByUserId 方法控制访问 ,控制 的本质是包装,外部不再直接使用 其他对象 ,而是使用 代理 ,再由代理来访问 其它对象。我们可以使用一个已有的 List 实现一个 IList ,并在使用 Add 方法时,打印一行日志。 public class LogList : IList{ // other code here.. private IList raw; // 这个就 "其它对象" public EventList(IList raw) { this.raw = raw; // 通过构造函数,这可以让 EventList 控制对 IList<T> 的访问。 } public void Add(T value) { this.raw.Add(value); Console.WriteLine(value); }}上面就是一个简单的代理模式的例子:为 IList 提供一种 LogList ,以控制对 IList 的访问。 实现简单实现上面 LogList 就是一种简单的实现。 但是你无法对这个类做外部扩展,所有的逻辑都在类型的内部被固定了。 于是我们可以使用下面的方法创建一个可以灵活扩展的 ListProxy public interface IListInterruption{ // other codes // 执行 IList.Add 时会进入的方法 void OnAdding(IList<T> list, T addingValue); // 执行完 IList.Add 时会进入的方法 void OnAdded(IList<T> list, T addedValue); // other codes } // 列表代理类// 允许外部提供 IListInterruption 来丰富 ListProxy 的逻辑。public class ListProxy : IList{ private readonly IList<T> raw; private readonly List<IListInterruption> interruptions; public ListProxy(IList<T> raw) { this.interruptions = new List<IListInterruption>(); this.raw = raw; } public void AddInterruption(IListInterruption interruption) { this.interruptions.Add(interruption); } public void Add(T value) { foreach(var item in this.interruptions) item.OnAdding(this.raw, value); this.raw.Add(value); foreach(var item in this.interruptions) item.OnAdded(this.raw, value); } }上面的代码实现一个较为灵活的 ProxyList 。 首先看看 IListInterruption。通过实现 IListInterruption 接口,可以向 ProxyList 提供各种各样的功能。 我们可以看一个简单的功能 public class LogListInterruption : IListInterruption{ // other codes public void OnAdding(IList<T> list, T addingValue) { Console.WriteLie("Adding : {0}", addingValue); } // other codes }向 ProxyList 添加上述组件,就可以实现在 Add 前打印待添加的值的功能。 List myList = new List();ProxyList proxy = new ProxyList(myList);proxy.AddInterruption(new LogListInterruption());proxy.Add(1);// >> Adding : 1这种实现方式可以创建出针对某个类型的代理类,并通过外部给予的 IListInterruption 来丰富代理类功能。 但缺点是,当你无法为所有的类型都创建 Proxy 和 Interruption 。 动态代理类之前的方法中,我们在编写阶段就已经建立了代理类,被称为静态代理类。 这种方法无法将代理类运用在系统中任何一个我们可能需要的类型上。 于是,动态代理类 就来了。 动态代理类 依靠编程语言本身的特征,让程序在 运行时 创建类型,实现接口,添加功能等等。 在 C# 中可以通过两种方式实现运行时创建类型的功能 CodeDom + 运行时编译EmitCodeDom 可以用来生成 C# 代码,再利运行时编译,会将C#代码编译为内存中的程序集,最后通过反射访问程序集中的成员。这种方法的特点就是慢。。。。因为要生成语句,还要编译,生成程序集,最后还要反射,都是大开销,所以慢是可想而知的。 Emit 提供了利用 IL 命令在运行时创建类型、方法,并填充方法内的功能。毕竟 C# 最终就是编译成 IL 的,所以直接使用 IL 性能当然快无敌了。 这个方式的缺点只有一个 : 学习 IL 。这玩意可不是每个人都愿意去学的。。。 于是,选择一些已经利用 Emit 做好了动态代理类功能的第三方功能库,成为了一个很好的选择。 C# 大环境下,可以用来生成动态代理类的库一般有两个选择 : PostSharpCaslte.DynamicProxy其中 PostSharp 使用了更复杂的技术,不是使用 Emit,而且在编译时,就将代理类所附加的功能放进了目标类型中,你可以通过 Attribute 向任意的方法添加额外的功能。 PostSharp 会在程序编译时,把这些 Attribute 的功能直接编译到方法内部。这种在编译时就把一切准备好的方法,让 PostSharp 有着最高的运行性能。但是又傻瓜、又好用的 PostSharp 只有一个缺点 ———— 收费。 Castle.DynamicProxy 是免费的,他是利用 Emit 在程序运行时创建代理类的。使用 Castle.DynamicProxy 有两个步骤: 编写 Interceptor将 Interceptor 附加到某个类型或接口上,并得到一个会调用你的 Interceptor 的代理类实例开发流程很像之前的 LogList 的例子。 相比较 PostSharp 那种一个 Attribute 就搞定一切的模式, Caslte.DynamicProxy 就没有那么方便了。 那么一个显而易见的问题就来了 :能不能利用 Caslte.DynamicProxy 实现像 PostSharp 那样利用 Attribute 创建代理类的功能呢? Reface.AppStarter.Proxy这是基于 Reface.AppStarter 开发的一个功能模块,使用它,可以利用 Attribute 的方式轻松的创建代理类,并实现 AOP 的功能。 你所要做的,就是创建一个继承于 ProxyAttribute 的特征。 ProxyAttribute 中有三个方法需要重写 OnExecuting ,被标记的方法执行时OnExecuted ,被标记的方法执行后OnExecuteError , 被标记的方法执行出现异常后你可以编写你的逻辑在这三个方法内,并将你的 Attribute 挂载到你需要的类型的方法上即可。 剩下的事情只有两件 向你的 AppModule 添加 ProxyAppModule为你需要创建代理的类型加上 [Component] 特征你已经完成了所有工作,当你利用 Reface.AppStarter 的框架的 IOC / DI 容器创建你的类型时,实际得到的就是代理类,这些代理类会调试你给予的 ProxyAttribute 。 关于 Reface.AppStarter.Proxy 的细节,会在以后的文章中进一步介绍。 原文地址https://www.cnblogs.com/ShimizuShiori/p/12752893.html
SpringCloud系列之网关(Gateway)应用篇 @ 目录前言项目版本网关访问鉴权配置限流配置前言由于项目采用了微服务架构,业务功能都在相应各自的模块中,每个业务模块都是以独立的项目运行着,对外提供各自的服务接口,如没有类似网关之类组件的话,相应的鉴权,限流等功能实现起来不能够进行统一的配置和管理,有了网关后一切都是如此的优雅。刚好新项目中采用了SpringCloud Gateway组件作为网关,就记录下项目中常用的配置吧。 项目版本spring-boot-version:2.2.5.RELEASEspring-cloud.version:Hoxton.SR3 网关访问示例项目还是延续SpringCloud系列原先的示例代码,引入网关仅仅只需新增spring-cloud-gateway项目即可。核心pom.xml(详细信息查看示例源码,在文章末尾) org.springframework.cloud <artifactId>spring-cloud-starter-gateway</artifactId> bootstrap.yml server: port: 9005spring: application: name: springcloud-gateway-service cloud: config: discovery: enabled: true service-id: config-server profile: dev label: master gateway: enabled: true #开启网关 discovery: locator: enabled: true #开启自动路由,以服务id建立路由,服务id默认大写 lower-case-service-id: true #服务id设置为小写 eureka: client: service-url: defaultZone: http://localhost:9003/eureka/ ApiGatewayApplication.java @EnableDiscoveryClient@SpringBootApplicationpublic class ApiGatewayApplication { public static void main(String[] args) { SpringApplication.run(ApiGatewayApplication.class, args); } }访问原先spring-cloud-system-server模块对外提供的接口http://localhost:9004/web/system/getEnvName 通过网关进行访问http://localhost:9005/system-server/web/system/getEnvName 请求能正常返回,那就说明网关组件已集成进来了,是不是很简单呢,一行配置项就搞定了,便于展现这边采用properties配置方式说明 spring.cloud.gateway.discovery.locator.enabled=true到此网关的基础配置应用已完成,通过网关访问的请求路径格式如下http://网关地址:网关端口/各自服务id/各自服务对外提供的URL访问 鉴权配置这边将spring-cloud-system-server模块引入spring security安全认证组件,上代码。pom.xml <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> application.properties spring.security.user.name=testspring.security.user.password=123456服务模块调整完后,重新启动该模块,访问对外请求接口,出现认证登录界面说明配置成功。http://localhost:9004/web/system/getEnvName 输入上述配置项中配置的用户名和密码后,接口请求返回正常。 请求网关地址,访问服务接口按下回车键时会跳转至服务项目认证页面,如下http://localhost:9005/system-server/web/system/getEnvName 接下来对网关模块进行相应调整bootstrap.yml spring: application: name: springcloud-gateway-service security: user: name: test password: 123456 新增安全认证过滤类SecurityBasicAuthorizationFilter.java @Componentpublic class SecurityBasicAuthorizationFilter implements GlobalFilter, Ordered { @Value("${spring.security.user.name}") private String username; @Value("${spring.security.user.password}") private String password; public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String auth = username.concat(":").concat(password); String encodedAuth = new sun.misc.BASE64Encoder().encode(auth.getBytes(Charset.forName("US-ASCII"))); String authHeader = "Basic " +encodedAuth; //headers中增加授权信息 ServerHttpRequest serverHttpRequest = exchange.getRequest().mutate().header("Authorization", authHeader).build(); ServerWebExchange build = exchange.mutate().request(serverHttpRequest).build(); return chain.filter(build); } /** * 优先级 * 数字越大优先级越低 * @return */ public int getOrder() { return -1; } }重启网关项目,重新访问服务地址,返回正常数据。这边说明下在测试时最好新开一个无痕窗口或者清理浏览器缓存后再进行测试,不然因会话缓存会导致安全认证没有生效的假象。http://localhost:9005/system-server/web/system/getEnvName 限流配置SpringCloud Gateway自带限流功能,但是基于redis,这边简单演示下,项目中没有使用而是使用了阿里开源的sentinel,后续将介绍下集成sentinel组件。pom.xml <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> bootstrap.yml spring: cloud: gateway: enabled: true #开启网关 discovery: locator: enabled: true #开启自动路由,以服务id建立路由,服务id默认大写 lower-case-service-id: true #服务id设置为小写 routes: - id: baidu_route uri: https://www.baidu.com/ predicates: - Path=/baidu/** filters: - name: RequestRateLimiter args: key-resolver: "#{@apiKeyResolver}" redis-rate-limiter.replenishRate: 1 #允许每秒处理多少个请求 redis-rate-limiter.burstCapacity: 5 #允许在一秒钟内完成的最大请求数 redis: host: 192.168.28.142 pool: 6379 password: password database: 1 RequestRateLimiterConfig.java @Configurationpublic class RequestRateLimiterConfig { @Bean @Primary public KeyResolver apiKeyResolver() { //URL限流,超出限流返回429状态 return exchange -> Mono.just(exchange.getRequest().getPath().toString()); } }重新启动网关项目,访问如下请求地址,会请求跳转至百度首页,目前配置项配置为1s内请求数5次,超过5次就会触发限流,返回429状态码,多次刷新就会出现如下页面http://localhost:9005/baidu/test 通过monitor命令实时查看redis信息 本次网关项目目录结构 同系列文章1-SpringCloud系列之配置中心(Config)使用说明2-SpringCloud系列之服务注册发现(Eureka)应用篇示例源码 原文地址https://www.cnblogs.com/chinaWu/p/12731796.html
初识指令重排序,Java 中的锁 指令重排序Java语言规范JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。 指令重排序的意义:使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率。看个demo public static void main(String[] args) throws InterruptedException { int j=0; int k=0; j++; System.out.println(k); System.out.println(j); } 上面这段代码可能会被重排序:如下 public static void main(String[] args) throws InterruptedException { int k=0; System.out.println(k); int j=0; j++; System.out.println(j); } 此时指令的执行顺序可以与代码逻辑顺序不一致,但不影响程序的最终结果. 再看个demo public class ThreadExample2 { static int i; public static boolean runing = true; public static void main(String[] args) throws InterruptedException { traditional(); Thread.sleep(100); runing = false; } public static void traditional() { Thread thread = new Thread() { @Override public void run() { while (runing){ i++;//没有方法,JVM会做指令重排序,激进优化 } } }; thread.start(); } } 执行下main方法 可以看出该程序一直在跑,不会停止. 此时jvm发现traditional方法内没有其他方法,JVM会做指令重排序,采取激进优化策略,对我们的代码进行了重排序 如下: static int i; public static boolean runing = true; public static void main(String[] args) throws InterruptedException { traditional(); Thread.sleep(100); runing = false; } public static void traditional() { Thread thread = new Thread() { boolean temp=runing;//注意这里,此时while的条件永远为true @Override public void run() { while (temp){ i++;//没有方法,JVM会做指令重排序,激进优化 } } }; thread.start(); } 因此程序不会停止. 我们稍微改动下代码,在while 循环里加个方法 static int i; public static boolean runing = true; public static void main(String[] args) throws InterruptedException { traditional(); Thread.sleep(100); runing = false; } public static void traditional() { boolean temp=runing; Thread thread = new Thread() { @Override public void run() { while (runing){// i++;//没有方法,JVM会做指令重排序,激进优化 //有方法,JVM认为可能存在方法溢出,不做指令重排序,保守优化策略 aa(); } } }; thread.start(); } public static void aa(){ System.out.println("hello"); } 看下结果 可以看出,程序自行停止了,因为有方法,JVM认为可能存在方法溢出,不做指令重排序,采取保守优化策略 runing = false;全局变量runing 改动值以后,被thread线程识别,while 循环里值变为false,就自动停止了. ok,继续,我们把main方法中的sleep()注释掉,如下 public static void main(String[] args) throws InterruptedException { traditional(); //Thread.sleep(100); runing = false;//会优先执行主线程的代码 } public static void traditional() { boolean temp=runing; Thread thread = new Thread() { @Override public void run() { while (runing){// i++; } } }; thread.start(); } 看下结果: 此时,程序停止了,这是为什么呢: 可能是因为thread 线程和main线程竞争cpu资源的时候,会优先分配给main线程(我不确定,读者们可以自己思考一下) Java 中的锁synchronized关键字在1.6版本之前,synchronized都是重量级锁 1.6之后,synchronized被优化,因为互斥锁比较笨重,如果线程没有互斥,那就不需要互斥锁 重量级锁1.当一个线程要访问一个共享变量时,先用锁把变量锁住,然后再操作,操作完了之后再释放掉锁,完成 2.当另一个线程也要访问这个变量时,发现这个变量被锁住了,无法访问,它就会一直等待,直到锁没了,它再给这个变量上个锁,然后使用,使用完了释放锁,以此进行 3.我们可以这么理解:重量级锁是调用操作系统的函数来实现的锁--mutex--互斥锁 以linux为例: 1.互斥变量使用特定的数据类型:pthread_mutex_t结构体,可以认为这是一个函数2.可以用pthread_mutex_init进行函数动态的创建 : int pthread_mutex_init(pthread_mutex_t mutex, const pthread_mutexattr_t attr)3.对锁的操作主要包括加锁 pthread_mutex_lock()、解锁pthread_mutex_unlock()和测试加锁 pthread_mutex_trylock()三个3.1 int pthread_mutex_tlock(pthread_mutex_t *mutex) 在寄存器中对变量操作(加/减1)3.2 int pthread_mutex_unlock(pthread_mutex_t *mutex) 释放锁,状态恢复3.3 int pthread_mutex_trylock(pthread_mutex_t *mutex) pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待函数pthread_mutex_trylock会尝试对互斥量加锁,如果该互斥量已经被锁住,函数调用失败,返回EBUSY,否则加锁成功返回0,线程不会被阻塞 偏向锁偏向锁是synchronized锁的对象没有资源竞争的情况下存在的,不会一直调用操作系统函数实现(第一次会调用),而重量级锁每次都会调用 看个demo public class SyncDemo2 { Object o= new Object(); public static void main(String[] args) { System.out.println("pppppppppppppppppppppp"); SyncDemo2 syncDemo = new SyncDemo2(); syncDemo.start(); } public void start() { Thread thread = new Thread() { public void run() { while (true) { try { Thread.sleep(500); sync(); } catch (InterruptedException e) { } } } }; Thread thread2 = new Thread() { @Override public void run() { while (true) { try { Thread.sleep(500); sync(); } catch (InterruptedException e) { e.printStackTrace(); } } } }; thread.setName("t1"); thread2.setName("t2"); //两个线程竞争时,synchronized是重量级锁,一个线程时,synchronized是偏向锁 thread.start(); thread2.start(); } //在1.6版本之前,synchronized都是重量级锁 //1.6之后,synchronized被优化,因为互斥锁比较笨重,如果线程没有互斥,那就不需要互斥锁 public void sync() { synchronized (o) { System.out.println(Thread.currentThread().getName()); } } } 代码很简单,就是启动两个线程,并且调用同一个同步方法,看下结果 可以看到,两个线程都执行了该同步方法,此时两个线程竞争,synchronized是重量级锁 我们把一个线程注释掉 //两个线程竞争时,synchronized是重量级锁,一个线程时,synchronized是偏向锁 thread.start(); //thread2.start(); 看下结果: 此时synchronized是偏向锁 那么怎么证明呢:我目前没那个实力,给个思路. 1.需要编译并修改linux源码函数pthread_mutex_lock(),在函数中打印当前线程的pid 2.在同步方法中打印语句"current id"+当前pid(需要自己写c语言实现),java的Thread.currentThread().getId()不能获取操作系统级别的pid 3.两个线程竞争时,执行一次 说明是重量级锁,因为每次都调用操作系统的函数pthread_mutex_lock()来实现 4.注释掉一个线程,再执行一次 说明是偏向锁,因为第一次会调用pthread_mutex_lock(),后面就不调用系统函数了. 原文地址https://www.cnblogs.com/lusaisai/p/12731593.html
【Android】EventReminder使用教程(日历事件导出封装库) 为啥要写这个库呢? 尝试自己写一个库调用,学习一下这个流程,为以后做准备日历库在网上的资料太少了,而这个功能却又很实用自己做的项目都会涉及到事件导出功能,不想重复写代码这个库目前可以支持日历事件的直接导出、删除,ics文件的生成 ics生成文件中支持重复规则 目前该库还在完善中 欢迎提供建议❤ 2|0使用方法 2|1引入 在项目中引用即可 implementation 'com.paul.eventreminder:eventreminder:0.0.3'2|2使用教程 CalendarManager不是特别方便的初始化方法: 你需要提供Activity(用来获取权限)以及日历账户配置名称 首先声明一个Manager: CalendarManager calendarManager=new CalendarManager(this,"测试");会自动请求日历写入权限,如果拒绝这一块逻辑我没有处理,你可以在自己的代码中去实现该逻辑。 添加一个事件的时候你需要创建一个CalendarEvent 对象,或者您也可以选择继承自这个类 属性如下: //总结 String summary; //内容 String content; //地点 String loc; //周次 List weekList; //周几 int dayOfWeek; //开始时间 String startTime; //结束时间 String endTime;这里的weeklist至关重要,因为通过该集合来控制事件的重复 考虑到部分存在[1,2,3,4,8,10]这种不规则的形式,因此采用逐一导入的方法。 开始时间和结束时间的格式为 :“8:00” 创建好你的事件后调用addCalendarEvent方法即可,这里需要传入一个当前周次来告诉我当前所处时间位置。这个周次一定是在你提供的weekList中的。 public void addCalendarEvent(CalendarEvent mySubject, int curWeek,OnExportProgressListener listener)当然你也可以选择不传curWeek这个参数,会自动默认添加从1月1号到当前时间过了几周 在添加事件事前,你也可以为其设置提醒 calendarManager.setAlarm(true);//开启提醒 calendarManager.setAlarmTime(15);//15分钟删除事件也很简单,直接调用delete方法即可。 public void deleteCalendarEvent(Context context,OnExportProgressListener listener)值得一提的是,删除判断的是事件内容末尾的@+ACCOUNT_NAME,所以请保持该名称在创建和删除时候要相同。 ICSManager初始化: ICSManager icsManager=new ICSManager(Context context,String userName);同Calendar一样,你需要创建对应的CalendarEvent并传入 icsManager.OutPutIcsFile(String filename,boolean useRule,List calendarEvents,int curWeek,OutPutListener listener) 这里有一个参数为useRule,为bool类型 true代表开启重复规则,false代表关闭 开启重复规则后,根据你提供的weeklist来进行判断 形如 [1,2,3,4,5,6]或者[2,4,6,8,10]或者[1,3,5,7,9]都可以支持规则导出 但如果是这种[1,2,3,4,6,8]不规则的,会自动按照重复逐一导出开启提醒的方式: icsManager.setAlarm_seconds(15); icsManager.setFalg_alarm(true);在回调函数中,onSuccess方法会传回来一个生成文件路径,由于该文件是保存在包名下的私有目录,因此不需要任何读写权限。 你也可以直接调用File来处理他。 项目github地址:https://github.com/paul623/EventReminder 具体实例请看项目内的代码。 EOF 本文作者:巴塞罗那的余晖本文链接:https://www.cnblogs.com/robotpaul/p/12726609.html
Django-rest-framework 是个什么鬼? 我们首先来回顾一下传统的基于模板引擎的 django 开发工作流: 绑定 URL 和视图函数。当用户访问某个 URL 时,调用绑定的视图函数进行处理。编写视图函数的逻辑。视图中通常涉及数据库的操作。在视图中渲染 HTML 模板,返回 HTTP 响应。其实,基于 django-rest-framework 的 RESTful API 的开发,过程是完全类似的: 绑定 URL 和视图函数。当用户访问某个 URL 时,调用绑定的视图函数进行处理。编写视图函数的逻辑,根据 HTTP 请求类型,对请求的资源进行相应操作,这个过程通常涉及数据库的操作。使用约定的资源描述格式(例如 XML 或者 JSON)序列化资源并将数据返回给客户端(通过 HTTP 响应)。对比发现,前两步几乎是完全相同的。不同点在于,在传统的基于模板引擎的开发方式中,资源使用 HTML 文档进行描述并返回给客户端,而在 RESTful API 的开发方式中,资源通常被描述为 JSON 或者 XML 的格式返回给客户端。 有的同学就要问了,虽然 django 的视图函数通常情况下返回 HTML 文档的响应,但是 django 也支持返回 XML 格式或者 JSON 格式的响应,那么为什么还要使用 django-rest-framework 呢? 事实上,的确能够在 django 中返回 JSON 或者 XML 格式的数据,但是 django 框架本身只提供了十分基础的功能。django-rest-framework 是基于 django 的拓展,专为 RESTful API 的开发而设计,提供了十分丰富的辅助类和函数,帮助我们方便地开发 API。下面就来简单介绍 django-rest-framework 为我们提供了哪些功能特性,这些功能和特性我们在接下来的实战中会进一步学习其用法,这里可以先从宏观层面,做一个简单的了解。 内容协商(Content Negotiation)。之前说过,在 RESFful 架构的系统中,资源以某种描述形式在客户端和服务器之间传递,django-rest-framework 根据客户端能够接受的资源格式,自动使用合适的资源描述工具,返回客户端可接受的资源。认证与鉴权(Authentication and Permission)。客户端对资源的操作通常是受限的,有些资源只能由经过身份认证或具有相应权限的用户才能操作,django-rest-framework 提供了丰富的认证类和鉴权类,帮助我们对用户的身份和权限进行校验。序列化(Serialization)。django 基于 Python 语言开发,因此资源通常由 Python 对象描述,那么在传递给客户端时,就要进行转换,例如将 Python 对象转换为 JSON 字符串,这个过程就叫做序列化。django 内置的序列化器功能有限,django-rest-framework 提供了功能更加丰富和强大的序列化器,让资源的序列化工作变得异常简单。各种通用视图(Generic Views)。django 针对 Web 开发中常见的处理逻辑,提供了各种通用视图函数,以提高代码的复用性,减少开发者的工作量。django-rest-framework 同样针对 RESTful API 开发中常见的处理逻辑,提供了各种通用视图函数。路由自动生成器(Router)。django-rest-framework 根据编写的视图函数,自动生成符合 RESTful 设计的 URL 路由。文档(Documentation)。django-rest-framework 基于 OpenAPI 模式自动生成 API 文档,无需我们手动编写和维护。除此以外,django-rest-framework 还提供了分页(Pagination)、API 版本控制(Versioning)、缓存(Caching)、限流(Throtting)等各种功能类。 在接下来的实战教程中,我们会逐一的学习并使用它们。 让我们正式开启 django-rest-framework 的学习之旅吧! 作者:HelloGitHub-追梦人物 转载地址https://www.cnblogs.com/xueweihan/p/12706064.html
.NET 下基于动态代理的 AOP 框架实现揭秘 Intro#之前基于 Roslyn 实现了一个简单的条件解析引擎,想了解的可以看这篇文章 https://www.cnblogs.com/weihanli/p/roslyn-based-condition-eval-engine.html 执行过程中会根据条件的不同会在运行时创建一个类,每一次创建都会生成一个新的程序集,我觉得这样实现的话可能会导致加载的程序集越来越多,虽然目前我们的使用场景下不会有很多,而且相同的条件只会生成一次,还是觉得这样不是特别好,此时想起来了一些 AOP 框架,Aspect.Core/Castle/DispatchProxy ,他们这些 AOP 框架会生成一些代码类,好像也没有生成很多额外的程序集,于是打算看看这些 AOP 框架的实现,看看它们是如何生成动态代理类的 动态代理实现原理#看了这三个 AOP 框架的实现代码之后,实现原理基本都是一样的 都是通过创建一个 DynamicAssembly 之后在这个 DynamicAssemly 中创建要动态生成代理类,通过 Emit 创建要生成动态代理类的方法/属性等 来个小示例#多说不如来点代码示例: Copyinternal class ProxyUtil{ private const string ProxyAssemblyName = "Aop.DynamicGenerated"; private static readonly ModuleBuilder _moduleBuilder; private static readonly ConcurrentDictionary<string, Type> _proxyTypes = new ConcurrentDictionary<string, Type>(); static ProxyUtil() { // 定义一个动态程序集 var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(ProxyAssemblyName), AssemblyBuilderAccess.Run); // 创建一个动态模块,后面创建动态代理类通过这个来创建 _moduleBuilder = asmBuilder.DefineDynamicModule("Default"); } public static Type CreateInterfaceProxy(Type interfaceType) { var proxyTypeName = $"{ProxyAssemblyName}.{interfaceType.FullName}"; var type = _proxyTypes.GetOrAdd(proxyTypeName, name => { // 定义要创建的类型,并实现指定类型接口 var typeBuilder = _moduleBuilder.DefineType(proxyTypeName, TypeAttributes.Public, typeof(object), new[] { interfaceType }); // 定义一个默认的构造方法 typeBuilder.DefineDefaultConstructor(MethodAttributes.Public); // 获取接口中定义的方法 var methods = interfaceType.GetMethods(BindingFlags.Instance | BindingFlags.Public); foreach (var method in methods) { // 在动态类中定义方法,方法名称,返回值和签名与接口方法保持一致 var methodBuilder = typeBuilder.DefineMethod(method.Name , MethodAttributes.Public | MethodAttributes.Virtual, method.CallingConvention, method.ReturnType, method.GetParameters() .Select(p => p.ParameterType) .ToArray() ); // 获取 ILGenerator,通过 Emit 实现方法体 var ilGenerator = methodBuilder.GetILGenerator(); ilGenerator.EmitWriteLine($"method [{method.Name}] is invoking..."); ilGenerator.Emit(OpCodes.Ret); // 定义方法实现 typeBuilder.DefineMethodOverride(methodBuilder, method); } return typeBuilder.CreateType(); }); return type; } }通过上面的定义我们可以创建一个简单的代理类,然后定义一个 ProxyGenerator 来创建代理 Copypublic class ProxyGenerator{ public static readonly ProxyGenerator Instance = new ProxyGenerator(); public object CreateInterfaceProxy(Type interfaceType) { var type = ProxyUtil.CreateInterfaceProxy(interfaceType); return Activator.CreateInstance(type); } }// 定义泛型扩展public static class ProxyGeneratorExtensions{ public static TInterface CreateInterfaceProxy<TInterface>(this ProxyGenerator proxyGenerator) => (TInterface)proxyGenerator.CreateInterfaceProxy(typeof(TInterface)); }使用示例: Copyvar testService = ProxyGenerator.Instance.CreateInterfaceProxy();testService.Test(); 可以看到这个类型就是我们动态创建的一个类型,输出结果也是我们定义在代理类中的结果 More#.NET 中的基于动态代理的 AOP 也是这样实现的,实现的原理大致就是这样,这个示例比较简单还没有涉及 AOP ,这只是一个简单的动态代理示例 ,AOP 只需要在原始方法执行的逻辑上包装一层拦截器增加对拦截器的处理和调用即可,暂时还没实现,后面有机会再分享 Reference#https://github.com/dotnetcore/AspectCore-Frameworkhttps://github.com/dotnetcore/AspectCore-Framework/blob/master/src/AspectCore.Core/Utils/ProxyGeneratorUtils.cshttps://github.com/castleproject/Corehttps://github.com/castleproject/Core/blob/master/src/Castle.Core/DynamicProxy/ModuleScope.cshttps://github.com/dotnet/runtime/blob/master/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cshttps://github.com/WeihanLi/SamplesInPractice/blob/master/AopSample/Program.cs作者: WeihanLi 出处:https://www.cnblogs.com/weihanli/p/12713908.html
.net 垃圾回收 垃圾回收器帮我们处理了内存中不在使用的对象,提高了机器的性能,让开发人员轻松了很多。 你真的了解垃圾回收吗? 或许你知道垃圾回收,听说过是通过标记回收,可是怎么标记回收呢就不是很清楚了,好吧,如果不清楚就继续往下看。如果你是大神对这块了如执掌,请直接跳过,欢迎来提不同的意见。 1、我们先来聊一下内存分配: 代码中声明变量是需要向内存申请地址的,内存呢又分托管堆和栈,我们今天主要聊的就是托管堆内存 啥事托管堆内存呢?想必各位也心中知道,不知道的自行百度谷歌去。 写代码中凡是需要使用new声明的变量都是引用类型变量,使用的都是托管堆内存地址,那声明了一个对象,需要分配多大的控件呢? 1.1、这个时候就需要计算类型的字段需要的字节数了 1.2、引用类型对象开销的字节数还需要(类型对象指针和同步索引块) 在32位应用中,这多出来的两个字段各需32位字节地址空间,所以每个对象需要多占用8个字节的地址控件 在64位应用中,这多出来的两个字段各需64位字节地址空间,所以每个对象需要多占用16个字节的地址控件 1.3、内存申请后,CLR会检查保留区是否能够提供分配对象所需的字节数,使用new 声明的对象会向托管堆请求地址分配,并返回对象地址,NextObjPtr指针会加上对象占据的字节数,得到一个新值 2、垃圾回收-Go Go Go 垃圾回收的基本逻辑:垃圾回收器会检查托管堆中是否又应用程序不再使用的任何对象,如果有,它们使用的内存就可以回收了。 回收之前的托管堆如下: 下面我们来聊一下标记回收的整个过程: 2.1、首先,应用有一组根(root)每个根都是一个存储位置,其中包含指向引用类型对象的一个指针,指针要么引用托管堆中的一个对象,要么为null 例如:类型中定义的任何静态字段被认为是一个根 任何方法参数或局部变量也被认为是一个根,只有引用类型的变量才被认为是一个根,值类型不能被认为是根。 2.2、垃圾回收的第一阶段,标记阶段: 这时,垃圾回收器会沿着线程栈上行以检查所有根,如果发现一个根引用了一个对象,就在对象 “同步索引块”上开启一位---标记, 以递归的方式遍历所有可达的对象。如果垃圾回收器试图标记一个先前标记过的对象,就会停止沿这个路径走下去。 这个行为有两个目的: 1、垃圾回收器不会多次遍历一个对象,所以性能得到显著增强 2、如果对象存在循环链表,可以避免无线循环。 检查完所有的根之后,堆中将包含一组已标记和未标记的对象,已标记的对象是代码可达的对象,而未标记的对象是不可达的,不可达的对象被认为是垃圾,它们占用的内存是可以被回收的 垃圾回收之后的托管堆如下: 2.3、垃圾回收的第二阶段,压缩阶段: 这个时候该回收内存空间已经都回收了,空出来的内存可能是前头一块,中间一块,后边又一块。 垃圾回收器线性遍历堆,以寻找未标记对象的连续内存块,如果发现内存块比较小,则忽略,如果发现大的,可用的连续内存块,垃圾回收器会把非垃圾的对象移动到这里以压缩堆。 参考:CLR Via C#(第三版) 原文地址https://www.cnblogs.com/miao817/p/12705140.html
在.net core中完美解决多租户分库分表的问题 前几天有人想做一个多租户的平台,每个租户一个库,可以进行水平扩展,应用端根据登录信息,切换到不同的租户库 计划用ef core实现,他们说做不出来,需要动态创建dbContext,不好实现 然而这个使用CRL很轻松就能解决了 以下为演示数据库,有两个库testdb和testdb2,查询结果如下 目标: 根据传入登录信息连不不同的库,查询返回结果,如登录人为01,返回d1.default,登录人为02 返回 d2.default 实际上这个需求就是分库分表的实现,通过设置数据库/表映射关系,根据传入的定位数据进行匹配,找到正确的库表配置,生成数据访问对象 以core控制台程序为例 class Program { static IServiceProvider provider; static Program() { var services = new ServiceCollection(); services.AddCRL<DBLocationCreator>(); services.AddScoped<Code.Sharding.MemberManage>(); provider = services.BuildServiceProvider(); provider.UseCRL(); } static void Main(string[] args) { label1: var instance = provider.GetService<Code.Sharding.MemberManage>(); var data = new Code.Sharding.MemberSharding(); data.Code = "01"; instance.SetLocation(data); var find1 = instance.QueryItem(b => b.Id > 0)?.Name; Console.WriteLine($"定位数据输入{data.Code},查询值为{find1}"); data.Code = "02"; instance.SetLocation(data); var find2 = instance.QueryItem(b => b.Id > 0)?.Name; Console.WriteLine($"定位数据输入{data.Code},查询值为{find2}"); Console.ReadLine(); goto label1; } } 上面代码中,通过SetLocation方法传入定位数据Code,通过QueryItem方法查询出数据并打印出来 通过services.AddCRL()注入定位配置,DBLocationCreator继承了接口IDBLocationCreator 这里完全符合core注入规范,可以通过配置或数据库存储动态读取定位设置 public class DBLocationCreator : IDBLocationCreator { ISettingConfigBuilder _settingConfigBuilder; public DBLocationCreator(ISettingConfigBuilder settingConfigBuilder) { _settingConfigBuilder = settingConfigBuilder; } public void Init() { //自定义定位 _settingConfigBuilder.RegisterLocation<Code.Sharding.MemberSharding>((t, a) => { var tableName = t.TableName; var dbName = a.Code == "02" ? "testdb2" : "testdb"; var dataBase = $"Data Source=.;Initial Catalog={dbName};User ID=sa;Password=123"; //返回定位库和表名 return new CRL.Sharding.Location(dataBase, tableName); }); _settingConfigBuilder.RegisterDBAccessBuild(dbLocation => { var connectionString = "Data Source=.;Initial Catalog=testdb;User ID=sa;Password=123"; if (dbLocation.ShardingLocation != null) { connectionString = dbLocation.ShardingLocation.DataBaseSource; } return new CRL.DBAccessBuild(DBType.MSSQL, connectionString); }); } } 在Init方法里,实现了两个操作,通过RegisterLocation定义如何根据定位数据Code,返回不同的库/表 通过RegisterDBAccessBuild实现数据访问 运行测试程序,结果输出为 上面代码通过自定义定位参数和定位规则,没有任何耦合,调用也很简单,完美达到了预期效果 测试代码地址:https://github.com/CRL2020/CRL.NetStandard/tree/master/Test/CRLCoreTest
真没想到,Springboot能这样做全局日期格式化,有点香! 说在前边最近部门几位同事受了一些委屈相继离职,共事三年临别之际颇有不舍,待一切手续办妥帖,寒暄过后送他们出公司,几个老哥临别时冲我鬼魅一笑,我顿时心里一紧有种不好的预感,这事绝对没有这么简单。等我接手这几个大佬的项目后,应验了我的预感,此刻我居然有点后悔,为啥送别之时没揍他们一顿!哈哈哈~ 而这种打人的冲动,在我开始优化几位老哥的项目时候,变得越来越强烈。 有个坑技术部每个月都会组织一下代码走查及优化,以前是各自审查优化自己的项目,如今几位老哥的离职他们的项目就落到了我的头上。对于程序员来说最痛苦的事情就是接手别人的项目,还要做优化改造,因为这一点也不比重构一遍项目简单。不过,军令在前,没办法硬着头皮上吧! 第一个优化的点就让我有点崩溃,这几个大佬的技能能力很强,一直都是我学习的榜样,但在项目里几乎所有的日期格式化都这样用 SimpleDateFormat ,像如下代码这样实现,emm~ ,受过伤的男人怎么啥事都做的出来,哈哈哈~ SvcOrderDailyStatisticsPo orderDailyStatisticsPo = new SvcOrderDailyStatisticsPo();SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");Date stationTime = dateFormat.parse(dateFormat.format(svcWorkOrderPo.getPayEndTime()));orderDailyStatisticsPo.setStatisticalDate(stationTime);而且项目中的时间和日期API用的也比较混乱,考虑到 java.util.Date 和 java.util.Calendar 不支持时区,且非线程安全,对于日期的计算相对繁琐,技术部一致要求用JDK1.8以后的 java.time LocalDateTime。但不少人还是在用 java.util.Date 和 java.util.Calendar 处理日期。 优化方案时间格式化是使用频率非常高的,如何让时间格式化变得既简单又不用重复造轮子,那么就应将它抽象出来,作为全局的日期格式化处理,下面就结合实践简单介绍下几种优化方案。 测试地址:http://127.0.0.1:8080/timeTest @GetMapping("/timeTest") public OrderInfo timeTest() { OrderInfo order = new OrderInfo(); order.setCreateTime(LocalDateTime.now()); order.setUpdateTime(new Date()); return order; } 1、@JsonFormat注解使用@JsonFormat注解格式化时间,应该算是一个基本操作了,大部分开发者都应用此种方式,简单方便。 /** @Author: xiaofu @Description:*/ public class OrderInfo { @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") private Date updateTime; public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; } public Date getUpdateTime() { return updateTime; } public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } }测试一下结果,发现 Date 类型和 LocalDateTime 类型都格式化成功,但还是有个问题,这样做仍然比较繁琐,每个实体类的日期字段都要加@JsonFormat注解,重复的工作量也不小。接着往下看~ 2、全局配置 (1)Springboot 已经为我们提供了日期格式化 ${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss},这里我们需要进行全局配置,配置比较简单,也无需在实体类属性上添加@JsonFormat注解。 /** @Author: xiaofu @Description:*/ public class OrderInfo { //@JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; //@JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") private Date updateTime; public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; } public Date getUpdateTime() { return updateTime; } public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } } 只需要用@Configuration定义一个配置类,注入两个Bean即可完成全局日期格式化处理,这种方式也是当前我项目中正在用的方式。 /** @Author: xiaofu @Description:*/ @Configurationpublic class LocalDateTimeSerializerConfig { @Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}") private String pattern; @Bean public LocalDateTimeSerializer localDateTimeDeserializer() { return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern)); } @Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { return builder -> builder.serializerByType(LocalDateTime.class, localDateTimeDeserializer()); } }这种方式可支持 Date 类型和 LocalDateTime 类型并存,那么有一个问题就是现在全局时间格式是yyyy-MM-dd HH:mm:ss,但有的字段却需要yyyy-MM-dd格式咋整? 那就需要配合@JsonFormat注解使用,在特定的字段属性添加@JsonFormat注解即可,因为@JsonFormat注解优先级比较高,会以@JsonFormat注解标注的时间格式为主。 3、全局配置 (2)这种全局配置的实现方式与上边的效果是一样的,不过,要注意的是使用这种配置后,字段手动配置@JsonFormat注解将不再生效。 /** @Author: xiaofu @Description:*/ public class OrderInfo { //@JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; //@JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") private Date updateTime; public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; } public Date getUpdateTime() { return updateTime; } public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } } /** @Author: xiaofu @Description:*/ @Configurationpublic class LocalDateTimeSerializerConfig { @Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}") private String pattern; @Bean @Primary public ObjectMapper serializingObjectMapper() { ObjectMapper objectMapper = new ObjectMapper(); JavaTimeModule javaTimeModule = new JavaTimeModule(); javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer()); javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer()); objectMapper.registerModule(javaTimeModule); return objectMapper; } public class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> { @Override public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeString(value.format(ofPattern(pattern))); } } public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> { @Override public LocalDateTime deserialize(JsonParser p, DeserializationContext deserializationContext) throws IOException { return LocalDateTime.parse(p.getValueAsString(), ofPattern(pattern)); } } }总结分享了一个Springboot项目开发过程中的一个小技巧,也顺便吐槽一下项目优化中遇到的坑,优化别的人的代码虽然是一件比较痛苦的事情,但在这个过程中确实能学习到很多技巧,对个人的技能提升也是很有帮助,因为都是些能够实实在在提高开发效率的干货。 原文地址https://www.cnblogs.com/chengxy-nds/p/12691541.html
【Docker笔记】使用Dockerfile创建镜像 在前面我们讲解了基于已有的镜像容器创建和基于本地模板导入两种方式来创建镜像,在这里我们就来说说第三种创建镜像的方式。Dockerfile是一个文本格式的配置文件,我们可以通过Dockerfile快速创建自定义的镜像。 一、基本结构Dockerfile是由多行命令语句组成的,并且在文件中支持以 # 开始的注释行。我们一般将Dockerfile文件分为四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。其中,第一行(不包含注释行)必须指定基于的基础镜像,例如:FROM ubuntu。之后可以是维护者信息,如:MAINTAINER gongziqi 14155830994@qq.com。再往后可以是镜像的操作指令,如: RUN echo "deb http://archive.ubuntu.com/ubuntu/ raring main universe" >> /etc/apt/sources.list RUN apt-get update && apt-get install -y nginx RUN echo "ndaemon off;" >> /etc/nginx/nginx.conf 最后可以是容器启动时执行指令,如:CMD /usr/sbin/nginx。 0. 在ubuntu镜像的基础上,安装inotify-tools/nginx/apache2/openssh-server等,创建新的Nginx镜像 # Nginx # VERSION 1.0 FROM ubuntu MAINTAINER gongziqi 1415583094@qq.com RUN apt-get update && apt-get install -y inotify-tools nginx apache2 openssh-server 1. 在ubuntu镜像基础上,安装firefox/vnc。启动后,用户可通过5900端口通过vnc方式使用firefox # FireFox over VNC # VERSION 1.0 FROM ubuntu RUN apt-get update && apt-get install -y x11vnc xvfb firefox RUN mkdir ./.vnc RUN x11vnc -storepasswd 123456 ~/.vnc/passwd RUN bash -c 'echo "firefox" >> /.bashrc' EXPOSE 5900 CMD ["x11vnc","-forever","usepw","-create"] 二、指令1、FROM语法为:FROM 或 FROM :。若在同一个Dockerfile中创建多个镜像,可以使用多个FROM即每个镜像一个。 2、MAINTAINER语法:MAINTAINER ,用来指定维护者信息。 3、RUN语法:RUN 或 RUN ["executable","param1","param2"]。第一个将在shell终端中运行命令,即 /bin/bash -c;第二个则使用 exec 执行。每条RUN指令将在当前镜像基础上执行指定指令,并提交为新的镜像。若命令行太长可以使用 来换行书写。 4、CMD支持三种方式: CMD ["executable", "param1", "param2"],使用 exec执行,推荐方式。CMD command param1 param2,在 /bin/sh 中执行,提供给需要交互的应用。CMD ["param1","param2"],提供给 ENTRYPOINT的默认参数。指定启动容器时执行的命令,每个Dockerfile只能由一条 CMD命令。若指定多条,则只有最后一条有效。若在启动容器时,指定了运行的命令,则CMD命令将会被覆盖。 5、EXPOSE语法:EXPOSE [ ...],如:EXPOSE 22 80 8443,即告诉Docker服务器容器暴露的端口,这些端口可供互联使用。此时在容器启动中,可使用 -P 来随机指定一个端口,也可使用 -p 来指定具体的端口映射。 6、ENV语法:ENV 。指定一个环境变量,会被后面的RUN指令使用,并在容器运行时保持。如: ENV PG_MAJOR 9.3ENV PG_VERSION 9.3.4RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && ...ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH7、ADD语法:ADD 。将复制指定的src到容器中的desc。其中,src 可以是Dockerfile所在目录的一个相对路径(文件/目录);也可为一个URL;还可为一个tar文件。 8、COPY语法:COPY 。复制本地主机的src(Dockerfile所在目录的相对路径)到容器中的desc。当目标路径不存在时,将自动创建。当使用本地目录为源目录时,推荐使用COPY,不推荐ADD。 9、ENTRYPOINT语法:ENTRYPOINT ["executable","param1","param2"] 或 ENTRYPOINT command param1 param2(shell中执行)。配置容器启动后执行的命令,并且不可被docker run 提供的参数覆盖。每个Dockerfile只能有一个ENTRYPOINT,当指定多个时,只有最后一个有效。 10、VOLUME语法:VOLUME ["/data"]。创建一个可以从本地主机或其他容器挂载的挂载点,一般用来存放数据库和需要保持的数据等。 11、USER语法:USER daemon。指定运行容器时的用户名或UID,后续的RUN指令也会使用指定用户。当服务不需要管理员权限时,可通过该命令指定运行用户,并且可以在之前创建所需要的用户。如:RUN groupadd -r postgres && useradd -r -g postgres postgres。若此时需要临时获取管理员权限,则可使用gosu,不推荐使用sudo。 12、WORKDIR语法:WORKDIR /path/to/workdir。为后续的 RUN、CMD、ENTRYPOINT指令配置工作目录。可使用多个WORKDIR,若后面的WORKDIR指定的是相对路径,则是基于前一个WORKDIR指定的路径。如: WORKDIR /aWORKDIR bWORKDIR cRUN pwd 结果为:/a/b/c 13、ONBUILD语法:ONBUILD [INSTRUCTION]。配置当所创建的镜像作为其他新创建镜像的基础镜像时,所执行的操作指令。如:Dockerfile使用如下的内容创建了镜像 image-A。 [...]ONBUILD ADD . /app/srcONBUILD RUN /usr/local/bin/python-build --dir /app/src[...]若基于image-A创建新的镜像时,新的Dockerfile中使用 FROM image-A指定基础镜像时,会自动执行 ONBUILD 指令内容,即等价于在 FROM指令后添加了以上的两条指令。在使用ONBUILD指令的镜像,我们推荐在标签中注明,如:ruby:1.9-onbuild。 三、创建镜像在编写完Dockerfile之后,可以通过 Docker build 命令来创建镜像。语法为:docker build [选项] 路径,即读取指定路径下的Dockerfile,并将该路径下所有内容发送给Docker服务端,由服务端来创建镜像。因此我们建议放置Dockerfile的目录为空目录。若为非空目录,希望忽略路径下的某些目录或文件,可通过 .dockerignore文件来配置。 指定Dockerfile路径所在路径为 /tmp/docker_builder/,希望生成镜像标签为 build_repo/first_image, # 在标签命名时需要注意,所有字母必须为小写 $ sudo docker build -t build_repo/first_image /tmp/docker_builder/四、一个实例根据以上信息,我们来自己创建一个Nginx镜像。 0. 创建Dockerfile所在目录 $ sudo mkdir /opt/tmp_dockerbuilder 1. 创建Dockerfile文件并编写内容 $ sudo vim Dockerfile # Nginx # VERSION 1.0 FROM ubuntu MAINTAINER gongziqi 1415583094@qq.com RUN apt-get update && apt-get install -y inotify-tools nginx apache2 openssh-server 2. 查看当前本地镜像 $ sudo docker imagesREPOSITORY TAG IMAGE ID CREATED SIZE 3. build Dockerfile文件,创建镜像 $ sudo docker build -t nginx/mynginx /opt/tmp_dockerbuilder/DockerfileSending build context to Docker daemon 2.048kBStep 1/3 : FROM ubuntu ---> 4e5021d210f6Step 2/3 : MAINTAINER gongziqi 1415583094@qq.com ---> Running in 8dc5269da475Removing intermediate container 8dc5269da475 ---> e27af3f3a447Step 3/3 : RUN apt-get update && apt-get install -y inotify-tools nginx apache2 openssh-server ---> Running in 2ec0f5b9fc81 ........ ........ ........Successfully built d27de6c7896dSuccessfully tagged nginx/mynginx:latest 4. 再次查看本地镜像 $ sudo docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEnginx/mynginx latest d27de6c7896d 6 minutes ago 275MB 5. 以当前创建的镜像运行容器 $ sudo docker run -ti d27de6c7896d /bin/bashroot@a505965d1b84:/# 6. 查看当前的容器中是否有我们刚才需要安装的相关软件 root@a505965d1b84:/# find / -name apache2/etc/cron.daily/apache2/etc/init.d/apache2/etc/logrotate.d/apache2/etc/apache2/etc/ufw/applications.d/apache2find: '/proc/1/map_files': Operation not permittedfind: '/proc/12/map_files': Operation not permitted/usr/lib/apache2/usr/sbin/apache2/usr/share/bug/apache2/usr/share/doc/apache2/usr/share/lintian/overrides/apache2/usr/share/apache2/var/cache/apache2/var/lib/apache2/var/log/apache2 7. 查看ubuntu镜像运行的容器中是否有apache2 $ sudo docker run -tid ubuntu /bin/bash$ sudo docker exec -ti 6df8ff14f57f /bin/bashroot@6df8ff14f57f:/# find / -name apache2find: '/proc/1/map_files': Operation not permittedfind: '/proc/10/map_files': Operation not permittedfind: '/proc/19/map_files': Operation not permittedroot@6df8ff14f57f:/# 说明:我们会发现刚才的配置的 apache2 软件已被安装,而基于的 ubuntu镜像本身是没有apache2的 原文地址https://www.cnblogs.com/chuanqi1415583094/p/12685136.html
mysql中关于exists的讲解 我认为exists语法是mysql中一个很强大的工具,可以简单地实现某些复杂的数据处理。 下面我谈谈与exists有关的三个方面。 all 与 any首先,看到了exists,难免还会想到all和any,它们比exists容易理解一些。all 和 any都能让一行数据与多行数据进行比较,这是它们的主要功能。 create table T(X int);insert into T(X) values(1),(2),(3),(4); eg.1 select from T where X > all( select from T where X < 3 ); #输出3,4 eg.2 select from T where X > any( select from T where X > 1 ); #输出3,4先看eg.1, 显然select * from T where X < 3结果是1,2;而all要求存在X大于集合{1,2}内的任意元素,即3,4。 同理,对于eg.2,select * from T where X > 1结果是2,3,4;any的要求是存在X大于集合{2,3,4}内的某个元素即可,即3,4。 划分表在说exists之前,再看看一个比较特别的语句,关于表(table)的“划分”用法。 eg.1 fruitTable Id Name Class Count Date 1 苹果 水果 10 2011-7-1 1 桔子 水果 20 2011-7-2 1 香蕉 水果 15 2011-7-3 2 白菜 蔬菜 12 2011-7-1 2 青菜 蔬菜 19 2011-7-2 现在要求进行筛选,条件是Id唯一,Date选最近的一次 这种筛选条件潜藏着对于表的划分要求。以fruitTable为例,需要划分为2个子表,Id为1的为一个子表、Id为2的为另一个子表,再从各自子表里面选出时间最大的那个元组。 先看看下面一个错误的解法 SELECT DISTINCT Id, Name, Class, Count, Date FROM fruitTable t1 WHERE (Date IN (SELECT MAX(Date) FROM fruitTable t2 GROUP BY Id)); 结果 1 桔子 水果 20 2011-7-2 1 香蕉 水果 15 2011-7-3 2 青菜 蔬菜 19 2011-7-2这周解法在逻辑上有漏洞。它将不同Id的最大时间混在了一起,没有真正地划分表格。 再来看看正确的解法 划分表格的思路是正确的,但问题是怎么划分,如果另外创建2个新的table,那这样显然太麻烦了,于是有了下面这种写法。 SELECT DISTINCT Id, Name, Class, Count, Date FROM fruitTable t1 WHERE (Date = (SELECT MAX(Date) FROM fruitTable t2 WHERE t2.Id=t1.Id)); 注意WHERE t2.Id=t1.Id 很巧妙地 对表t2 基于t2.Id=t1.Id这个标准 进行了划分。可以推导一下,比如遍历表t1,先是第1个元组:1 苹果 水果 10 2011-7-1, 可以知道t1.Id=1, 带入第2个select: (SELECT MAX(Date) FROM fruitTable t2 WHERE t2.Id=1) , 观察这个select语句的筛选条件WHERE t2.Id=1, 发现它的范围限定在了Id为1的元组内,聚集函数MAX(Date)返回Id为1的所有元组中Date最大的值(2011-7-3)。 因此对于表t1, 当t1.Id=1时,只有Date=2011-7-3的元组才会被选出来;而当tl.Id=2时,第2个select又变为SELECT MAX(Date) FROM fruitTable t2 WHERE t2.Id=2, 返会所有Id=2的元组中Date的最大值(2011-7-2)。 可以发现,表t2是受t1.Id控制的,根据t1.Id的不同而被划分为不同的子表,这就是表的划分,并且不需要另外创建新的表。 exists先说说exists的基本用法 create table R( X int, Y varchar(5), Z varchar(5) ); create table S( Y varchar(5), Z varchar(5), Q int ); insert into R(X,Y,Z) values( 1,'a','A' ),( 1,'b','B' ),( 1,'a','B' ),( 1,'c','C' ),( 2,'a','B' ),( 2,'b','B' ),( 2,'c','A' ),( 3,'z','Z' ); insert into S(Y,Z,Q) values( 'b','B',1 ),( 'a','B',2 ); select from R where exists( select from S where S.Y='b' and R.Y=S.Y ); 结果 '1', 'b', 'B''2', 'b', 'B'对于exists可以先简单地理解为if判断。比如语句select from R where exists( select from S where S.Y='b' and R.Y=S.Y );就可以理解为 从表R中筛选出满足条件 S.Y='b' and R.Y=S.Y (select * from S where S.Y='b' and R.Y=S.Y) 的元组。 这个性质可以看出2个特性 首先exists()括号内的表不会影响最终返回的结果。比如上面的例子,返回的结果始终是关于表R的元组,和表S没有任何关系对于exists()语句,关键的是括号内的where子句。对于exists( select * from S where S.Y='b' and R.Y=S.Y ) 这种语句,可以直接当作 if( S.Y== 'b' and R.Y ==S.Y )。当然也不是说select不重要,比如exists( select 1 from S where S.Y='b' and R.Y=S.Y )是永远为真的条件。理清上面2点,我们就更能意识到exists非常像是一个关于条件判断的语句。 下面例子类似 选了张三老师课的学生 select distinct sc.sid from sc where exists ( select * from course c,teacher t where sc.cid = c.cid and c.tid = t.tid and t.tname = "张三"); 但仅仅只有exists还不够,因为很多其它语句也能实现这个功能,真正强大的是not exists。对于存在exists只是一个元组与某个局部作比较,因为只要存在即可。而对于不存在,却是一个元组和整体做比较,因为要确定不存在,就必须遍历所有。在这方面来说,not exists比exists更强大。 找最值 SELECT DISTINCT Id, Name, Class, Count, Date FROM fruitTable t1 WHERE (Date = (SELECT MAX(Date) FROM fruitTable t2 WHERE t2.Id=t1.Id)); 用not exists SELECT DISTINCT Id, Name, Class, Count, Date FROM fruitTable t1 WHERE NOT EXISTS( SELECT * FROM fruitTable t2 WHERE t2.Id=t1.Id and t2.Date > t1.Date ); 这里not exists同样可以看作not if,关键是明白哪部分条件被否定(not)。根据之前的理论,这里条件明显是t2.Id=t1.Id and t2.Date > t1.Date , 而t2.Id=t1.Id不能作为否定的对象,因为这是必然存在的(自己想想,t1和t2内容一样),用来限定表t2的范围(即之前说的划分子表),再看t2.Date > t1.Date,这才是否定的部分,即对于t2中Id为t1.Id的所有元组的Date都不大于t1.Date,而此时的t1.Date也即最大值。 嵌套not exists 还有更复杂的情况,多层not exists嵌套使用。比如实现关系代数里的除法运算。 表R,S的定义上面已经给出 下面计算 R除以S select distinct R1.x from R R1 where not exists ( select * from S where not exists ( select * from R R2 where R1.X=R2.X and R2.Y=S.Y and R2.Z=S.Z )); 这里有3个select,2个not exists。最里面的not exists是用来否定R2.Y=S.Y and R2.Z=S.Z (因为R1.X=R2.X一定成立,这个是用来划分子表的), 最外层的not exists就用来表示不存在这个意思,你会发现最后这个句子表达的意思就是关系代数里面除法的定义。 使用联合来解决exists问题因为MySQL每次的操作都是基于行的,当涉及到表与表之间类似集合的关系时,处理起来比较麻烦。比如下面这个问题。 insert into R(X,Y) values( 1,'a' ),( 1,'b' ),( 1,'B' ),( 1,'C' ),( 2,'A' ),( 2,'c' ),( 3,'z' ); insert into S(Y,Q) values( 'b',1 ),( 'B',2 ); 问题:表R内,对于X值相同的行组成一组(或叫集合)。在这样的每组元素中,要求R(Y)中不能出现与S(Y)相同的值,求这样的组的X值有哪些。 这种问题是关于集合之间的关系,不同于 一行与一个集合之间的关系。 下面运用之前讲的not exists来求解 select distinct X from R R1 where not exists ( select * from R R2 where R2.X=R1.X and R2.Y in (select distinct Y from S)); 下面来介绍另外一种方法,联合。 仔细观察可以发现R和S之间是有关系的,因此可以将它们进行自然连接,这样就直接得到了所有R(Y)=S(Y)的值。 select distinct X from R where X not in (select distinct X from R,S where R.Y=S.Y);原文地址https://www.cnblogs.com/friedCoder/p/12678145.html
JVM基础结构与字节码执行引擎 JVM基础结构JVM内部结构如下:栈、堆。 栈JVM中的栈主要是指线程里面的栈,里面有方法栈、native方法栈、PC寄存器等等;每个方法栈是由栈帧组成的;每个栈帧是由局部变量表、操作数栈等组成。 每个栈帧其实就代表一个方法 堆java中所有对象都在堆中分配;堆中对象又分为年轻代、老年代等等,不同代的对象使用不同垃圾回收算法。 -XMs:启动虚拟机预留的内存-Xmx:最大的堆内存 一、堆的分代假设根据研究表明,堆中对象大部分都是创建后,立马就可以被销毁的。如: 为了优化堆中的内存,将堆中对象分为不同代。在年轻代中,GC发生比较频繁;在老年代中,GC发生比较少。 二、堆的分代年轻代:Young Generation老年代:Old Generation/Tenured永久代:Permanent Generation永久代在Java虚拟机规范中是没有的,但是Host Spot虚拟机中有。 三、方法区方法区被所有线程共享;方法区是用来存储编译后的代码,即存储每个类的运行时常量池、字段和方法。方法区在虚拟机启动时创建;虽然方法区在逻辑上是堆的一部分,但在一些简单的实现中,方法区可以选择不进行垃圾回收和紧凑化。 方法区在java8的变化java7之前:方法区的实现:永久代,是作为堆的一部分;java8之后:方法区的实现:metaspace,是堆外的内存;1、为什么要这样改变?因为java可以动态加载字节码信息,这样方法区就会慢慢的挤占堆中内存。为了避免与堆争抢内存,java8将方法区的实现移至堆外。2、方法区、永久代、MetaSpace的区别?方法区是java虚拟机规范所规定的一个概念。其中java7实现方法区的地方称为永久代;java8实现方法区的地方称为MetaSpace 字节码文件的结构java程序在运行的时候,将源码编译成字节码,字节码在不同系统上的JVM翻译成对应的机器码。这是Java平台无关性的基础。 但是,编译后的字节码是如何读取到JVM中的?字节码执行引擎是如何识别、执行指令? 1、如何查看字节码文件 classpy工具IDEA的jclasslib Bytecode viewer插件2、字节码文件结构一个字节码文件包含以下部分: (1)magic:0xCAFEBABEclass文件的magic code,用于标识该文件是class文件。 (2)minor_version、major_version用于标识该class文件的版本,防止高版本的class文件被低版本的JVM读取并执行。 (3)constant_pool:常量池用于存储该class文件经常被使用的信息,优化内存。比如说System.out.print() (4)access_flag表示这个类得访问权限,对应到java源码就是public、final之类的 字节码执行引擎这里以一个线程为例。一般来说,一个方法栈最底层的栈帧都是Thread.run方法。当一个线程准备调用另一个方法时,会先将实参拷贝一份到新栈帧的局部变量表里,然后再执行代码。1、局部变量表每次调用新方法时,会默认将当前对象的地址this作为局部变量表的第一个参数;后面存放传过来的参数。这与javascript的做法很相似。 2、方法调用的相关指令 invokevirtual:一般实例方法,有多态;invokeinterface:接口方法,有多态invokestatuc:静态方法,无多态invokespecial:特殊方法,无多态invokedynamic:动态调用,JDK7新增,方法无需在编译时确定3、方法调用的过程(1)在开始时 方法栈新增一个栈帧;实例方法的this、参数放到局部变量表中;开始新栈帧中字节码的执行;(2)在返回时 将返回值放在调用者方法栈帧中的操作数栈上;(3)在异常出现时 寻找匹配的异常处理代码(4)在finally时 为每个分支新增一个跳转4、为什么Mockito、EasyMock无法对private、static方法进行mock?因为他们mock方法是通过覆盖这些方法来实现的,而private、static没法被覆盖。PowerMock是通过修改字节码文件达到mock私有、静态方法的。 原博客地址 转载地址https://www.cnblogs.com/fourther/p/12673213.html
为什么我建议每个开发人员都需要学Python? 世界上只有几种编程语言提供多种功能。 在当今世界,开发人员必须能去构建各种类型的应用程序,所以多学习一种多功能开发语言是有必要的。 虽说php是世界上最好的语言,但这也不妨碍Python成为通用编程开发语言,它可以让开发人员构建各种类型的应用程序。 Web解决方案Python被认为是Web开发人员的首选语言之一。 因为有着许多由Python设计和支持的互联网巨头背书,如谷歌、YouTube、Netflix、Instagram、Dropbox、雅虎、Reddit、Spotify等等。Python为它们构建服务器端应用程序。 因此,作为一名Web开发人员,学习并掌握它将可能对你未来的工作带来一些帮助。 Python还拥有了很多很棒的库,它们包括了例如:JSON、HTML、XML、beautifulSoup、Feedparser、电子邮件处理、Request等。同时Python还有一些很好用的框架, 例如:Django,Pyramid,Flask,Tornado,web2py,CherryPy,Falcon等。 2.数据科学在当下的世界里,数据成为了工作和生活中至关重要的一部分,能科学而有效的组织数据成为了为运营各个方面提供价值重要基础。 而Python在数据科学方面有一些大量的库可供数据挖掘、分析和可视化使用,数据挖掘和分析的库,例如: SciPy、Pandas、NumPy等等。数据可视化方面,例如: Matplotlib,Datashader Seborn,Basemap,Cartopy,Ridge Map,GeoPlot,Holoview,Decida等等。 这些库能为Python开发人员在进行数据挖掘、数据分析及数据可视化期间提供不少便利,正因为它的易用性,并且具有与其他难以学习的编程语言相比编码更简单的特点,所以Python成为了众多数据科学家的首选。 3.可行性Python是一种高级语言。对大多数语言来讲,语言可分为两种,一种是高级编程语言,而另一种是低级编程语言。 这里的低级和高级并不是字面上高级低级的意思,而是指面向的阅读对象。 高级语言与人类语言相近,开发度程序快,可以通用不用的计算机。编译后的程序在运行前会翻译成机器语言知让计算机能够识别。低级语言能直接对处理器等硬件进行访问和控制,进行低层操作,功能强大,复杂,适合开发驱动道等低层程序。 在这,与人类语言更接近的语言更容易被人理解。最重要的是,Python除了具备了可读性高的特点外还有代码精简的特点。 这点是Python能更广泛普及的一个很重要因素。 4.学习简单目前学习Python的人正在不断发展壮大。在这背后是Python本身不是一种非常复杂的语言,因为首先Python是一门更接近人类的语言,它的语言逻辑更接近人而非机器;然后,它代码十分简洁,几行代码就能完成别的语言十几行才能完成的工作;最后,丰富的内置函数使得很多工作直接调用函数即可完成。 因为上述特点,它已经成为了美国顶尖大学中最受欢迎的入门教学语言。另外, 它被排名前十的计算机科学系中的八个用来在计算机科学入门课程中教授。 5.人工智能与机器智能以前我们看过的科幻电影中的机器人曾让我们惊叹不已,而现在,这些虚构的故事很快就会变成现实。因为人工智能(AI)和机器智能(MI)的出现,这已经成为可能。 机器学习将帮助我们观察并提供大量数据的细致分析结果。因为要获取、分析和处理收集到的数据,我们需要一种编程语言去完成这项工作,而几乎所有的开发人员都选择了Python。这是因为Python出色的稳定性和易用性, 另外Python提供了更多好用的的库来协助完成,如: 用于科学技术计算的SciPy用于数学函数和科学计算的NumPy用于机器学习和神经网络的库Keras和Scikit-learn用于机器学习和符号数学库的TensorFlow6.适应性Python被认为是最灵活的编程语言之一。之所以说,是因为有以下两个原因: 与其他编程语言集成Python能够将其他语言的不同大型组件组合在一起,形成一个整体。在Python的帮助下,可以以更好的方式编写应用程序,因为它使不同类型的程序员可以在一个项目上共同工作。 让我们举个例子。如果我们计划构建数据科学应用程序,C/C++开发人员可以研究和执行算法,而在同一项目的数据科学家将能够通过编写Python程序,测试和使用所开发的算法。这就是集成的工作方式。 平台独立性Python是一种跨平台的编程语言。这意味着Python应用程序可以同时运行在例如Windows,Linux / Unix,Mac等各种操作系统上。 这个功能节省了购买和学习新操作系统所涉及的额外费用。因此,这是Python的一个值得称赞的特征。 7.多种功能Python可以称为通用编程语言,是因为它各个领域和方面都有帮助。 除了已经提到的数据科学,Web解决方案以及人工智能和机器智能之外,还有以下内容: 桌面图形用户界面应用程序我们能够使用Python开发桌面图形用户界面应用程序。如果要创建GUI应用程序,可以使用Python中已有的GUI框架,例如: PyQT,Tkinter,PyGUI,WxPython,Kivy等。其中,Kivy是构建多点触控应用程序的首选。 商业和企业应用使用Python,您可以制作商业和企业相关的应用程序,从而对整个业务提供帮助。类似的应用是ERP和电子商务系统。 甚至可以在组织内使用的应用程序也可以使用我们最喜欢的语言进行开发。我们可以以Picalo,Odoo和Tryton为例。 3D图形和游戏因为有了Python的帮助,你还可以创建使用3D图形的应用程序。而在其中一些很棒的框架可以帮助你完成构建游戏和3D渲染,例如PyKyra和PyGame。 CAD应用CAD能帮助我们以更好的方式进行可视化。但因为必须对对象表现的更出色,所以这对开发人员来说很痛苦的。但Python可以使开发者变得轻松起来,Fandango就是这种类型的一个例子。 音频和视频应用使用Python,你可以创建能够与音频和视频等多媒体交互的应用程序。我们有一些使用类似Python的Cplay和TimPlayer开发的应用程序示例。因此,使用Python可以创建出全能播放器。 8.框架和库首先我们需要先去了解框架和库之间的区别。 构架 应用程序由大量代码组成。其中如果应用程序很庞大,那么它们将需要大量的编码。其中,有些组件通常用于构建网站。 Web框架包含可随时使用的代码和结构。这将有助于使编程过程标准化。 库库是程序通常使用的一组预编译模块。它们以对象的形式存储,开发时通过直接引用的形式使用。 9.社区支持正如前面提到的,Python已经诞生了超过25年,全球已有超过800多万的Python用户。 因此,社区成员在这期间提供了很多的贡献,例如: 建议即使是最具经验的开发人员有时也需要建议。社区成员是由最优秀的开发人员组成,当你在发展过程中遇到困难时,他们会为你提供帮助。 Bug信息在库或框架中发现的Bug会在社区进行讨论重现方式和解决方案,通过社区我们能及时掌握这些信息。 入门教程通过社区,新手可以学到很多内容,因为有很多经验丰富的开发人员都是社区成员,这也就是成熟社区帮助开发人员的方式。 10.成本效益Python是开源的编程语言,所以,我们可以在我们认为合适的其他地方使用原本的预算。而且,Python中大多数框架和库也是开源的。 最后,Python也被证明拥有良好的就业前景,因为市场上有很多开发工作都有Python的技能要求,学习它,有机会为你带来更为丰厚的回报。因此,学习它将是一个明智的选择。 结论在这里,我们为您列举了学习Python的10大原因。 而如果您对Python有更多的建议和补充欢迎通过留言评论告诉我们。 转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。 原文出处:https://dzone.com/articles/10-reasons-why-every-developer-should-learn-python
k8s + docker + Jenkins使用Pipeline部署SpringBoot项目时Jenkins错误集锦 背景#系统版本:CentOS7Jenkins版本:2.222.1maven版本:apache-maven-3.6.3Java版本:jdk1.8.0_231Git版本:1.8.3.1docker版本:1.13.1k8s版本:1.9.8使用yum install jenkins方式安装jenkins。 俺的Jenkins只部署了一台机器。 错误1#使用git branch: "$brans", credentialsId: 'platform-jenkins', url: "$GIT_URL/${app_name}.git"下载代码时提示以下异常,但是在服务器上执行git clone命令却能正常执行。 CopyERROR: Error cloning remote repo 'origin'hudson.plugins.git.GitException: Command "git fetch --tags --progress git@git/kd-gateway.git +refs/heads/:refs/remotes/origin/" returned status code 128:stdout: stderr: Permission denied (publickey).fatal: Could not read from remote repository. Please make sure you have the correct access rightsand the repository exists.错误原因#因为使用的是yum方式安装的Jenkins,所以Jenkins启动的时候启动用户是jenkins(可以通过ps -ef|grep jenkins来查看启动用户),而Jenkins服务器上ssh的相关配置和用户却是root,所以就导致没有权限去执行git命令。 解决办法#配置git账号的ssh免密登录,具体怎么配置可以自行百度,这里说下配置的时候需要注意的点: 生成的对应的公钥、私钥的位置是在/var/lib/jenkins/.ssh目录下确认id_rsa、id_rsa.pub这两个文件的所属用户、用户组都是jenkinsgit网站中需要给对应账号配置ssh keyCopy[root@infra2-test-k8s .ssh]# ls -ltotal 12-rw-------. 1 jenkins jenkins 1679 Apr 7 20:50 id_rsa-rw-r--r--. 1 jenkins jenkins 393 Apr 7 20:50 id_rsa.pub-rw-r--r--. 1 jenkins jenkins 197 Apr 7 19:19 known_hosts修改文件所属用户、用户组命令:chown -R jenkins:jenkins id_rsa。 然后去掉拉取git代码的命令行中的credentialsId:git branch: "$brans", url: "$GIT_URL/${app_name}.git 还有另外一种就是修改jenkins用户的用户组,将其放入root用户组中,命令如下: Copy 添加 gpasswd -a root jenkins 移除 gpasswd -d root jenkinsjenkins启动用户是放在/etc/sysconfig/jenkins文件中的JENKINS_USER参数下,对应的用户组参数:JENKINS_GROUP 这个办法理论上是可以的,不过我没试,这里仅供参考。 错误2#CopyERROR: Error fetching remote repo 'origin'hudson.plugins.git.GitException: Failed to fetch from git@git/kd-gateway.git at hudson.plugins.git.GitSCM.fetchFrom(GitSCM.java:909) at hudson.plugins.git.GitSCM.retrieveChanges(GitSCM.java:1131) at hudson.plugins.git.GitSCM.checkout(GitSCM.java:1167) at org.jenkinsci.plugins.workflow.steps.scm.SCMStep.checkout(SCMStep.java:124) at org.jenkinsci.plugins.workflow.steps.scm.SCMStep$StepExecutionImpl.run(SCMStep.java:93) at org.jenkinsci.plugins.workflow.steps.scm.SCMStep$StepExecutionImpl.run(SCMStep.java:80) at org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution.lambda$start$0(SynchronousNonBlockingStepExecution.java:47) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Caused by: hudson.plugins.git.GitException: Command "git config remote.origin.url git@git/kd-gateway.git" returned status code 255:stdout: stderr: error: could not lock config file .git/config: Permission denied at org.jenkinsci.plugins.gitclient.CliGitAPIImpl.launchCommandIn(CliGitAPIImpl.java:2430) at org.jenkinsci.plugins.gitclient.CliGitAPIImpl.launchCommandIn(CliGitAPIImpl.java:2360) at org.jenkinsci.plugins.gitclient.CliGitAPIImpl.launchCommandIn(CliGitAPIImpl.java:2356) at org.jenkinsci.plugins.gitclient.CliGitAPIImpl.launchCommand(CliGitAPIImpl.java:1916) at org.jenkinsci.plugins.gitclient.CliGitAPIImpl.launchCommand(CliGitAPIImpl.java:1928) at org.jenkinsci.plugins.gitclient.CliGitAPIImpl.setRemoteUrl(CliGitAPIImpl.java:1542) at hudson.plugins.git.GitAPI.setRemoteUrl(GitAPI.java:160) at hudson.plugins.git.GitSCM.fetchFrom(GitSCM.java:897) ... 11 more 错误原因就是没有配置git的ssh免密登录权限,在服务器上配置下即可。 错误3#使用maven命令打包时提示: Copy mvn -Dmaven.test.failure.ignore clean package -P testwhich: no java in (/sbin:/usr/sbin:/bin:/usr/bin) The JAVA_HOME environment variable is not defined correctlyThis environment variable is needed to run this programNB: JAVA_HOME should point to a JDK not a JRE但是去服务器上看了下Java的配置没问题,maven的配置也没问题,PATH也没问题。 原因#maven是从/sbin:/usr/sbin:/bin:/usr/bin这几个目录下找Java的,去服务器上的这几个目录下看了,确实没有Java,那么原因应该就是这个了。 解决办法#做个Java的软连接即可。命令: Copyln -s JAVA_HOME/bin/java /usr/bin/java错误4#Copy mvn -Dmaven.test.failure.ignore clean package -P test[INFO] Scanning for projects... Downloading from nexus: http://39.96.216.150:8081/repository/maven-public/org/springframework/boot/spring-boot-starter-parent/2.2.5.RELEASE/spring-boot-starter-parent-2.2.5.RELEASE.pom[WARNING] Failed to create parent directories for tracking file /opt/apache-maven-3.6.3/LocalRepo/org/springframework/boot/spring-boot-starter-parent/2.2.5.RELEASE/spring-boot-starter-parent-2.2.5.RELEASE.pom.lastUpdated[ERROR] [ERROR] Some problems were encountered while processing the POMs:[FATAL] Non-resolvable parent POM for cn.kuaidao:gateway:0.0.1-SNAPSHOT: Could not transfer artifact org.springframework.boot:spring-boot-starter-parent:pom:2.2.5.RELEASE from/to nexus (http://39.96.216.150:8081/repository/maven-public/): /opt/apache-maven-3.6.3/LocalRepo/org/springframework/boot/spring-boot-starter-parent/2.2.5.RELEASE/spring-boot-starter-parent-2.2.5.RELEASE.pom.part.lock (No such file or directory) and 'parent.relativePath' points at no local POM @ line 5, column 13 @ [ERROR] The build could not read 1 project -> [Help 1][ERROR] [ERROR] The project cn.kuaidao:gateway:0.0.1-SNAPSHOT (/var/lib/jenkins/workspace/test-kd-gateway/pom.xml) has 1 error[ERROR] Non-resolvable parent POM for cn.kuaidao:gateway:0.0.1-SNAPSHOT: Could not transfer artifact org.springframework.boot:spring-boot-starter-parent:pom:2.2.5.RELEASE from/to nexus (http://39.96.216.150:8081/repository/maven-public/): /opt/apache-maven-3.6.3/LocalRepo/org/springframework/boot/spring-boot-starter-parent/2.2.5.RELEASE/spring-boot-starter-parent-2.2.5.RELEASE.pom.part.lock (No such file or directory) and 'parent.relativePath' points at no local POM @ line 5, column 13 -> [Help 2][ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.[ERROR] Re-run Maven using the -X switch to enable full debug logging.[ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles:[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/ProjectBuildingException[ERROR] [Help 2] http://cwiki.apache.org/confluence/display/MAVEN/UnresolvableModelException错误原因#原因和错误1一样,都是用户权限的问题,jenkins用户没有权限去LocalRepo这个目录下创建文件、文件夹,所以就导致找不到文件。ls -l命令查看/opt/apache-maven-3.6.3/LocalRepo这个文件夹所属用户和用户组都是root,修改为jenkins即可。 解决办法#修改文件夹所属用户、用户组。命令:chown -R jenkins:jenkins LocalRepo。 错误5#使用docker构建应用提示以下错误: Copy docker build -t 192.168.30.176:5000/kd-gateway:1.0.29-test -f docker/Dockerfile .Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post http://%2Fvar%2Frun%2Fdocker.sock/v1.26/build?buildargs=%7B%7D&buildbinds=null&cachefrom=%5B%5D&cgroupparent=&cpuperiod=0&cpuquota=0&cpusetcpus=&cpusetmems=&cpushares=0&dockerfile=docker%2FDockerfile&labels=%7B%7D&memory=0&memswap=0&networkmode=default&rm=1&shmsize=0&t=192.168.30.176%3A5000%2Fkd-gateway%3A1.0.29-test&ulimits=null: dial unix /var/run/docker.sock: connect: permission denied 错误原因#/var/run/docker.sock权限问题,这个文件的所属用户、用户组都是root,需要将其修改为jenkins用户。 解决办法#修改文件所属用户、用户组。命令:chown -R jenkins:jenkins docker.sock。 总结#大部分都是权限问题,但是不知道什么原因,我的root用户切换不了jenkins用户,导致排查问题只能靠我机智的大脑去猜,这个就很坑。 原文地址:https://www.lifengdi.com/archives/article/1805 作者: 李锋镝 出处:https://www.cnblogs.com/lifengdi/p/12660352.html
Linux IO模型 简述#IO操作不外乎读和写,但是不同场景对读写有不同的需求,例如网络中同时监控多个文件句柄,例如关键数据希望一路刷到存储设备而不是扔到cache就返回。 怎么读,怎么写,等不等结果返回,是否等获取到数据才发返回,组成了不同的IO模型,分别适用于不同的场景。 根据同步与异步,阻塞与非阻塞,可以把IO模型如下分类: 同步/异步与阻塞/非阻塞怎么理解呢? 同步与异步,就是IO发起人是否等待数据操作结束。发起一次IO请求后,发起IO的进程死等操作结束才继续往下跑,就是同步。发起一次IO请求后,不等结束直接往下跑,就是异步。 阻塞与非阻塞,就是没有数据时是否等到有数据才返回。如果没有数据,就让IO进程干等,直到有数据再返回,就是阻塞。如果看到没有数据,直接就返回了,就是非阻塞。 什么情况下会没有数据呢?一个文件就这么大,除非读到文件末尾了,不然怎么会说没有数据呢?实际上,阻塞与非阻塞并不指磁盘上常规的文件读写,而是指socket或者pipe之类的特殊文件。这些特殊文件并没有明确意义上的大小,就是说,理论上他们的数据是无限大的,只要有人往里面写数据,你就能无限读出数据。这种特殊文件有数据的前提,就是有人往里面”灌“数据。如果你读的太快,别人写得太慢,就会出现池子里面没有数据的情况。这情况并不表示文件读到末端了,只表示暂时还没有数据。这时候你等呢?还是不等呢?这就是阻塞与非阻塞。 如果是同步/异步是IO发起人是否主动想等待,阻塞/非阻塞就是没有数据时读是否被动等待。 本文并不会严格按上图依次介绍6种IO模型,相信网上有一大堆的资料。本文尝试从常规读写、多路复用的2个常用场景介绍IO模型。 上图中的信号IO,需要内核在某个时机向用户空间传递SIGIO信号,且用户空间在捕抓到此信号后进行IO处理。这做法并不常见,本文不会展开介绍。且由于是需要被动通知后才会执行IO操作,因此被我归类为异步IO。 上图中的事件驱动依赖于某些特定的库。其原理类似于为某些事件注册钩子函数,由库函数实现事件监控。在事件触发时调用钩子函数解决。这些函数库屏蔽了平台之间的差异,例如Linux中通过epoll()来监控多个fd。不同库有不同的使用方法,本文也不会展开介绍。 常规read()和write()#读操作#Copy/ 同步阻塞 /fd = open("./test.txt", O_RDONLY); // 常规文件ret = read(fd, buf, len); / 同步非阻塞 /int fds[2];ret = pipe2(fds, O_NONBLOCK); // 无名管道ret = read(fds[0], buf, len); / 同步非阻塞 /fd = open("./fifo", O_RDONLY | O_NONBLOCK); // 有名管道ret = read(fd, buf, len);在大多数场景下,我们系统调用read()正确返回时就表示已经读到数据了,此次的IO操作已经结束了。毫无疑问,大多数情况下的读操作,都是同步的。 是否要阻塞,取决于open()时是否有O_NONBLOCK参数。 总的来说,我们系统调用read(),除非指定O_NONBLOCK,否则都是同步且阻塞的。 写操作#Copy/ 异步 /fd = open("./test.txt", O_WRONLY);ret = write(fd, buf, len); / 同步 /fd = open("./test.txt", O_WRONLY | O_SYNC);ret = write(fd, buf, len);阻塞与非阻塞主要针对读数据,写数据主要区分同步还是异步 由于Linux的IO栈中,有专门为IO准备的Cache层。在正常情况下,写操作只是把数据直接扔到了Cache就返回了,此时数据并没回刷到磁盘。要不等到系统回刷线程主动回刷,要不应用主动调用fsync(),否则数据一直都在Cache层,此时掉电数据就丢了。 写操作是同步还是异步,主要看open()时,是否带O_SYNC参数。带O_SYNC就是同步写,否则就是异步写。 本文开头就提到,同步/异步是IO发起人是否主动想等待IO结束,这里的写IO结束,指的是数据完全写入到磁盘,而非write()返回。在没有O_SYNC情况下,数据只是写到了Cache,需要内核线程定期回刷,所以此时的write是并没有结束的,因此是异步的。相反,如果有O_SYNC,write()操作会一直等到数据完全写入到磁盘后再返回,所以是同步的。 从写性能角度来说,异步写会优于同步写。由内核IO调度算法,对写请求进行合并与排序,再一次性写入,效率绝对高于东一块西一块的随机写。因此,除非是担心掉电丢失的关键数据,否则建议使用异步写 多路复用#多路复用常用于网络开发,例如每个客户端由一个socket与服务器进行远程通信,此时这个服务程序需要同时监控多个socket,为了避免资源损耗和提高响应速率,就会使用多路复用。 多路复用是怎么一回事呢?我们假设一下有100个socket,在某一时间可能只有个别socket是有数据的,即客户端向服务端发送的请求数据。此时服务程序怎么监控这100个socket,找出有数据的socket,并做出响应?有一种做法,就是非阻塞读每个socket,没数据直接返回读下一个,有数据(请求)就响应,以此实现轮询。还有一种做法,创建100个进程/线程,每个线程,进程对应一个socket。 对少量的文件还行,如果文件数量一多,数百个,上千个socket逐一轮询,或者创建上千个线程,这效率得多低啊。可不可以批量等待,当哪怕有一个socket有数据时,内核直接告诉应用那个socket来数据了?可以!这就是内核支持的多路复用的系统调用select()和epoll() Copy/ select 函数原型 /int select(int nfds, fd_set readfds, fd_set writefds, fd_set exceptfds, struct timeval timeout); / epoll 函数原型 /int epoll_create(int size);int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);他们原理大抵相似: 创建一个文件句柄集合,把要监视的文件句柄按顺序整合到一起在有数据时置位对应的标识后返回应用通过检查标识就可以知道是哪个socket有数据了,此时读socket即可直接获取数据具体的使用方法不在这里详细介绍,网上有总多资料,可以参考《UNIX环境高级编程》。 本文顺便记录下select()与epoll()的优缺点对比: select()#每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大每次调用select,都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大select支持的文件描述符数量太小了,默认是1024epoll()#epoll不仅仅一个函数,而是切分为3个函数,使得监控新的fd时,不需要拷贝所有的fd集合,只需要拷贝新的fd到内核即可。epoll采取回调的形式,当某个fd就绪了,就会调用回调,而在回调中,把就绪的fd加入就绪链表epoll没有数量,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048可以发现,epoll()是对select()存在的问题进行针对性的解决。 作者: 广漠飘羽 出处:https://www.cnblogs.com/gmpy/p/12652578.html
面向对象编程基础(java) 1.1 面向对象概述在程序开发初期,大家使用的是结构化开发语言,也就是面向过程(opp),但随着市场需求剧增,软件的规模也越来越大,结构化语言的弊端也暴露出来。 开发周期无休止的拖延,软件质量也越来越差。 为了更好的适应市场,有人就开始以另一种开发思想引入程序中,也就是面向对象的开发思想(oop)。 面向对象思想是人类最自然的一种思考方式,他将所有预处理的问题抽象为对象,同时了解这些对象具有哪一些对应的属性以及行为,以解决这些对象面临的一些实际问题,面向对象设计实质上就是对现实世界的对象进行建模操作。或者说把现实世界的操作虚拟到程序中去 1.2 对象对象,是一个抽象概念,英文称为“Object”,表示存在的事物。通俗的来说就是世间万物皆对象!现实世界中肉眼可见的一种事物都是对象,对象是事物存在的实体,例如:人、手机、电脑。为了大家能简单的理解,只要现实世界中看得到的实物都是一个对象。 1.2.1 对象的组成一个实体(对象)可以划分两个部分,一个是描述外观和功能。那么在程序里关于实体(对象)描述外观的称为“属性”;实体(对象)的功能称之为“行为”或“方法”,也就是实体(对象)执行的动作。例如:人的性别是描述这个实体(对象)的,人能行走也能吃饭和学习是执行的动作。 实体(对象)由属性和行为组成,也就是实体(对象)的外观和功能。 1.3 类类是封装对象的属性和行为的载体,反过来说具有相同属性和行为的一类实体被称之为类(简单的理解也可以是分类)。例如:大雁群就可以看做是大雁类,因为每一只大雁都有相同的嘴、翅膀和爪等外貌特性;同时每一只大雁也会睡觉、飞行、觅食等行为。所以就可以进行分类用程序角度来说的话这一群大雁就是一个大雁类。而大雁群中的每一只大雁都是大雁类的一个对象。 从上面两张流程图我们知道大雁群中的每一只大雁都具备一样的外观描述和行为(功能),那么我们就可以把这个实体(对象)称之为这个分类下的对象或者实体。 总结:类可以理解为分类,是抽象的。而对象就是实体,可以看得到摸得着的。 1.3.1 OPP程序设计特点简单介绍面向对象程序设计具有以下三个通用特点: √封装性 √继承性 √多态性 a).封装封装是面向对象编程的核心思想。也就是将对象的特征和行为封装起来,其载体就是类,类通常会隐藏其实现细节,这就是封装的思想。也就是说现实生活中的物品或商品你只要负责使用就ok,其他的你可以不管。例如:使用手机打电话,只要你打通电话就行,具体内部怎么实现无需知道。 总结:程序中采用封装的思想主要是保证了类内部数据结构的完整性和安全性,使用该类的用户不能轻易的直接操作次数据结构,只能操作类允许公开的数据。这样一来就避免了外部操作对内部数据的影响,提高了程序的可维护性。 b) 继承继承是每一门计算机语言不可少的机制,主要作用就是利用继承机制使新建的类可以建立在原有类的基础之上。在使用或者重写原有类的行为或者功能(程序里叫成员方法)以及访问原有类的特征(也就是类的成员变量)。我们可以称新类为子类,原有类为父类。举个例子:如果类A是类B的父类,而类B是类C的父类,那么也可称类C是类A的子类,类C是从类A基础而来。同时在java中,关于类的继承是单一继承的,也就是说,一个子类只能拥有一个父类,一个父类可以有多个子类。总而言之,继承是在代码中实现重复利用的重要手段,子类通过继承,复用父类特征和行为的同时又添加了子类特有的特征和行为。 c) 多态将父类对象应用于子类的特征就是多态。多态通俗理解就是多种形态,但每一个实体(对象)还是具备了父类的特征和行为。 2.类与对象上面我们认识类的时候就已经讲过,类是我们封装实体(对象)的外观特征(属性)和行为的载体,也就是说这个实体(对象)有哪一些外观描述和功能都可以在类里面进行封装描述 2.1 类的声明语法: public class 类名称{ //类的主体... } 注意: 如果类文件中只有一个类时,文件名必须与类名保持一致;一个java文件中只能有一个public类;如果类文件中不止一个类,文件名必须与public类名一致;如果文件中不止一个类,而且没有public类,文件可与任意类名一致;java类名的命名规则: 类名应该以下划线(_)或者字母开头,最好以字母开头;第一个字母最好大写,如果类名由多个单词组成,则每个单词的首字母最好都大写;类名不能为java中的关键字,例如:String、this、double、int等;类名不能包括任何嵌入的空格或点号,以及除了下划线(_)和美元符号($)字符之外的特殊符号。2.2 a 成员变量java中实体(对象)的特征(属性)也称之为成员变量,成员变量的定义和普通变量定义是一样的。 数据类型 变量名称 [ = 值 ]; 注意:代码中的[ = 值]表示可选内容,也就是说定义变量时可以为其赋值也可以不赋值。 我们来举一个例子,例如鸟类==》Bird类,成员变量对应于类对象属性,鸟都有翅膀、爪子、嘴巴、羽毛...而这一些都是描述鸟的外观也就是对象的属性,所以Bird应该有四个成员变量:wing、claw、beak、feather。 //定义一个Bird鸟类public class Bird{ String wing; //翅膀 String claw; //爪子 String beak; //嘴巴 String feather; //羽毛 } 分析:上面 代码使用关键字class定义类,类名是Bird,同时在Bird类中定义了4个成员变量,成员变量的类型可以设置java中合法的数据类型,说到底成员变量和普通变量是一样的,成员变量也可以设置初始值,也可以不设置初始值。不设置初始值就会有一个默认值。 数据类型 默认值 详细说明 float、double 0.0 浮点零char '' 空格字符boolean false 逻辑符引用类型,如:String null 空值int、byte、short、long 0 0值上面是我们在定义类时使用的一些数据类型以及对应的默认值 2.2 b 成员方法2.2.1 成员方法的定义成员方法对应类对象(实体)的行为或者功能,主要是用来定义类可执行的操作,行为或者功能就是一系列语句组成的代码块。 语法: [权限修饰符] [返回值类型] 方法名 ([参数类型 参数名]){ //方法体--> 一系列代码 return 返回值; } 分析: 权限修饰符可以是private、public、protected中的任意一个,也可以不写,主要来控制方法(行为)的访问权限。返回值类型用来指定方法(行为)返回数据的类型,可以是任何类型,如果方法不需要返回值则可以使用void关键字。一个成员方法(行为)可以有参数也可以没有参数,同时参数是对象也可以是基本数据类型。 class Person{ String name = "张三";//成员变量也称之为类对象属性 //定义一个方法,作用是做一个自我介绍 //不需要返回 public void showInfo(){ System.out.println(name); } } public class Per{ public static void main(String[] args){ Person p = new Person(); p.showInfo();//输出张三 } } 注意: 方法的定义必须是在某个类的里面 ,也就是在类中;同时定义方法如果没有给定权限访问修饰符,该方法的默认权限也缺少,也就是只能在本类以及同一个包中的类进行访问。 如果定义的方法有设置了返回值,则方法里必须使用return关键字返回一个指定类型的数据,并且返回类型要与方法返回值的类型一致。 2.2.2 成员方法的参数在调用方法时可以给该方法传递一个或者多个值,传递给方法的值叫做实参,在方法内部,接受实参的变量叫作形参,形参的声明语法和变量的声明语法是一样的。在java中方法的参数主要有以下三种: 值参数引用参数不定长参数【值参数:】 值参数是实参与形参之间按值传递,当使用值参数方法被调用时,编译器为形参分配储存空间,然后将对应的实参的值复制到形参中,由于是值类型的传递方式,所以在方法中对值类型的形参和修改不会影响实参。 例:书架上有30本书,箱子里有40本书,现吧书架上的书全部放入箱子后,使用带参数的成员方法统计箱子里书的总数。 public class Book{ public static void mian(String []){ Book b = new Book(); int num1 = 30; int num2 = 40; int box = b.add(num1,num2); System.out.println("箱子书总数为:"+ box); } //统计箱子中所有书本 public int add(int num1, int num2){ int box = 0; box = num1+num2; return box; } } 【引用参数:】 在给方法传递参数时,参数的类型数组或者其它引用类型,那么,在方法中对参数的修改会反映到原有的数组或者其他的引用类型上,这种类型的方法参数就叫做引用参数。 现将1、10、100美元存入double数组中,使用引用参数将数组的美元转化成RMB,例1:6.903 public class Rate{ public static void main(String[] args){ double[] arr = {1, 10, 100}; //输出数组中的美钞 for(int i = 0; i < arr.length; i++){ System.out.println(arr[i] + "美元"); } //调用方法change change(arr); //再次输出数组中数据 for(int i = 0; i < arr.length; i++){ System.out.println(arr[i] + "元"); } } //定义一个方法,参数为一维数组(形参) public void change(double[] arr){ for(int i = 0; i < arr.length; i++){ arr[i] *= 6.903; //换算成RMB } }} 【不定长参数:】 在声明方法的时候,如有若干个相同类型的参数,则可以定义不定长参数的成员方法。 public class Scal{ public static void main(String[] args){ int num1=20,num2=42,num3=32,num4=329,num5=329; Scal s = new Scal(); int sum1 = s.add(num1, num2); //只计算二个数值的和 int sum2 = s.adds(num1,num2,num3,num4,num5); System.out.println("sum1=" + sum1); System.out.println("sum2=" + sum2); } //计算二个数之和 public int add(int num1, int num2){ return num1 + num2; } //计算一系列整数之和 public int adds(int... num){ int sum = 0; for(int i = 0; i < x.length; i++){ sum += x[i]; } //返回和 return sum; }} 2.3 局部变量局部变量从字面意思来理解就是在一定范围有效。 定义:成员方法内部定义的变量,那么这个变量就是局部变量。 分析:局部变量在方法执行时被创建在方法执行结束后就会被销毁。同时局部变量在使用时必须进行赋值操作或者初始化,否则会出现编译错误。 public class Demo{ public static void main(String[] args){ Demo d = new Demo(); d.sayHello(); } //说hello public void sayHello(){ String name = "Java"; //是在方法sayHello体内声明的所以是局部变量 System.out.println(name); } } 2.4 构造方法2.4.1 无参构造方法定义在类中除了有成员方法之外还有一个特殊类型的方法,那就是构造方法。构造方法是一个类与类同名的方法,我们实体就是通过构造方法来创建的。也就是说每当类实例化一个对象时,类都会自动调用构造方法。 构造方法没有返回值类型,也不能定义void构造方法的名称要与本类的名称相同构造方法主要作用是完成初始化工作,也能把定义对象的参数传给对象成员分析: 在定义一个类的构造方法时,构造方法是没有返回值(一定要记住),同时也要知道构造方法与普通方法没有返回值的方式不同,普通方法返回值的方法用public void 方法名()形式定义,但构造方法并不需要void关键字进行修饰。 public class 类名 { //构造方法 public 类名() { }}注意: public 修饰符关键字不能少类名一定要保持一致2.4.2 定义有参构造方法在对构造方法中可以为成员变量进行赋值,这样当实例化一个本类的对象时,相应的成员变量也会被初始化。类中没有定义构造方法时,编译器会自动创建一个不带参数的构造方法作为默认。 public class Book { //书名 String name; //定义一个有参数的构造方法 public Book(String str){ name = str; }} 注意: public修饰符关键字不能少类名一定要保持一致构造方法的参数可以是一个或多个分析: 如果在类中定义的构造方法是有参构造方法,编译器就不会为类自动生成一个默认无参构造方法,此时当调用无参构造方法实例化一个对象时,编译器就会报错。因此要记住,只有在类中没有定义构造方法时,编译器才会去自动生成一个不带参数的构造方法。 public class Per{ public static void main(String[] args){ Per p = new Per(); p.sayHello("张三", "男"); } //无参构造方法 public Per(){ } //介绍自己 public void sayHello(String name, String sex){ System.out.println("我的名字"+ name + ",性别是" + sex); }} public class Per{ String username; //成员变量 String agender; //成员变量 public static void main(String[] args){ } //有参构造方法 public Per(String name, String sex){ username = name; agender = sex; } public void sayHello(){ System.out.println("我的名字是:" + username + ",性别为:" + agender); } } 2.5 this关键字this关键字在方法体内表示当前类的对象。类是对象的载体,把通过类可以创建出很多的对象,那么当创建的对象去调用方法时,被调用的方法体内有this关键字,那么这个方法体内的this就是当前对象自己本身。 关于this的三种用法: 1.区分成员变量和局部变量 public class Demo{ public static void main(String[] args){ Demo2 d = new Demo2("张三", 32); d.sayHello(); }}class Demo2{ String name; //名字 int age; //年龄 public Demo2(String name, int age){ name = name; age = age; } //说出自己个人信息 public void sayHello(){ System.out.println("我的名字是" + name + ",年龄为" + age); }} 输出结果为:我的名字是null,年龄为0 在我们构造方法中如果没有使用this来作为变量区分,那么name = name 和 age = age 都是局部变量在操作。也就是局部变量name = 局部变量name,所以成员变量name就一直没有操作。 public class Demo{ public static void main(String[] args){ Demo2 d = new Demo2("张三", 32); d.sayHello(); }}class Demo2{ String name; //名字 int age; //年龄 public Demo2(String name, int age){ this.name = name; this.age = age; } //说出自己个人信息 public void sayHello(){ System.out.println("我的名字是" + name + ",年龄为" + age); }} 输出结果为:我的名字是张三,年龄为32 总结:this在构造方法中,如果参数名称和成员变量一样,则可以使用this来进行区分。 2.构造方法中的使用 public class People{ String name; //姓名 int age; //年龄 String agender; //性别 public static void main(String[] args){ People p = new People("张三"); p.sayHello(); } public People(String name, String agender, int age){ this.name = name; this.agender = agender; this.age = age; System.out.println("这是类的第一个构造方法"); } public People(String name, String agender){ this.name = name; this.agender = agender; System.out.println("这是类的第二个构造方法"); } public People(String name){ this.name = name; System.out.println("这是类的第三个构造方法"); } public void sayHello(){ System.out.println("name=" + name + ",agender=" + agender + ",age=" +age); }} 输出结果为: 这是类的第三个构造方法 name=张三,agender=null,age=0 如果在类构造方法中使用this关键字后: public class People{ String name; //姓名 int age; //年龄 String agender; //性别 public static void main(String[] args){ People p = new People("张三"); p.sayHello(); } public People(String name, String agender, int age){ this.name = name; this.agender = agender; this.age = age; System.out.println("这是类的第一个构造方法"); } public People(String name, String agender){ this(name, agender, 32); this.name = name; this.agender = agender; System.out.println("这是类的第二个构造方法"); } public People(String name){ this(name, "男"); this.name = name; System.out.println("这是类的第三个构造方法"); } public void sayHello(){ System.out.println("name=" + name + ",agender=" + agender + ",age=" +age); }} 输出结果为: 这是类的第一个构造方法 这是类的第二个构造方法 这是类的第三个构造方法 name=张三,agender=男,age=32 3.表示当前对象 public class Demo{ String name; int age; public static void main(String[] args){ Demo d = new Demo("张三", 32); System.out.println(d.name + d.age); } public Demo(String name, int age){ this.name = name; this.age = age; }} 分析: this表示当前调用对象也就是对象“d”,那么构造方法中的this.name就完全等价于d.name = “张三”。 3 static关键字在代码中使用static关键字修饰的变量、方法、常量都被称为静态变量、方法、常量,也可以称之为类的静态成员。 静态成员的使用不需要实例化对象就可以使用,使用的方法也很简单就是【类名.家庭成员】 3.1 静态成员 public class StaticStu{ public static String name = ""; public static void main(String[] args){ name = "张三"; //等价于StaticStu.name = "张三" System.out.println(name); }}//输出的结果为:张三 注意: 在类中使用静态成员可以直接使用,也是不需要实例化。 3.2静态方法 public class StaticStu{ public static void main(String[] args){ StaticStu.SayHello(); } public static void SayHello(){ System.out.println("深夜了"); }}//输出结果为:深夜了 注意: 定义和使用跟变量一样,都是static进行修饰,也不需要实例化就可直接使用 3.3 静态代码块类有类的成员,那么类也有着自己的行为,那就是类的代码块,也称之为静态代码块。主要作用和构造方法雷同,是用来为为类做一些初始化工作。 public class StaticStu{ static { System.out.println("我是类的代码块,我初始化了"); } public static void main(String[] args){ }}//输出结果为:我是类的代码块,我初始化了 注意: 类有代码块那么对象也有代码块,直接是以{}即可。 原文地址https://www.cnblogs.com/beimingdaoren/p/12639285.html
MySQL进阶篇(01):基于多个维度,分析服务器性能 一、服务器性能简介1、性能定义服务器性能优化是一项非常艰巨的任务,当然也是很难处理的问题,在写这篇文章的时候,特意请教下运维大佬,硬件工程师,数据库管理,单从自己的实际开发经验来看,看待这个问题的角度起码是不全面的。 补刀一句:在公司靠谱少撕逼,工程师这个群体是很好交朋友的,互相学习一起进步,升职加薪他不好吗? 服务性能定义:完成一个任务或者处理一次接口请求所需要的时间,这个时间是指响应完成时间,即请求发出,到页面响应回显结束,这是看待性能问题的基本逻辑。 2、分析性能服务的基本过程一般如下图,这是一张最简单的前后端分离,加一台数据库存储的流程,但是想要说明一个复杂的逻辑。 从页面请求,到获取完整的响应结果,这个过程每个环节都可能导致性能问题,抛开网络,硬件,服务器,MySQL存储这些核心客观因素,单是下面这行代码就可以秒掉很多人的努力。 Thread.sleep(10000); // 仿佛整个世界都安静了。影响性能的因素很多,一般说性能优化会从下面几个方面考虑: 网络传输,比如私有云和公有云的交互,接口传输内容过大;应用服务,接口设计是否最简,没有逻辑问题,架构设计是否合理;存储服务,MySQL的查询写入,表设计是否合理,连接池配置是否合理;硬件设施,CPU和内存的利用是否在合理区间,缓存是否合理;这些问题每个处理起来都是非常耗费时间,且对人员的要求相对较高,不说一定要到达专家水平,起码性能问题出现时候,基本的意识要有。 二、MySQL执行机制基于上述流程图,MySQL性能分析主要从下面几个方面切入,基本方向就不会偏。 1、连接池配置查看默认最大连接数配置: SHOW VARIABLES LIKE 'max_connections';最小连接数是连接池一直保持的会话连接,这个值相对好处理许多,评估服务在正常状态下需要多少会话连接。 最大连接数服务器允许的最大连接数值,这个参数的设计就比较飘逸,需要对高并发业务有把控,且要分析SQL性能,和CPU利用率(基本上是70%-85%),想获得这一组参数,可是相当不容易,需要测试精细,配合运维进行服务监控记录,开发不断优化,可能要分库分表,或者集群,拆服务分布式化等一系列操作,最终才能得到合理处理逻辑,当然这样费心对待的都是核心业务,一般的业务也就是经验上把控。 2、SQL执行过程MySQL解析器识别SQL的基本语法,生成语法树,然后优化器输出SQL可执行计划,非常复杂的流程。 客户端发送请求到MySQL服务器;如果执行查询,会检查缓存是否命中;服务端进行SQL解析,预处理,最后优化器生成执行计划;根据执行计划调用存储引擎API执行;返回客户端处理结果;补刀一句:这也就是为什么现在接口提倡最简化设计,或者接口拆分,分步执行,不要问这样会不会多次请求,给网络造成压力,这都5G时代了。 3、逻辑总结总结一句话:分析是否存在MySQL服务的性能问题,需要考量是不是服务配置问题,或者SQL编译过程问题,导致大量等待时间,还是SQL执行有问题,或者查询数据量过大导致执行过程漫长。 补刀一句:MySQL性能问题的基本原因很简单,数据量不断变大,服务器承载不住。作为开发,这是面对数据库优化的根本原因。 三、执行语句分析1、基本描述上面几个方面都是在说明面对服务性能问题时,意识上要清楚的边界,作为开发实际上要面对两个直接问题:表设计,SQL语句编写,大部分的开发都被这两个问题毒打过。 2、表结构设计表设计:表设计关系到数据库的各个方面知识:数据类型选择,索引结构,编码,存储引擎等。是一个很大的命题,不过也遵循一个基本规范:三范式。 规范的表结构,合适的数据类型可以降低资源的占用,索引可以提高查询效率,存储引擎更是关系到事务方面的问题。 表的结构的逻辑清晰,是后续查询和写入的基本条件,结构过大,会出现很多索引,分表结构多,带来很多连接查询,同样会把开发感觉按在地上。这就涉及到一个玄学:开发要根据经验和因素,权衡表结构设计。 补刀一句:如果你去问3.5年的开发,最想写什么业务,他肯定会说单表的增删改查,为什么?因为这类任务是不会排期给他的。 3、数据更新假设在表结构符合逻辑的情况下,数据更新(增删改)操作一般情况下不会出现较大问题,遵循几个基本原则。 数据量大的写入,执行批量操作,占用连接少;删除和更新要考虑锁定的粒度,不要导致大范围锁定;经常执行删除操作,要考虑内存碎片问题;批量操作可以基于应用层面使用多线程处理;4、数据查询查询是开发中最常面对的问题,针对查询的规范也是特别多,确实查询也是最容易出错的环节。但是影响查询的因素很多,可能很多情况下查询只是背黑锅: 表设计规范,减少各种关联,子查询;列类型规范,数据值规范,Null和空处理;索引结构和使用规范,对查询性能影响最大;存储引擎选择合适,直接影响锁定粒度大小;外键关联导致表强行耦合,最讨厌的一个功能;SQL在执行的时候,如果性能很差,还需要基于MySQL慢查询机制进行分析,查看是否出现磁盘IO,临时表,索引失效等各种问题。 四、模块总结上述的描述可能感觉有点乱,但是整体上看,就分为下面三个模块: 应用服务流程化分析,判断瓶颈出现环节;熟悉MySQL基本机制,分析等待和执行时间;MySQL的表结构设计和SQL执行优化;这篇文章只是笼统描述一下服务性能的问题,重点还是想陈述一个基本逻辑:具备服务性能问题分析的意识,且意识的边界相对全面,不要只盯着某个方面思考。 补刀一句:因为文章的分类是MySQL模块,所以重点的描述也在MySQL层面。实际情况中,任何层面都可能导致性能问题。 五、源代码地址GitHub·地址https://github.com/cicadasmile/mysql-data-baseGitEE·地址https://gitee.com/cicadasmile/mysql-data-base本文源码:GitHub·点这里 || GitEE·点这里原文地址https://www.cnblogs.com/cicada-smile/p/12637476.html
介绍ASP.NET Core框架 在这篇文章中,我将要向你们简短介绍一下ASP.NET Core 框架。当今社会,当提到软件开发,每个人都是讨论着开源以及跨平台开发。总所周知,微软是以它的基于Windows产品出名的,比如Windows系统,Office办公套件等。现在我们处在新时代软件开发的潮流中,一个新的革命性的产品,被微软推出市场,那就是-----ASP.NET Core.作为本文的一部分,我将详细述说下面几点。 ASP.NET的历史什么是ASP.NET CoreASP.NET Core的特点和优点有哪些ASP.NET Core中没有哪些东西ASP.NET 的历史总所周知,ASP.NET是一个web框架,并且它被用来开发数据驱动的Web应用程序,已经好多年了。从那以后,ASP.NET框架,就在稳固的更新变革中,现在最新的版本就是ASP.NET Core. ASP.NET Core不是ASP.NET Framework的连续扩展,相反,它是从头到尾,完完全全的一个新框架。ASP.NET Core实际上是在当前ASP.NET Framework上的重写,但是它更小,更具模块化。一些人可能认为ASP.NET Core在大多数方面和ASP.NET Framework保持一致,但是这并不完全正确。ASP.NET Core实际上是在ASP.NET Framework基础上做的一个大的根本性改变。什么是ASP.NET CoreASP.NET Core是一个全新的跨平台、高性能、轻量级、开源的框架。它可以用来开发现代的、联网的、基于云的Web应用程序、IoT物联网、以及WebApIs,ASP.NET Core开发的这些应用可以运行在Windows、Linux、或者Mac操作系统上。 ASP.NET Core框架是基于.NET Framrwork 4.x的完全重写,ASP.NET Core改变了.NET Framework的架构,现在它变得更加模块化、可扩展、开源的、高性能、轻量级、并且可以跨平台。 ASP.NET Core的优点和特点现如今,ASP.NET Core在开发者中,变得越来越流行,是因为下面几个原因,我们来详细看看吧: 开源的:ASP.NET Core框架是开源的。框架的源代码在:https://github.com/aspnet ,你可以免费的下载,甚至如果你还可以修改,编译成你自己的版本。跨平台:ASP.NET Core是重新设计的框架,它的开发和部署都是跨平台的。我们来讨论一下ASP.NET Core跨平台的特点,并且把它和早期的.NET Framrwork做一个比较。早期的ASP.NET Framework应用程序,仅仅只能运行在Windows平台上,然而ASP.NET Core可以开发运行在不同的平台上,例如:Windows、Mac、或者Linux操作系统上。我们仅仅只能把ASP.NET Framework 4.x的应用程序部署在IIS上;然而我们可以在IIS、Nginx、Docker、Apache部署ASP.NET Core应用程序,甚至还可以自托管部署。为了开发ASP.NET Core应用程序,你的选择有很多,你可以选择使用Visual Studio或者Visual Studio Code等。如果你想,你可以选择任何第三方的编辑器来开发ASP.NET Core应用程序。对HTML以及Http请求的完全控制:在ASP.NET Core MVC框架中,你将获得HTML的完全控制权。这就意味着,你可以创建从简单到复杂的、带有CSS样式的HTML页面,并且把他们显示在浏览器上。同样的,你将获得HTTP请求的完全控制权,这样就非常简单的来创建一个Ajax请求了;在ASP.NET Core中,你可以很方便的以插件的形式使用客户端框架,例如JQuery、Bootstrap、React、以及Angular。统一的MVC和Web API框架:ASP.NET Core提供了一个统一的编程模型,用来开发Web Apps和Web APIs.这就意味着,一个Controller类,可以用来处理MVC和Web APIs. 我们在ASP.NET Core(Web Apps或者Web APIs)中创建的控制器,都是要继承Controller基类,并且返回IActionResult接口。IActionResult提供了很多的实现,例如JsonResult以及ViewResult等,还有后面要讲到的很多,都实现了IActionResult接口。在ASP.Net Core API应用程序中,控制器中的方法,是返回JsonResult.与此同时,在ASP.NET Core Web应用程序中,控制中的Action方法返回的是ViewResult.可扩展的框架:ASP.NET Core MVC框架是高度可扩展的。这意味着,今天你开发了一个应用程序,以后也很容易扩展二次开发。下面的这些关键特点提供了ASP.NET Core强大的可扩展性:1.视图组件(View Components) 2.Tag帮助类(Tag Helper) 3.路由(Routing) 接下来的文章中,我将会一一详细,讨论这些细节的,敬请关注! 6. 依赖注入: 在真实的应用程序中,最重要的设计模式之一就是:依赖注入。并且同样重要的是ASP.NETCore 框架提供了内置的依赖注入,我将会在后面的文章中,带大家一起来学习ASP.NET Core中的依赖注入。 7. 测试可维护性:你可以很方便的测试维护ASP.NET Core应用程序。这是因为ASP.NET Core允许你将应用程序分成各个独立的部分,并且允许你独立的测试各个部分。测试框架例如:xUnit以及MOQ可以很轻松的集成到ASP.NET Core应用程序中,进行任何模拟测试。 8. 处理请求响应管道:我们可以使用中间件组件,来处理ASP.NET Core应用程序中的请求响应。在早期的ASP.NET Framework 4.x中,我们通常使用处理程序和模块来处理请求响应管道。ASP.NET Core提供了很多内置的中间件组件,我们可以使用这些中间件组件来处理请求响应管道。如果你愿意,也可以创建自己的中间件组件,来处理请求响应管道。在后面的文章中,我将会教大家中间件组件的使用,以及在ASP.NET Core中怎么来创建自定义的中间件组件。 什么是ASP.NET Core没有的东西如果你是从ASP.NET 4.x过来学习这个系列的人,那么你就会发现,下面这些在ASP.NET Core中是没有的: Global.asax文件Web.config文件HTTP Handlers以及HTTP Modules(HTTP 处理程序和HTTP模块)ASP.NET 页面生命周期模型(ASP.NET Page Life-Cycle model)在下篇文章中,我将带领大家,使用Visual Studio创建第一个ASP.NET Core应用程序.这篇文章中,我向大家简单介绍了ASP.NET Core框架,希望可以帮到大家。 原文地址https://www.cnblogs.com/caofangsheng/p/12631345.html
Python面向对象高级一、 特性特性是指的property. property这个词的翻译一直都有问题, 很多人把它翻译为属性, 其实是不恰当和不准确的. 在这里翻译成特性是为了和属性区别开来. 属性是指的attribute, 我们以前学习的实例变量和类变量是attribute, 所以也可以叫做实例属性和类属性. property(特性)到底是个什么东西? 我们前面学习类属性和实例属性的时候知道, 访问他们的时候就可以直接获取到这些属性的值. 而特性可以看成一种特殊的属性, 为什么呢? 但从访问方式来看, 特性和属性看不出来差别, 但是特性实际上会经过计算之后再返回值. 所以每一个特性都始终与一个方法相关联. 1.1 定义特性定义特性和定义实例方法类似, 只需要另外在方法上面添加一个内置装饰器:@property 访问特性和访问实例变量完全一样, 不需要使用添加括号去调用. import math class Circle: def __init__(self, r): self.r = r @property def area(self): """ 定义特性 这个特性是计算出来圆的面积 :return: """ return math.pi * (self.r ** 2) c = Circle(10) print(c.area) 很明显, 特性背后的本质是一个方法的存在, 所以你不可能在外面去修改这个特性的值! 试图修改特性的值只会抛出一个异常. c.area = 100 1.2 使用特性的设计哲学这种特性使用方式遵循所谓的 统一访问原则. 实际上, 定义一个类总是保持接口的统一总是好的. 有了特性, 把访问属性和访问方法统一了, 都像在访问属性一样, 省得去考虑到底什么时候需要添加括号,什么时候不用添加括号. 1.3 特性的拦截操作python 还提供了设置和删除属性. 通过给方法添加其他内置装饰器来实现 设置:@特性名.setter 删除:@特性名.deleter class Student: def __init__(self, name): self._name = name # name 是特性了, 所以用实例变量存储特性的值的是换个变量名!!! @property def name(self): return self._name @name.setter def name(self, name): if type(name) is str and len(name) > 2: self._name = name else: print("你提供的值" + str(name) + "不合法!") @name.deleter def name(self): print("对不起, name 不允许删除") s = Student("李四")print(s.name) s.name = "彩霞"print(s.name) s.name = "张三"print(s.name) del s.name 二、三大特性之一-封装性面向对象的三大特征:封装, 继承, 多态 2.1什么是封装性 1.封装是面向对象编程的一大特点 2.面向对象编程的第一步,就是讲属性和方法封装到一个抽象的类中 3.外界使用类创建对象,然后让对象调用方法 4.对象方法的细节都被封装在类的内部 在类中定义属性, 定义方法就是在封装数据和代码. 2.2 私有化属性首先先明确一点, python 不能真正的对属性(和方法)进行私有, 因为 python 没有想 java 那样的private可用. python 提供的"私有", 是为了怕在编程的过程中对对象属性不小心"误伤"提供的一种保护机制! 这种级别的私有稍微只要知道了规则, 是很容易访问到所谓的私有属性或方法的. 2.2.1 为什么需要私有封装和保护数据的需要. 默认情况下, 类的所有属性和方法都是公共的, 也就意味着对他们的访问没有做任何的限制. 意味着, 在基类中定义的所有内容都可以都会被派生类继承, 并可从派生类内部进行访问. 在面向对象的应用程序设计中, 我们通常不希望这种行为, 因为他们暴露基类的内部实现, 可能导致派生类中的使用的私有名称与基类中使用的相同的私有名称发生冲突. 属性或方法私有后就可以避免这种问题! 2.2.2 "私有"机制为了解决前面说的问题, python 提供了一种叫做名称改写(name mangling)的机制 如果给属性或者方法命名的时候, 使用两个下划线开头(__)的属性和方法名会自动变形为_类名__方法名, 这样就避免了在基础中命名冲突的问题. class Student: def __init__(self): pass def __say(self): print("我是私有方法你信吗?") s = Student() s.__say() # 双下划线开头的方法已经被形变, 此处访问不到 s._Student__say() 2.2.3 不是真正的私有尽管这种方案隐藏了数据, 但是并没有提供严格的机制来限制对私有属性和方法的访问. 虽然这种机制好像多了一层处理, 但是这种变形是发生在类的定义期间, 并不会在方法执行期间发生, 所以并没有添加额外的开销. 2.2.4 不同的声音有部分人认为这种使用双__的机制好辣鸡, 写两个下划线影响效率. 他们使用一个下划线, 并把这个作为一个约定. 好吧, 你喜欢哪种呢? 三、面向对象三大特性-继承性(Inheritance)这一节我们来学习面向的对象的再一个特征: 继承 3.1继承性的概念继承(extends)是创建新类的一种机制, 目的是专门使用和修改先有类的行为. 原有类称为超类(super class), 基类(base class)或父类. 新类称为子类或派生类. 通过继承创建类时, 所创建的类将继承其基类所有的属性和方法, 派生类也可以重新定义任何这些属性和方法, 并添加自己的新属性和方法 3.2 继承性的意义继承实现代码的重用,相同的代码不需要重复的编写 从子类的角度来看,避免了重复的代码。(子类继承父类后,子类可以直接使用父类的属性和方法) 从父类的角度来看,子类扩展了父类的功能。(因为子类也是一个特殊的父类) 子类可以直接访问父类的属性和方法。子类可以新增自己的属性和方法。子类可以重写父类的方法。3.3 继承的语法和具体实现继承的语法如下: class 父类名: pass class 子类名(父类名): pass 3.3.1最简单的继承python 的继承是在类名的后面添加括号, 然后在括号中声明要继承的父类. class Father: def speak(self): print("我是父类中的 speak 方法") Son继承 Father 类 class Son(Father): pass s = Son()s.speak() 说明: 从字面上我们看到Son没有定义任何的方法, 但是由于Son继承自Father, 则Son会继承Father的所有属性和方法调用方法时, 方法的查找规则: 先在当前类中查找, 当前类找不到想要的方法, 则去父类中查找, 还找不到然后继续向上查找. 一旦找到则立即执行. 如果找到最顶层还找不到, 则会抛出异常示例代码 创建人类 class Person: # 定义吃东西方法 def eat(self): print("吃窝窝头。。") # 定义睡觉方法 def sleep(self): print("睡着啦。。") 创建学生类 class Student(Person): # 子类新增方法:学习 def study(self): print("学生学习啦。。。把你爸乐坏了。。。。。") 创建父类对象,访问父类的方法 zhangsan = Person();zhangsan.eat()zhangsan.sleep() 创建子类对象,访问父类的方法和子类的方法 ergou = Student();ergou.eat() # 访问父类的方法ergou.sleep() # 访问父类的方法ergou.study() # 访问子类的新增方法 3.3.2 继承中的__init__()的调用规则如果子类没有手动__init__()方法, 则 python 自动调用子类的__init__()的时候, 也会自动的调用基类的__init()__方法. class Father: def __init__(self): print("基类的 init ") Son继承 Father 类 class Son(Father): def speak(self): pass s = Son() 如果子类手动添加了__init__(), 则 python 不会再自动的去调用基类的__init__() class Father: def __init__(self): print("基类的 init ") Son继承 Father 类 class Son(Father): def __init__(self): print("子类的 init ") def speak(self): pass s = Son() 如果想通过基类初始化一些数据, 则必须显示的调用这个方法, 调用语法是:基类名.__init__(self, 参数...) class Father: def __init__(self, name): print("基类的 init ") self.name = name def speak(self): print("我是父类中的 speak 方法" + self.name) Son继承 Father 类 class Son(Father): def __init__(self, name, age): # name 属性的初始化应该交给基类去完成, 手动调用基类的方法. 一般放在首行 Father.__init__(self, name) # 调动指定类的方法, 并手动绑定这个方法的 self print("子类的 init ") self.age = age s = Son("李四", 20)s.speak()print(s.name)print(s.age) 3.4方法的重写(override)3.4.1重写的概念我们已经了解了调用方法时候的查找规则, 先在子类中查找, 子类查找不到再去父类中查找. 如果父类的方法不满足子类的需求, 利用这个查找规则, 我们就可以在子类中添加一个与父类的一样的方法, 那么以后就会直接执行子类的方法, 而不会再去父类中查找. 这就叫方法的覆写.(override) 重写,就是子类将父类已有的方法重新实现。 父类封装的方法,不能满足子类的需求,子类可以重写父类的方法。在调用时,调用的是重写的方法,而不会调用父类封装的方法。 3.4.2重写父类方法的两种情况覆盖父类的方法父类的方法实现和子类的方法实现,完全不同,子类可以重新编写父类的方法实现。具体的实现方式,就相当于在子类中定义了一个和父类同名的方法并且实现 对父类方法进行扩展子类的方法实现中包含父类的方法实现。(也就是说,父类原本封装的方法实现是子类方法的一部分)。在子类中重写父类的方法 在需要的位置使用super().父类方法来调用父类的方法 代码其他的位置针对子类的需求,编写子类特有的代码实现。 如果在覆写的方法中, 子类还需要执行父类的方法, 则可以手动调用父类的方法:父类名.方法(self, 参数...) class Father: def __init__(self, name): self.name = name def speak(self): print("我是父类中的 speak 方法" + self.name) Son继承 Father 类 class Son(Father): def __init__(self, name, age): Father.__init__(self, name) self.age = age # 子类中覆写了父类的方法 def speak(self): Father.speak(self) print("我是子类的 speak 方法" + self.name + " 年龄:" + str(self.age)) s = Son("李四", 20)s.speak() 3.4.3关于super在Python中super是一个特殊的类(Python 3.x以后出现) super()就是使用super类创建出来的对象 最常使用的场景就是在重写父类方法时,调用在父类中封装的方法实现 3.5、父类的私有属性和方法子类对象不能在自己的方法内部,直接访问父类的私有属性或私有方法子类对象可以通过父类的共有方法间接访问到私有属性或私有方法私有属性和方法是对象的隐私,不对外公开,外界以及子类都不能直接访问 私有属性和方法通常用于做一些内部的事情 3.6、多继承3.6.1多继承的概念多继承:子类可以拥有多个父类,并且具有所有父类的属性和方法 比如:孩子会继承自己的父亲和母亲的特性 3.6.2多继承的语法class 子类名(父类名1, 父类名2...): pass 示例代码: 父类A class A: def test1(self): print("A类中的test1方法。。") 父类B class B: def test2(self): print("B类中的test2方法。。") 子类C同时继承A和B class C(A,B): pass 创建C对象 c1 = C()c1.test1()c1.test2()3.6.3多继承的注意事项提问:如果不同的父类中存在同名的方法,子类对象在调用方法时,会调用哪一个父类中的方法呢? 开发时,应该尽量避免这种容易产生混淆的情况。如果父类之间存在同名的属性或者方法,应该尽量避免使用多继承 3.6.4 Python中的 MRO (方法搜索顺序)[扩展]python中针对类提供了一个内置属性,___mro__可以查看方法搜索顺序 MRO是method resolution order,主要用于在多继承时判断方法,属性的调用路径 print(C.__mro__)输出结果: (, , , )在搜索方法时,是按照__mro_-的输出结果从左至右的顺序查找如果当前类中找到方法,就直接执行,不再搜索如果没有找到,就查找下一个类中是否有对应的方法,如果找到,就直接执行,不再搜索如果找到最后一个雷,还没有对应的方法,程序报错3.6.5 python 中的上帝类型python 中有个类比较特殊, 所有的类都直接和间接的继承自这个类. 这个类就是:object. 他是所有类的基类. 如果一个类没有显示的去继承一个类, 则这个类默认就继承object, 也可以去显示的继承这个类. class Student(object): pass 3.6.6 新式类和旧式(经典)类[扩展]object是python为所有对象提供的基类,提供有一些内置的属性和方法,可以使用 dir函数查看 新式类:以object为基类的类,推荐使用 经典类:不以object为基类的类,不推荐使用 在python 3.x中定义类时,如果没有指定父类,会默认使用object作为该类的父类。所以python 3.x中定义的类都是新式类在python 2.x中定义类时,如果没有指定父类,则不会以object作为父类新式类和经典类在多继承时,会影响到方法的搜索顺序 提示:为了保证编写的代码能够同时在python 2.x 和python 3.x 运行,在定义类的时候,如果没有父类,建议统一继承自object class 类名(object): pass 四、面向对象三大特性-多态性(Polymorphism)4.1多态性的概念封装性,根据职责将属性和方法封装到一个抽象的类中 定义类的准则继承性,实现代码的重用,相同的代码不需要重复的编写 设计类的技巧 子类针对自己特有的书需求,编写特定的代码 多态性,不同的子类对象,调用相同的父类方法,产生不同的执行结果 多态可以增加代码的灵活性 以继承和重写父类方法为前提 是调用方法的技巧,不会影响到类的内部设计 示例代码: """多态性: 继承和重写为前提,创建不同的对象执行的具体方法不同 """class Father(object): def __init__(self, name): print('父类的init方法') self.name = name def say(self): print('父类的say方法' + self.name) # Son类继承于Father类,python中是类继承于类的 class Son(Father): def __init__(self, name, age): Father.__init__(self, name) self.age = age print('子类的init方法') def say(self): Father.say(self) print('子类的say方法:' + self.name + ',' + str(self.age)) # 以下程序会体现出多态性 def mytest(obj): obj.say() f1 = Father("张爸爸")mytest(f1) print("---------------")f2 = Son("小头儿子",5)mytest(f2)4.2属性和方法查找顺序多态性(多态绑定)是在有继承背景情况下使用的一种特性. 是指在不考虑实例背景的情况下使用实例 多态的理论根据是属性和方法的查找过程. 只要使用obj.attr的方式使用属性和方法, 则查找顺序一定是: 对象本身, 类定义, 基类定义... 关于先查找对象本身的说明: 因为 python 是一门动态语言, 允许我们在代码执行的过程中去动态的给对象添加属性和方法, 所以先从对象本身查找. class Father: def __init__(self, name): self.name = name def speak(self): print("我是父类中的 speak 方法" + self.name) Son继承 Father 类 class Son(Father): def __init__(self, name, age): Father.__init__(self, name) self.age = age def speak(self): Father.speak(self) print("我是子类的 speak 方法" + self.name + " 年龄:" + str(self.age)) def foo(): print("我是动态添加上去的...") s = Son("李四", 20)s.speak = foos.speak() 4.3 鸭子类型python 的多态有的时候很多人把它称之为鸭子类型 鸭子类型是指: 看起来像鸭子, 叫起来像鸭子, 走起来像鸭子, 那么它既是鸭子, 你就可以把它当鸭子来用. 换成编程语言的说法就是: 对象属性和方法的时候完成时和类型分开的. class A: def speak(self): print("a 类中的方法") class B: def speak(self): print("b 类中的方法") def foo(obj): obj.speak() a = A()b = B() foo(a)foo(b) 说明: foo接受一个对象, 只要这个对象中有speak()方法, 就可以正常执行, 我们并不关注他的类型A, B这两个类没有任何的关系, 但是他们都有speak方法, 所以传递过去都没有任何的问题.这就是鸭子模型, 只要你看起来有speak就可以了五、其他5.1 特殊属性__slot__5.1.1动态添加属性的问题通过前面的学习中我们知道, 由于 python 的动态语言的特性, 我们可以动态的给对象添加属性和方法. 但是这种方式添加的属性和方法, 只在当前对象上有用, 在其他对象上是没用. class A: pass a1 = A()a1.name = "李四" #给 a1 对象添加一个属性print(a1.name) a2 = A()print(a2.name) # a2中没有 name 属性, 所以抛异常 5.1.2 __slot__的基本使用添加属性和方法最好直接在类中添加, 这样所有的对象都可以拥有了. 如果我想避免把某些属性直接添加到实例对象上, 可以使用一个特殊属性:__slot__类实现. 给__slot__定义一个元组, 则元组内的属性名允许在实例对象上直接添加, 其他的都不允许. class A: __slots__ = ("name", ) a1 = A()a1.name = "李四" # 给 a1 对象添加一个属性 name 属性是允许的print(a1.name)a1.age = 20 # age 不允许, 所以抛异常print(a1.age) 注意: 我们的__init__()中添加属性是在self上添加的, 其实也是直接在对象上添加, 所以没有在元组中的属性名, 也是不允许的.对于我们直接在类中添加方法是没有任何的影响的.class A: __slots__ = ("name",) def __init__(self): self.age = 30 # 也是不允许的 a = A() 5.1.3 继承中的__slot__slot__只对当前类有用, 对他的子类不起作用. 所以子类也要有自己的__slot class A: __slots__ = ("name",) def __init__(self): self.age = 30 # 也是不允许的 class B: def __init__(self): self.age = 30 b = B()print(b.age) 5.1.4 __slot__对性能上的提升一些人把__slot__作为一种安全的特性来实现, 然后实际上他对内存和执行速度上的性能优化才是最重要的. 不使用__slot__, python 使用字典的方式去存储实例数据的, 如果一个程序使用大量的实例, 测内存占用和执行效率都会影响比较大. 使用__slot__后, python 存储实例数据的时候, 不再使用字典, 而是使用一种更加高效的基于数组的数据结构. 可以显著减少内存占用和执行时间. 5.2 实例的测试类型任何一个类都可以做为类型! 创建类的实例时, 该实例的类型是这个类本身, 如果有继承存在, 则父类型也是这个实例的类型. 有些情况下, 我们需要先测试实例的类型然后再写相应的代码. python 支持 2 种测试方法: 5.2.1 内置函数:type(实例)class A: pass class B(A): pass class C: pass a = A()b = B()c = C() print(type(a))print(type(b))print(type(c)) 说明:type返回的是这个实例的所属类的类对象. 补充一下:其实我们经常接触到的有两种对象:1. 实例对象 2. 类对象 类对象就是: 表示类本身的那个对象! 5.2.2 内置函数:isinstance(实例, 类型)class A: pass class B(A): pass class C: pass a = A()b = B()c = C() print(isinstance(a, A)) # Trueprint(isinstance(b, B)) # Trueprint(isinstance(b, A)) # True 继承关系print(isinstance(c, C)) # Trueprint(isinstance(c, A)) # False说明: 这个函数返回的是布尔值, 使用起来方便, 所以以后测试类型建议用这个函数这个函数继承关系也可以测试出来. b是B类创建出来的, B继承自A, 所以b也算是类A的实例.对一个实例也可以同时测试多个类型, 有一个满足就返回True, isinstance(实例, (类 a, 类 b, ...))). 需要把多个类封装到一个tuple中.print(isinstance(c, (A, B, C))) # True5.2.3 类与类的关系: issubclass(类1, 类2)用来测试类1是不是类2的子类. class A: pass class B(A): pass class C: pass print(issubclass(B, A)) # Trueprint(issubclass(C, A)) # Falseprint(issubclass(A, B)) # Falseprint(issubclass(A, object)) # Trueprint(issubclass(C, (object, A))) # True 第二个参数也是可以 class 组成的元组 原文地址https://www.cnblogs.com/yanadoude/p/12625816.html
2022年02月
2022年01月