• springboot入门05 – 包装SpringBoot Controller返回值

    一个项目使用了SpringBoot,需要对Controller的返回值进行二次包装。包装类结构大致如下: 通过查找资料,找到了两种封装方式。 方法一 第一种方式是替换掉RequestResponseBodyMethodProcessor,这需要使用一个MethodReturnValueHandler的装饰类: 在装饰类中使用一个Result类的实例替换了returnValue。而后在InitializingBean中基于原来的RequestResponseBodyMethodProcessor的实例创建一个ResponseBodyWrapHandler的实例来完成替换: 使用ResponseBodyWrapFactoryBean,完成afterProperties方法的调用,只需要创建一个ResponseBodyWrapFactoryBean的实例即可: 这行代码可以放在启动类中。 方法二 第二种方式基于ControllerAdvice和HttpMessageConverter实现。 首先用一个ResponseBodyAdvice类的实现包装Controller的返回值: 如果Controller类的返回值没有String类型的,仅有上面这个类就够了。如果有String类型的返回值,就有可能遇到类型不匹配的问题。HttpMessageConverter是根据Controller的原始返回值类型进行处理的,而我们在ResponseAdvisor中改变了返回值的类型。如果HttpMessageConverter处理的目标类型是Object还好说,如果是其它类型就会出现问题,其中最容易出现问题的就是String类型,因为在所有的HttpMessageConverter实例集合中,StringHttpMessageConverter要比其它的Converter排得靠前一些。我们需要尝试将处理Object类型的HttpMessageConverter放得靠前一些,这可以在一个Configuration类中完成: 在这个方案中,如需要对异常做些特别处理,还可以创建一个ExceptionAdvisor类来完成: 这样还可以根据异常类型来设置返回时的HttpStatus。 就这样。 有朋友在评论区指出问题了,做了些调整,也写了一份示例程序上传到CSDN。有兴趣的可以下载来看看。下载地址如下:点击此处下载。 这是许久之前刚用springboot时写的,现在适应springboot的最新版本存在一些问题。所以稍稍重新调整了下,并将之加入到了最近正在尝试进行的一个系列里面。 新版本的代码可以在GitHub / zhyea上看到。 ##########

    [阅读更多...]
  • springboot入门04 – 使用SpEL表达式

    概述 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入门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

    [阅读更多...]
  • 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来间接引入,这样指向性会更明确一些。

    [阅读更多...]
  • SpringBoot探索01 – @Import注解

    Overview Spring中@Import注解最初主要是在配置类中使用,目的是引入其他的配置类(@Configuration)并实现自动注入。 目前Import并不只是支持引入@Configuration注解的类,也支持引入ImportSelector和ImportBeanDefinitionRegistrar接口的实现类,甚至可以引入普通的Java Bean并完成注入。 写了一个简单的应用来进行测试:spring-boot-import。 做些说明。在应用中定义了一个Worker类,应用做的事情就是结合@Import注解用不同的方式注入Worker类的多个Bean实例。 每个Worker Bean的实例通过name进行区分。 代码的一个核心是MyConfig类,代码如下: 这个类中包含@Configuration注解,说明是一个配置类,Spring会自动注入这个类的实例。此外这个类还通过@Bean注解注入了一个Worker Bean实例“tom”,又通过@Import接口引用三个其他类,目的是尝试注入其他的Worker Bean实例。最后在WorkerService中尝试获取并逐行打印注入的Worker实例: 接下来详细介绍下这个过程中是如何使用@Import接口的。 引入普通的Java Bean MyConfig类中使用@Import注解注入的MyAnotherConfig类没有继承任何超类或实现任何接口: 可以看到,如果不是内部的一个方法使用了@Bean注解,它就是一个普通的Java Bean了。也是通过这个@Bean注解,实现了另一个Worker Bean的注入。 引入ImportSelector实现类 根据Spring的文档,ImportSelector的作用是根据一些注解的属性来决定使用哪些@Configuration类。也就是配置类的选择器。通常在spring的引用包中会看到ImportSelector的实现。 因此这里定义了另一个配置类MySelectConfig,不过为了避免当前应用下Spring的自动注入,没有在这个类中添加@Configuration注解。 看起来和前面的MyAnotherConfig是一样的。不过和前例不一样的是:MySelectConfig类的注入是通过MyImportSelector来实现的。 MyImportSelector的实现如下: 这里没有基于AnnotationMetadata进行判定就直接返回了配置类的名称,在实际工作中不是一个好的实践。不过我们这里只是做一个演示,不需纠结太多。 引入ImportBeanDefinitionRegistrar实现类 ImportBeanDefinitionRegistrar与ImportSelector的作用是有着根本上的不同的:ImportSelector的作用是提供配置类;而ImportBeanDefinitionRegistrar的作用则是根据类定义完成相应Bean实例的创建。 通常ImportBeanDefinitionRegistrar多与ClassPathMapperScanner配合使用。ClassPathMapperScanner可以用来扫描指定的package,获取目标类并完成相应实例的创建。具体应用如MyBatis的@Mapper注解的解释。 看下在我们示例中的使用: 在这里通过Worker类的定义创建了一个名为“jerry”的实例。需要注意:这里虽然完成了Worker实例的创建,但是并没有配置任何属性。等在输出注入的Worker Bean的时候我们会看到这个实例的属性都是默认值。 引入Spring Component 使用@Import注解不仅可以引入普通的Java Bean,也可以引入Spring组件类,即需要使用@Component或者@Service等注解标记的类。组建类中通过@Autowired注解引用的其他组件也会被递归引用并注入。 示例应用中的WorkerService类并没有使用任何注解标记,而是在使用的时候通过@Import注解进行的引入。 这样虽然也可以使用,但并不建议这么做。 引入@Configuration注解的类 这个留到最后是因为一开始比较困惑:既然已经有@Configuration注解了,Spring就一定会自动引入这个类的,应该就没必要再使用@Import注解进行引用并注入了。 后来意识到我的想法是有漏洞的:比如一些第三方spring组件包中的配置类,既没有配置packageScan,也没有配置starter,直接使用肯定是不行的。此时使用@Import注解来导入相关的配置类及组件是一个很好地解决方案。 测试 执行WorkerControllerTest类的测试方法all(),观察测试结果,期间会输出我们创建的几个Worker Bean实例: 可以看到一个Worker实例的属性都是默认值,这个实例即是通过ImportBeanDefinitionRegistrar创建的Worker Bean “jerry”。 其他 关于@Import注解的实现原理可以参考 AbstractApplicationContext.refresh -> BeanFactoryPostProcessor -> ConfigurationClassPostProcessor -> ConfigurationClassParser.processImports()。具体就不展开了。 此外,还有另外一个注解@ImportResource主要用来引入xml或groovy配置文件。

    [阅读更多...]
  • CaffeineCache 慎用weakKeys

    前两天在一个Spring项目里使用了Caffine缓存,在application.yml中的配置如下: 为了避免缓存占用过多内存导致频繁GC,使用了weakKeys和weakValues选项。 不过测试时发现缓存不能命中,仍然会查询数据库。 通过debug发现,caffine使用WeakKeyReference将缓存的key做了封装。WeakKeyReference的结构如下: 需要注意这里的equals()和hashCode()方法。 equals()调用的referenceEquals()方法是接口InternalReference的default方法,具体为: referenceEquals()方法中调用的get()方法在WeakKeyReference类中获取的是key的原始值。在方法中对两个key是否一致的判定使用的是==,而非是equals()。也就是说需要两个key指向同一个对象才能被认为是一致的。 hashCode()的实现也与equals()方法呼应。生成hashCode使用的是System.identityHashCode()。identityHashCode方法是jre的一个native方法,这个方法的注释如下: 注释说明这个方法对于指定的对象会返回相同的hashCode。即这个方法是针对对象进行操作的,比如两个字符串对象,即使其字符序列相同,通过identityHashCode方法生成的hashCode也不会相同。 看一个示例程序: 示例程序输出了相通字符序列“zhyea”的两个字符串对象的identityHashCode执行结果,结果为: 可以看到最终结果是不同的。 到现在缓存不能命中的原因应该是找到了:因为使用了weakKeys选项,caffine使用WeakKeyReference封装了缓存key,导致相同字符序列的不同String对象的key被视为是不同的缓存主键。 果然在去掉weakKeys和weakValues配置项后,测试发现缓存能够命中了。 后来在Caffeine的文档中找到了如下说明: Caffeine.weakKeys() stores keys using weak references. This allows entries to be garbage-collected if there are no other strong references to the keys. Since garbage collection depends only on identity equality, this causes the whole cache to use identity (==) equality to compare keys, instead of equals(). 文档中提到因为GC的限制,需要对weakKey使用“==”替换equals()。 原因算是找到了,不过回过头来想想,在Spring中Caffeine的weakKeys选项确实有些鸡肋:Spring的CacheKey生成方式导致weakKey必然指向不同的对象,结果就是缓存注定不能命中,并且每次调用都会在缓存中插入一条新的记录。这样尽管使用weakKey不会造成内存泄漏,可是也会增加GC负担。因此在SpringBoot中使用Caffeine时需要慎用weakKeys。

    [阅读更多...]
  • Spring Controller层测试 – 05 SpringBootTest & WebServer

    在使用@SpringBootTest测试时可以指定一个端口,如@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) 或 @SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT),这样在测试时会启动Spring内嵌的Http Server。 这时就可以使用一个RestTemplate 或者 TestRestTemplate。 使用RANDOM_PORT和DEFINED_PORT的区别在于前着使用的是配置文件中的端口号(server.port,默认值为8080),后者使用的是一个随机端口号。在进行并行测试的时候可以使用随机端口号以避免端口冲突。 看下测试代码: Web Server测试 这里依然使用SpringRunner执行测试。同时使用@SpringBootTest注解的RANDOM_PORT 模式来得到一个内嵌的WebServer运行当前应用。 测试代码中使用RestTemplate来触发请求,这个过程和使用外部服务器很像。 测试中的assertion现在有了一点儿变化,因为要验证的返回值由MockHttpServletResponse变成了ResponseEntity。 TestRestTemplate 因为使用了@SpringBootTest注解,就可以使用@Autowired注解来获取TestRestTemplate的实例。这个TestRestTemplate实例的作用和常见的RestTemplate实例几乎没有任何区别,只是添加了一些额外的能力。实际上,可以将TestRestTemplate视为RestTemplate在测试环境的装饰类。 参与测试的角色关系如下图: 关于性能 也许我们会觉得第一个方案会更加有性能优势,因为它不需要加载Spring Context。事实上确实如此。但是即使加载了SpringContext,也不会造成特别恐怖的影响,因为在同一个Test Suite中,已经加载的Spring Context是可以重用的。 不过Spring Context重用也会导致一些问题,比如一些测试方法对公共的Bean做了修改就可能会影响到其他测试方法。此时可以使用@DirtiesContext注解来要求重新加载Context。 总结 到现在为止我们从轻到重共介绍了四种SpringBoot Controller测试方案。 虽然我们的目标一直都是对Controller层进行测试,但是从第一种测试方案(Standalone MockMVC)到现在,测试的角度还是有些变化的。一开始我们只是会加载测试的Controller类,却不会加载其周边的一些角色如Filter或Advice。到现在这个方案里我们启动了内嵌的WebServer,加载了整个SpringBoot Context。 目前这个方案是提到的四个测试方案里最重的一个,也是离单元测试的概念最远的一个。 下面说几个使用建议: 如果在单元测试中关注Controller的逻辑,优先选择第一种方案:Standalone MockMVC方案; 如果要测试Web层的其它角色(Filter或Advice)的行为,优先选择第四种测试方案执行集成测试; 避免将单元测试和集成测试混在一起,最好分开来写。 其他 Spring Controller测试 – 01 概述 Spring Controller测试 – 02 Standalone MockMVC Spring Controller测试 – 03 WebContext & MockMVC Spring Controller测试 – 04 SpringBootTest & MockMVC Spring Controller测试 – 05 SpringBootTest & WebServer 示例代码可在CSDN下载,地址:https://download.csdn.net/download/tianxiexingyun/11065824 参考文档:https://thepracticaldeveloper.com/2017/07/31/guide-spring-boot-controller-tests/

    [阅读更多...]
  • Spring Controller层测试 – 04 SpringBootTest & MockMVC

    这种测试方案会加载完整的SpringContext,但我们仍然不需要Web Server,需要继续通过MockMVC来模拟请求。 在测试的时候主要用到了@SpringBootTest注解。看下代码: @SpringBootTest和@AutoConfigureMockMvc 使用@SpringBootTest注解会加载整个Context。这样我们可以自动获得所有在Context中注入的Bean,以及从application.properties中加载的配置信息。 在@SpringBootTest中声明webEnvironment为WebEnvironment.MOCK(默认值就是WebEnvironment.MOCK)后,结合@AutoConfigureMockMvc注解,在测试的时候会得到一个模拟的Web/Servlet环境。 因为没有Web Server,所以就无法使用RestTemplate,也就只能继续使用MockMVC了。这次MockMVC的实例是由@AutoConfigureMockMvc注解来完成的。这归功于SpringBoot的自动化配置。 所有参与测试的对象的关系如下图: 总结 这种测试方案更倾向于集成测试。它的关注点主要在于SpringBoot不同类之间的交互。 在这个测试方案中,请求仍然是通过MockMVC模拟的。不过因为有一个完整的Context,请求处理过程中的所有逻辑都是真实的,请求返回结果也是真实的。因此测试效果和使用Web Server几乎是差不多的了。 如果还是要只测试Controller中的逻辑,也可以继续使用 @MockBean注解来mock一个IWorkerService实例来覆盖Context中已有的实例。即使使用了真正的WebServer,也可以继续使用MockBean。同样在测试中也需要继续mock数据。 概括来说,这种测试的位置稍显尴尬:如果要执行单元测试来测试WEB层的逻辑,建议优先第二种方案;如果要执行集成测试,启动一个真正的Web Server,使用RestTemplate进行测试会更彻底也更加方便。 Spring Controller测试 – 01 概述 Spring Controller测试 – 02 Standalone MockMVC Spring Controller测试 – 03 WebContext & MockMVC Spring Controller测试 – 04 SpringBootTest & MockMVC Spring Controller测试 – 05 SpringBootTest & WebServer 其他:示例代码可在CSDN下载,地址:https://download.csdn.net/download/tianxiexingyun/11065824

    [阅读更多...]
  • Spring Controller层测试 – 03 WebContext & MockMVC

    这种Controller层测试方案会(部分)加载Spring Application Context。不过仍然还是主要是用MockMVC来进行测试,也不需要部署WebServer。 示例代码如下: 和standalone MockMVC模式相比,这种测试方案主要有如下几点不同。 SpringRunner 这种测试是由SpringRunner来执行的。SpringRunner(部分)完成了Spring Context的初始化工作,在执行测试的时候,在日志开始的部分可以看到加载Context的内容。 MockMVC 自动化配置 使用@WebMVCTest注解可以完成MockMVC实例的自动化配置,以便于使用@Autowire注解来引用这个实例。同时,在@WebMVCTest注解中还指明了要测试的Controller类,这样Spring就会在Context中加载该Controller类及其相关的配置项。 此外,@WebMVCTest注解还会主动发现Controller类相关的Filter类和Controller Advice类并完成注入。这样,我们就不需要在setup()方法中再对其进行配置了。 使用MockBean 前面提到过这种测试方案是部分加载了Spring Context。原因就是@WebMVCTest在加载类的时候只会扫描含有@Controller,@ControllerAdvice, @JsonComponent,Filter,WebMvcConfigurer和HandlerMethodArgumentResolver这些注解或接口的类,也就是在三层模型中WEB层相关的类,而@Component注解的类则会被忽略掉。因此我们无法直接从Context获取IWorkerService实例。 因此测试中使用的WorkerService实例是通过@MockBean注解生成并注入到了Spring Context中的,并不是真正的WorkerService实例。 没有真正的服务调用 需要注意这里的返回值仍然是伪造的,在测试中也没有用到任何Web Server。 这次测试的主要关注点仍然是Controller类的内部逻辑,以及一些相关的角色如Filter和Controller Advice是如何影响Controller的返回值的。 总结 这次测试和使用MockMVC Standalone模式的主要区别在于不需要显式加载Controller相关的角色,因为这里使用到了Spring Context。参与测试的角色如下图: 如果我们创建了新的Filter、新的Controller Advice或者其他参与到WEB请求响应过程中的角色,也都会在测试中完成自动注入,不需要任何其它的配置。这和我们实际使用的场景非常类似。 这次测试可以算是向集成测试的一个小小过渡。这里我们没有做任何配置就实现了对Filter和Controller Advice的测试,如果有更多的其它角色,也可以直接集成到测试中。 Spring Controller测试 – 01 概述 Spring Controller测试 – 02 Standalone MockMVC Spring Controller测试 – 03 WebContext & MockMVC Spring Controller测试 – 04 SpringBootTest & MockMVC Spring Controller测试 – 05 SpringBootTest & WebServer 其他:示例代码可在CSDN下载,地址:https://download.csdn.net/download/tianxiexingyun/11065824

    [阅读更多...]