概述 SpEL即Spring表达式语言(Spring Expression Language)。 从我通常的使用场景(API开发)来说,SpEL提供的大部分能力都可以划到奇技淫巧的范畴内。但是在一些场景下如缓存配置、ThymeLeaf取值等,SpEL还是大有可为的。 SpEL表达式的默认格式为:#{expression}。SpEL表达式以“#”开头,表达式主体包围在花括号中。 我们通常使用的属性取值表达式(也可称为属性占位符,格式${expression})不可以嵌套SpEL表达式。不过SpEL表达式可以嵌套属性取值表达式,如下: 在上面的这个表达式里面,如果属性“someProperty”的值是2,这个表达式的值就是4。 运算符 下表列出了SpEL支持的运算符: Type Operators Arithmetic +, -, *, /, %, ^, div, mod Relational <, >, ==, !=, <=, >=, lt, gt, eq, ne, le, ge Logical and, or, not, &&, Conditional ?: Regex matches 接下来我们主要以@Value注解的形式介绍并演示这些运算符在SpEL中的应用。 算数运算符(Arithmetic) 如下是算数运算符的使用示例: 除运算和取模运算都有字母形式的别名(除运算“div”,取模运算“mod”)。“+”运算符还可以用来执行字符串连接。 关系运算符(Relational) 如下是关系运算符的使用示例: 所有的关系运算都有字母形式的别名。主要是为了适配使用xml配置文件的场景。在xml中使用带有三角符号的运算符(如小于“<”,大于“>”等)是不被允许的,此时我们可以使用字母形式的别名(lt,gt等)来进行运算。 逻辑运算符(Logical) 如下是逻辑运算符的使用示例: 同关系运算符一样,每个逻辑运算符也都有字母形式的别名。 条件运算符(Conditional) 条件运算符,顾名思义是用来根据不同的情况来注入不同的值。实际上,就是一个三目运算符: 三目运算符主要被用来处理“if-then-else”这样的判定。 三目运算符通常的使用场景式是判断一个属性是否为null,如果是的话就返回一个默认值,如下: 此外还有一种“Elvis”运算符,简化了上面这种“判定是否为空,为空则返回默认值”的场景: 如上,“Elvis”运算符的符号是“?:”,我们可以在Groovy语言中见到它。现在SpEL也引入了这个运算符。 正则运算符(Regex) 正则运算符被用来校验字符串是否匹配某个指定的正则表达式。如下: 访问List或Map对象 使用SpEL,我们还可以访问容器中的任何Map或List对象。看个例子: 这里我们创建了一个List对象来存放工人名字,一个Map对象来存放每个工人的工资。 接下来我们使用SpEL来访问这两个集合对象里面的元素: 使用程序解析SpEL表达式 有时候会需要编程处理SpEL表达式。Spring也为我们提供了相关的工具类。这些类都位于“spring-expression”包下。下面是一个封装好的解析SpEL表达式的方法: 调用ExpressionParser.parseExpression()后获得的值是Object类型的,这里会通过强制类型转换转为需要的类型。 现在我们将字符串(’zhyea’)作为SpEL表达式传入并执行,毫无疑问,执行结果也应当返回字符串“zhyea”: 在SpEL中还可以调用方法,访问属性,使用构造器。代码大致如下: 代码都上传到了github上,我就不一一贴执行结果了。 之前我们获取解析后的值是通过了一次强制类型转换的。Spring也提供了一个传入泛型类型来获取目标类型结果的方法,简单做了下封装: 程序处理SpEL这块儿还有很多的内容,不过从我个人的角度来说,到目前的程度已经够了。如果想继续多了解一些的话可以查阅下方的参考文档。话说,spring官方文档中对SpEL的说明完全是基于程序处理来进行的。 相关代码已上传到了GITHUB/zhyea,如有需要请直接查看。 参考文档 Spring Expression Language Spring Expression Language Guide SpEL表达式
[阅读更多...]-
springboot入门04 – 使用SpEL表达式
-
springboot入门03 – 配置定时任务
概述 在Java环境下创建定时任务有多种方式: 使用while循环配合Thread.sleep(),虽然稍嫌粗陋但也勉强可用 使用Timer和TimerTask 使用ScheduledExecutorService 定时任务框架,如Quartz 在SpringBoot下执行定时任务无非也就这几种方式(主要还是后两种)。只不过SpringBoot做了许多底层的工作,我们只需要做些简单的配置就行了。 通过注解实现定时任务 在SpringBoot中仅通过注解就可以实现常用的定时任务。步骤就两步: 在启动类中添加@EnableScheduling注解 在目标方法中添加@Scheduled注解,同时在@Scheduled注解中添加触发定时任务的元数据。 注意: 目标方法需要没有任何参数,并且返回类型为void 。 这里的定时任务元数据是“fixRate=1000”,意思是固定间隔每1000毫秒即执行一次该任务。 再来看几个@Schedule注解的参数: fixedRate:设置定时任务执行的时间间隔,该值为当前任务启动时间与下次任务启动时间之差; fixedDelay:设置定时任务执行的时间间隔,该值为当前任务结束时间与下次任务启动时间之差; cron:通过cron表达式来设置定时任务启动时间,在Cron Generator网站可以直接生成cron表达式。 这样创建的定时任务存在一个问题:如存在多个定时任务,这些任务会同步执行,也就是说所有的定时任务都是在一个线程中执行。 再添几个定时任务来执行下看看: 代码中一共创建了三个定时任务,每个定时任务的执行间隔都是1000毫秒,在任务体中输出了执行任务的线程ID和执行时间。 看下执行结果: 可以看到这三个定时任务的执行有如下的特点: 所有的定时任务每次都是在同一个线程上执行; 虽然未必是job1第一个开始执行,但是每批任务的执行次序是固定的——这是由fixRate参数决定的 这样的定时任务已经能够覆盖绝大部分的使用场景了,但是它的缺点也很明显:前面的任务执行时间过长必然会影响之后的任务的执行。为了解决这个问题,我们需要异步执行定时任务。接下来的部分我们将主要着眼于如何实现异步执行定时任务。 通过@Async注解实现异步定时任务 最常用的方式是使用@Async注解来实现异步执行定时任务。启用@Async注解的步骤如下: 在启动类中添加@EnableAsync注解: 在定时任务方法上添加@Async注解 我们为前面的三个定时任务都加上@Async注解再运行看看: 通过输出信息可以看到每个定时任务都在不同的线程上执行,彼此的执行次序和执行时间也互不影响,说明配置为异步执行已经成功。 通过配置实现异步定时任务 现在我们有必要稍稍深入了解下springboot定时任务的执行机制了。 springboot的定时任务主要涉及到两个接口:TaskScheduler和TaskExecutor。在springboot的默认定时任务实现中,这两个接口的实现类是ThreadPoolTaskScheduler和ThreadPoolTaskExecutor。 ThreadPoolTaskScheduler负责实现任务的定时执行机制,而ThreadPoolTaskExecutor则负责实现任务的异步执行机制。二者中,ThreadPoolTaskScheduler执行栈更偏底层一些。 尽管在职责上有些区别,但是两者在底层上都是依赖java的线程池机制实现的:ThreadPoolTaskScheduler依赖的底层线程池是ScheduledExecutorService,springboot默认为其提供的coreSize是1,所以默认的定时任务都是在一个线程中执行;ThreadPoolTaskExecutor依赖的底层线程池是ThreadPoolExecutor,springboot默认为其提供的corePoolSize是8。 说到这里应该清楚了:我们可以不添加@Async注解,仅通过调整ThreadPoolTaskScheduler依赖的线程池的coreSize也能实现多线程异步执行;同样的,即使添加了@Async注解,将ThreadPoolTaskExecutor依赖的线程池的corePoolSize设置为1,那定时任务还是只能在一个线程上同步执行。看下springboot的相关配置项: 其中spring.task.scheduling是ThreadPoolTaskScheduler的线程池配置项,spring.task.execution是ThreadPoolExecutor的线程池配置项。 再稍稍扩展下:@Async注解的value属性就是用来指明使用的TaskExecutor实例的。默认值是空字符串,表示使用的是springboot自启动的TaskExecutor实例。如有需要,也可以使用自定义的TaskExecutor实例,如下: 此外,还有一种做法是通过提供自定义的TaskScheduler Bean实例来实现异步执行。要提供提供自定义的TaskScheduler 实例,可以直接通过@Bean注解声明创建,也可以在SchedulingConfigurer接口中配置。这些在后面我们会提到。 调用SpringBoot接口实现定时任务 有时候会需要将定时任务的定时元数据写在数据库或其他配置中心以便统一维护。这种情况就不是通过注解能够搞定的了,此时我们需要使用springboot定时任务一些组件来自行编程实现。常用的组件包括TaskScheduler、 Triger接口和SchedulingConfigurer接口。 注意:因为我们用到了springboot的定时任务组件,所以仍然需要在启动类上添加@EnableScheduling注解。 Trigger接口 Trigger接口主要用来设置定时元数据。要通过程序实现定时任务就不能不用到这个接口。这个接口有两个实现类: PeriodicTrigger用来配置固定时长的定时元数据 CronTrigger用来配置cron表达式定时元数据 使用TaskScheduler接口 TaskScheduler接口前面我们提过,这个接口需要配合Trigger接口一起使用来实现定时任务,看个例子: 在上面的代码里,我们使用@Autowired注解获取了springbootr容器里默认的TaskScheduler实例,然后通过PeriodicTrigger设置了定时元数据,定时任务的任务体则是一个Runable接口的实现(在这里只是输出一行信息)。 因为默认的TaskScheduler实例的线程池coreSize是1,所以如有多个并发任务,这些任务的执行仍然是同步的。要调整为异步可以在配置文件中配置,也可以通过提供一个自定义的TaskScheduler实例来设置: 使用SchedulingConfigurer接口 SchedulingConfigurer接口的主要用处是注册基于Trigger接口自定义实现的定时任务。 在实现SchedulingConfigurer接口后,通常还需要使用@Configuration注解(当然启动类上的@EnableScheduling注解也不能少)来声明它实现类。 这个接口唯一的一个方法就是configureTasks,字面意思是配置定时任务。这个方法最重要的参数是一个ScheduledTaskRegistrar定时任务注册类实例,该类有8个方法,允许我们以不同的方式注册定时任务。 简单做了个实现: 这里我们只使用了三种注册任务的方法,分别尝试注册了fixDelay、fixRate以及cron触发的定时任务。 springboot会自动启动注册的定时任务。看下执行结果: 在执行结果中可以看到这里的任务也是在单一线程同步执行的。要设置为异步执行也简单,因为SchedulingConfigurer接口的另一个作用就是为定时任务提供自定义的TaskScheduler实例。来看下: 在这里,我将之前注册的定时任务去掉了,目的是想验证下这里的配置是否对注解实现的定时任务有效。经检验是可行的。当然对在configureTasks方法中配置的定时任务肯定也是有效的。我就不一一贴结果了。 另外,需要注意:如SchedulingConfigurer接口实例已经注入,将无法再获取到springboot默认提供的TaskScheduler接口实例。 通过Quartz实现定时任务 Quartz是一个非常强大的定时任务管理框架。短短的一篇文章未必能介绍清楚Quartz的全部用法。所以这里只是简单地演示下如何在springboot中是如何使用Quartz的。更多的用法建议优先参考Quartz官方文档。 在spring-boot-web 2.0及之后的版本,已经自动集成了quartz,如果不使用spring-boot-web或使用较早的版本的话我们还需要加一些依赖: 添加完成这些依赖后,springboot服务在启动时也会自启动内部的quartz。事实上springboot已经为我们准备好了几乎全部的quartz的配置。我们要做的只是把自定义的任务填进去。 首先我们需要创建一个Job实例,来实现Job的具体行为。 QuartzJobBean是Spring提供的Quartz Job抽象类。在实现这个类的时候我们可以获取注入到spring中的其他Bean。 配置Job 在创建QuartzConfig类的时候实现了InitializingBean接口,目的是在QuartzConfig实例及依赖类都完成注入后可以立即执行配置组装操作。 这里面有几个关键接口需要说明下: SchedulerFactoryBean,Quartz Scheduler工厂类,springboot自动化配置实现; Scheduer,负责Quartz Job调度,可从工厂类实例获取; JobDetail,执行Quartz Job封装; Trigger,完成Quartz Job启动。 还可以在配置文件中添加Quartz的配置: 这里配置了让Quartz默认延迟启动3分钟。 看下执行结果: 好了,就这些内容了。前面用到的程序都上传到了GITHUB,有需要可以参考下。 参考文档 Spring Task Execution and Scheduling Scheduling Tasks SpringBoot Quartz Scheduler Spring Boot Quartz Scheduler Example: Building an Email Scheduling app Quartz Scheduler Tutorials
[阅读更多...] -
Hadoop HAR文件的读取操作
概述 Hadoop Archive是Hadoop官方提供的解决HDFS上小文件过多的一种方案。可以通过如下命令来执行生成har文件: 执行archive命令会提交一个MapReduce任务来生成har文件。在了解har文件结构后也可以考虑本地生成har文件再上传。 关于“hadoop archive”指令的更多细节请参考官方文档 。 HAR文件结构 HAR文件实际上是一个以”.har”结尾命名的目录,其中至少包含三个文件: _index _masterindex part-00000 … 其中“_index”文件中存储包内目录、文件的元数据信息,并按路径java字符串hashCode()运算的哈希值排序。 “_masterindex”记录了“_index”文件中每1000条文件元数据信息的起止哈希值、以及其在“_index”文件中的起止位置。 “part-X”文件中直接拼接了原始文件内容,无压缩处理,每个map操作的节点生成一个“part-X”文件。 HAR文件读取 在平时工作中读取hdfs文件有三种形式:即在java代码中通过hadoop-client形式读取,执行spark任务读取,通过webhdfs的rest api读取。下面一一介绍下相关的的解决方案。 通过java-client方式读取 假设我们有一个har归档文件,其存储目录是:/har/hp2.har。 在这个归档文件下存在一个目录hp,存储了7个txt文件: 下面通过hadoop-client编写程序列出“har:///har/hp2.har/hp/”下的全部文件: 上面是用scala代码做的实现。 比照普通的HDFS文件的访问方式,访问Har文件的主要特点在于其FileSystem操作对象是一个HarFileSystem的实例。 对HarFileSystem实例执行initialize()操作的时候需要传入要访问的har文件的根路径,其后所有的操作都是对har子项的相对路径进行操作。 注意:执行initialize()操作时只能传入har文件的根路径,不能像执行上面的“hfs -ls har:///har/hp2.har/hp/”指令一样传入一个完整的har子项的路径。 通过spark任务读取 spark读取数据文件大致有三种形式: 通过SparkSession.read.textFile读取文件创建DataSet 通过SparkSession.read.text读取文件创建DataFrame 通过SparkContext.textFile读取文件创建RDD 针对这三种形式,我分别写了一段代码进行验证: 在执行日志中输出的验证信息如下: 根据执行结果可知,通过SparkSession读取的两种方案均不可行,只有通过SparkContext进行读取才能达到预期效果。 通过webhdfs方式读取 关于直接通过webhdfs方式读取har中内容的方式,我查了些资料,包括Hadoop官方文档,StackOverflow的相关话题以及Google检索出的条目,其中勉强可行的是如下两篇文章建议的方案: Downloading a file inside a hadoop archive using Apache Knox Hadoop WebHDFS usage in combination with HAR (hadoop archive) from PHP 其实现思路大致如下: 通过webhdfs获取har的index文件 在index文件中找到在HDFS中存储目标文件的数据文件,以及目标文件在数据文件中的起始offset及长度 通过webhdfs获取目标文件:http://<hadoop-server>:50070/webhdfs/v1/data-file-path?op=OPEN&offset=$offset&length=$len 扫描har index文件并截取内容不太像是一种优雅的做法,至少我并不喜欢。 简单介绍一种替代方案,即通过hadoop-client实现数据流拷贝,下面是代码实现: 在api接口中调用copy方法即可实现下载功能。代码稍稍有些多,但窃以为还是要比扫描index好一些。
[阅读更多...] -
codeIgniter的多主题、静态文件及端口号的问题
问题描述 这段时间在用php写一个cms应用消磨时间。框架选型用了CodeIgniter,这个框架整体上还是挺让人中意的——只是稍嫌不够灵活,需要做些额外的工作。 这些额外的工作中让人比较费心的是路径相关的问题: 多主题的路径问题 静态文件的路径问题 site_url()提供的路径中端口号的问题 前两个问题,我觉得和CodeIgniter的定位有关:这个框架似乎是将自己定位为轻量级模板,适合用来开发小型应用,所以没有将多主题纳入设计方案。既然框架在设计之初就没有打算兼容多主题,官方对静态文件的处理方案(就是将其全部放在一个指定的目录下)也没什么好奇怪的了。 至于第三个问题,则是在解决前两个问题时遇到的一个惊喜:我的机器上的80端口已经被占用了,所以在开发时给apache分配了8082端口,不过在调用site_url()方法或redirect()方法时发现会跳转到“ http://127.0.0.1/xxx ”这样的路径上——ci将端口抹去了。 另外,需要说明下,目前使用的ci版本是:3.1.10 。 解决方案 针对每个问题说下解决方案。 多主题的路径问题 对多主题的路径,我的解决方案是在Loader层进行处理:创建MY_Loader类并重写view()方法,在view()方法中将主题路径添加到原来的视图路径上。 代码如下: 这里的主题名称我用了硬编码,当然也可以选择通过配置文件或数据库获取。 静态文件的路径问题 对于静态文件,难点是如何将之和视图文件放在同一个主题目录下,并且可以方便的访问。 我的思路是直接把主题的根路径提供出来。 获取主题路径还得在Loader层完成。首先要获取网站$view_folder(在index.php配置的)路径: 比如我目前正在写的项目,它的base_url就是这样的: 然后在MY_Loader重写的views()方法中填上一个表示base_url的变量: 这样所有的视图页都能得到一个指向主题根路径的$base_url变量,当访问静态资源的时候就可以拿来用了: 比如,我这里的一段程序: 默认路径端口号的问题 关于前面提到的端口号的问题,出在CodeIgniter对系统根路径的解析上。 对此有一个比较简单的解决方法:就是在application/config/config.php中配置“base_url”变量。添加了这个配置后,CodeIgniter就不会再尝试自己解析系统根路径了。我不太喜欢这种方案,因为这会大大降低写出来的产品的灵活性。 还有一个方案就是修改CodeIgniter解析系统根路径的代码。我最后就是使用了这种方法,但也有些顾虑:这种修改势必会对升级CodeIgniter版本造成影响。还是说下这种修改是怎么实现的吧,相关的代码在system/core/Config.php中,找到构造方法__construct,修改下相关的逻辑: 其实也是蛮简单的。OK,就这样了。 ######
[阅读更多...] -
查找占用CPU时间最长的线程
记录下查找Java应用占用CPU时间最长的线程的过程。 获取进程ID 使用jps指令获取java服务进程ID: 命令结果如下: 其中19064即是目标java服务进程ID。 查询线程占用时间 使用top -H -p pid指令查询指定进程下的线程占用CPU的信息。 其中“-H”说明查询线程信息,“-p”指示进程ID, pid为进程ID。 执行命令如下: 执行结果如下: 输入大写P(shift + p)可以查看占用CPU最多的线程;输入大写T(shift + t)可以查看占用CPU时间最长的线程。 结果中第一列为线程ID。这里的线程ID是十进制的。因为jstack中的线程ID是以十六进制表示,所以需要将线程ID转为十六进制。 以第一行的线程ID 43225为例,使用如下命令进行转换: 转换结果为: 通过输出结果可知,43225对应的十六进制值是a8d9,通常记为“0xa8d9”。 找到线程进行定位 找到占用CPU较多的线程ID后,还需要将线程执行程序进行关联。 使用jstack命令打印线程堆栈信息,在堆栈信息中搜索十六进制的线程ID,如根据a8d9搜索到的结果为: 注意堆栈信息中的“nid=0xa8d9”,nid即表示十六进制线程ID。 根据堆栈信息可以看出这个线程是日志打印线程,占用CPU时间较长也是可以理解的。 就这样。 其他 如果为目标应用启用了jvm监控服务(如JSPY)的话,这套流程会简单许多——只需要先排序再查看就可以了。 再记录下top指令的一些交互命令:
[阅读更多...] -
springboot入门02 – 自定义数据源及多数据源配置
spring-boot的自动化配置中是包含数据源连接配置的。但有些时候我们需要自定义数据源连接的配置,比如: 使用的数据库连接池Spring暂时还不支持; 需要配置连接多数据源; 需要自定义一些数据库连接配置项。 这三种只是我曾经遇到的情形,当然还有些其他的情形。接下来就以曾经遇到的一个问题进行展开。 自定义数据源 我们在生产环境使用的数据库连接池是alibaba的druid。数据库连接配置大致是这样的: 这里我用h2的内存数据库做个演示。在启动的时候会遇到一个错误日志: 解决这个错误的方法按理来说是很简单,只需要在数据库的配置中添加一行validation-query就好了: 不过不幸的是SpringBoot目前的版本(2.1.7.RELEASE)中的数据库连接自动化配置中没有validation-query选项,也不支持druid数据库连接池的自动化配置。 要解决这个问题,自定义Druid的自动化配置当然是一个好办法;另一个较为简单些的办法就是使用自定义数据源连接配置了。 首先,修改下项目配置: 为了避免因自动化配置产生干扰,这里将数据库连接配置移动到了custom下。 然后,创建一个数据库连接配置类: 在配置类的MapperScan注解中,通过basePackages指明了该数据源所有相关的Mapper类的位置。另外,为了支持事务,这里还创建了DataSourceTransactionManager的Bean实例。如果不需要事务支持可以取消这个Bean方法。 MyBatis有个非常有用的配置项“mapUnderscoreToCamelCase”用来自动识别下划线并转为驼峰结构,这个配置项需要在setSqlSessionFactory方法中完成配置: 这样完成配置后,启动程序可以看到日志中的ERROR信息消失。 使用自定义数据源,还意味着需要承担一些损失,比如: 大部分配置项没有默认值,需要手动配置; springboot原生数据源一些非常有用的特性如schema和platform将不可用; MyBatis的自动化配置将不可用 如果不想承担这些损失,我的建议是:换个数据库连接池试试,如Hikari或DBCP2。 多数据源支持 多数据源通常是自定义数据源配置应用最多的场景了,接下来演示下是如何完成的。 首先,肯定是要在spring配置文件中添加另一个数据库的配置信息了: 这里添加了另一个h2内存数据库master,然后添加master数据库对应的数据源配置: 可以看到Master数据库的配置与Worker数据库的差别不大。唯一不同的是Master数据库配置中,每个Bean的set方法上多了一个@Primary注解。 @Primary注解表示同一个类出现多个可用Bean时,将绑定@Primary注解的Bean。这里的几个Bean,transactionManager的@Primary注解不使用事务的话可以省略,sqlSessionFactory的@Primary注解没有的话使用某些spring版本启动会出错。不过在我的测试应用(版本:springboot 2.1.7.RELEASE)中,没有使用@Primary也没问题。通过debug日志可以看到,使用@Primary注解会影响数据源自动化配置和MyBatis自动化配置,前者用不着,后者用不了,所以也就没啥影响了。 transactionManager的@Primary注解还是有用的。如果没有@Primary注解,为方法添加事务注解需要指明使用哪个transactionManager的Bean,如下: 如果为transactionManager添加了@Primary注解,就会默认使用@Primary注解的transactionManager。因此建议为使用事务较多的数据源的transactionManager添加@Primary注解。 还有一点,每个数据源指向的basePackages是不一样的,需要将不同数据源的Mapper类置于不同的目录下。 写了一个测试应用spring-boot-database,上传到了GitHub,有需要可以参考下。
[阅读更多...] -
springboot入门01 – 缓存的使用
前两天解决了一个Spring缓存的问题,因此就想到记录一下spring-boot缓存的使用。 开启缓存 SpringBoot开启缓存也容易,在启动类上添加@EnableCaching注解就可以了,不需要过多的配置。不过此时开启的缓存是比较简单的缓存,即基于ConcurrentHashMap实现的缓存。虽然简单,但对于负载不高的应用也足够用了。 SpringBoot缓存的两个关键类是:CacheAutoConfiguration和CacheAspectSupport。这两个类分别位于spring-boot-autoconfigure和spring-context包。也就是说不使用spring-boot-starter-cache包也能在spring-boot中使用简单的缓存。 在CacheType类中可以看到SpringBoot支持的缓存类型。懒得一个一个介绍了,直接看下代码好了: 可以看到,支持了差不多所有的主流缓存。 缓存注解 spring boot的缓存主要是由注解支持实现的。下面是几个常见的注解: @Cacheable:针对方法进行配置,根据方法的请求参数缓存返回值,如无缓存值则执行方法; @CacheEvict:用于移除缓存记录,调用方法时会执行移除操作; @CachePut:用于更新缓存记录;保证方法一定会被调用,同时缓存方法返回值;与@Cacheable的区别在于是否每次都调用方法; @CacheConfig:统一配置管理类内部所有缓存注解的属性。 @Cacheable和@CachePut注解属性说明 keyGenerator:缓存数据key生成策略; CacheManager:缓存管理器,管理缓存组件; CacheResolver:缓存解析器,根据实际情况动态解析决定使用哪个缓存实例; value:缓存实例(一个缓存实例可以存储多个缓存KV对)的名称,至少需要设置一个; key:缓存的 key,如果不为空需要要按照 SpEL 表达式编写;如果不为空则缺省按照方法的所有参数进行组合; condition:缓存条件,使用 SpEL 编写;若不为空,只在条件结果为 true 时才进行缓存; unless:缓存排除条件,当条件结果为TRUE时不会缓存; sync:是否同步操作,如为true,那么多个线程访问同一条记录时,会同步执行调用的方法。 @CacheEvict注解属性说明 @CacheEvict注解的属性大致同@Cacheable一致,不过@CacheEvict还有两个独有的属性: allEntries:是否清空所有缓存内容,缺省为 false;如果指定为 true,则方法调用后将立即清空所有缓存; beforeInvocation:是否在方法执行前就清空,缺省为 false;如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存。 @CacheConfig注解属性说明 @CacheConfig注解的属性@Cacheable都有。因为@CacheConfig注解的作用本来就是统一管理相同类下所有方法的缓存配置。 需要提一下的就是在spring4.1之前是没有@CacheConfig注解的,那时候需要在每个方法的@Cacheable注解中设置cacheNames属性。 SpEL表达式 下面简单介绍下SpEL表达式的语法——如下表所示,摘自Spring官方文档: 名称 位置 描述 示例 methodName root对象 当前被调用的方法名 #root.methodname method root对象 当前被调用的方法 #root.method.name target root对象 当前被调用的目标对象实例 #root.target targetClass root对象 当前被调用的目标对象的类 #root.targetClass args root对象 当前被调用的方法的参数列表 #root.args[0] caches root对象 当前方法调用使用的缓存列表 #root.caches[0].name Argument Name 执行上下文 当前被调用的方法的参数,如findArtisan(Artisan artisan),可以通过#artsian.id获得参数 #artsian.id result 执行上下文 方法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict的beforeInvocation=false) #result 注意事项: 当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性, 如@Cacheable(key = “targetClass + methodName +#p0″); 使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”,如:@Cacheable(value=”users”, key=”#id”)和@Cacheable(value=”users”, key=”#p0″)。 类型 运算符 关系 <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne 算术 +,- ,* ,/,%,^ 逻辑 &&,||,!,and,or,not,between,instanceof 条件 ?: (ternary),?: (elvis) 正则表达式 matches 其他类型 ?.,?[…],![…],^[…],$[…] 使用Caffeine缓存 前面的配置,即只在在启动类上添加@EnableCaching注解的情形下,我们只能使用最基础的SIMPLE缓存。现在我们尝试使用Caffeine缓存,就需要再多做些配置了。 我在做这个配置的时候遇到了些问题,所以在这里也简单记录下解决问题的过程。 首先,需要在配置文件中指明使用Caffine缓存并添加Caffeine缓存的配置: 然后加入Caffeine缓存依赖: 然而此时调用方法会发现启动失败,报错如下: 提示缺少CacheManager,说实话一开始看到这个错误提示我是不知咋回事儿的。所以尝试从debug信息中找些提示。将spring-boot的debug配置设置为true,再重新执行,在一长串的autoconfigure解析日志中有如下提示: 提示Caffeine缓存的自动配置需要两个类com.github.benmanes.caffeine.cache.Caffeine和org.springframework.cache.caffeine.CaffeineCacheManager。前者在引入caffeine的jar以后已经不是问题,后者则需要引入新的依赖。 CaffeineCacheManager这个类位于spring-context-support中,我们可以直接引入这个依赖,不过更推荐通过spring-boot-starter-cache来间接引入,这样指向性会更明确一些。
[阅读更多...] -
FileInputStream与BufferedInputStream
程序中有一块儿功能是读取硬盘文件到内存里。这块儿代码执行得异常缓慢,读4个G左右的文件竟会用上十几分钟。平时也不着急,但是今天催得紧,就得优化下了。 上传代码大致如下: 这里的问题在于直接使用了FileInputStream,就是说没有使用任何缓冲(buffer)。因此是一个字节一个字节地从磁盘里面读取文件,又一个字节一个字节地写到内存里,这样子效率可想而知。 尝试优化下,每次多读一些数据: 这样效果明显好了些,因为不再是一个字节一个字节的读了,而是每次读取了1024个字节的数据,也就是每1024字节内存才会与硬盘进行一次交互。因为大量减少了硬盘与内存的交互,速度自然就快了。 继续优化,使用BufferedInputStream: BufferedInputStream是FilterInputStream的子类,实现了装饰设计模式。BufferedInputStream是带缓冲区的输入流,默认缓冲区大小是8M,也就是说这个类会一次读取8M数据到内存缓存里以便进行后续操作。其原理与程序二差不多。 通过这三段程序分别测试读取了一个不太大的文件(1.57M),这三段程序的耗时分别是 8825、15、5,时间单位毫秒。 做一次调整:将程序二的缓存数组长度改为2048 * 1024,会看到执行时长也是5ms。 再做一次调整:这次读取一个34.6M的文件,并将程序二的缓存数组长度调整为8 * 1024 * 1024。可以看到程序二与程序三的执行时长分别是73ms与97ms。二者的性能还是比较接近的。 就这样!
[阅读更多...] -
Metrics学习03 – Histogram
Histogram用来统计数据的分布。Histogram可以提供收集到的数据的最大值、最小值、平均值和中值,此外还能提供百分比分布,如75%,95%,99.9%等等。 Histogram是我学习Metrics的驱动之一。最初是想使用Histogram来优化接口处理统计能力。 看下下面的类图: 类图表示了Histogram需要用到的几类和个接口之间的关系,简单说明下: Sampling接口:意思是取样器,只有一个方法,作用是取出某一阶段的统计结果快照(Snapshot); Reservoir接口:数据池,所有的记录最终都会写入Reservoir实例并完成运算;Metrics提供了多种数据池来执行不同的抽样运算; Snapshot接口:快照接口,作用是对Reservoir中的数据进行二次计算并生成统计结果;Snapshot提供了统计数据的最大值、最小值、标准差、平均值、中值、95分位值等指标; Histogram类:实现了Sampling接口,是对外交互的入口。 老规矩,看一段示例代码: 这段代码中定义了一个delayedMethod()方法,该方法会随机sleep一段时间来模拟方法执行时长。代码主体就是使用Histogram报表来统计每三秒钟内这个方法的执行状态。 MetricRegistry也提供了Histogram实例的创建注册方法,不过为这里了更直观一些,还是使用了直接new关键字来构建Histogram实例。可以看到,每次创建Histogram对象都需要传入一个Reservoir接口的实例。 看下执行结果片段: 统计结果即是由Snapshot提供。 简单介绍下metrics提供的几种Reservoir: UniformReservoir:默认保存1028条记录,每次进行update操作的时候,首先会依次地将值填入1028条记录中,当记录满了之后,就会使用随机替换0 – 1027中的一条(随机抽样1028条记录)。因为是随机替换,所以也不需要进行加锁和解锁。 SlidingWindowReservoir:固定大小的数据池,从0到n-1填入数据,但是不会对数据进行更新,也不会进行加锁和解锁(固定抽样n条记录)。 SlidingTimeWindowReservoir:非固定大小的数据池,但是只会存储过去N秒的数据(抽样N秒内的记录)。使用ConcurrentSkipListMap进行存储。 ExponentiallyDecayingReservoir:固定大小的数据池。首先会逐个数据填满数据池,随后会将老的数据替换为新的数据(抽样n条最新的记录),使用ConcurrentSkipListMap进行存储。可以说是SlidingWindowReservoir与SlidingTimeWindowReservoir的结合。 就这样。
[阅读更多...] -
dependencyManagement导致版本冲突
今天遇到了一个问题: 程序中某处报了ClassNoDefineError。这个类属于jna框架。检查jna的jar,发现确实没有那个类。关键在于这个报错是在一个依赖内部发生的,jna的jar是这个依赖的内部依赖,即当前应用的一个间接依赖。因为使用的jar是一个比较小众的服务,所以第一印象就是这个服务依赖的jna版本错了。进入到服务的pom看了后,发现该服务使用的jna版本是正确的,但是当前应用的jna版本却是错误的。 首先想到的是依赖冲突,使用如下命令检查了当前应用的全部依赖: 检查后可以确认当前应用的依赖(包括间接依赖)中只有一个jna框架,可以排除依赖冲突的问题。 仔细思索了一会儿,猜测是spring框架的问题。 退到工程最外部的pom文件,找到了spring-boot-dependencies: 检查spring-boot-dependencies的pom果然看到了jna的版本变量: 这个版本和应用的版本是一致的。随后在spring-boot-dependencies的dependencyManagement中找到了jna框架: 现在问题清晰了。问题出在maven的dependencyManagement上。 dependencyManagement的作用主要是依赖版本控制。其影响的范围包括: 其范围内的未直接声明版本的依赖; 其范围内的所有间接依赖(不论声明版本与否)。 在这个应用中,spring-boot-dependencies是应用pom的dependencyManagement成员,因此spring-boot-dependencies自己的dependencyManagement成员也会传递生效。jna框架是应用通过一个依赖的间接依赖,虽然在这个依赖中指明了jna的版本号,但是受到spring-boot-dependencies的dependencyManagement的影响,还是使用了一个早期的版本。 解决方式两个: 将jna框架也添加到dependencyManagement中; 先exclude掉jna间接依赖,再直接添加jna依赖并声明版本号
[阅读更多...]