首页
友情链接
精美壁纸
给我留言
更多
关于我
Search
1
uniapp Click点击事件冲突解决
4,558 阅读
2
【插件】UNI APP 实现商米打印机功能支持T1,T2,V2机型
3,875 阅读
3
【测试可用】个人码免签支付系统源码/免签支付系统/微信支付平台
1,978 阅读
4
Java Validation参数校验注解使用
1,230 阅读
5
windows10下docker:给已存在的容器添加端口映射的方法
1,220 阅读
Java
Spring Boot
Spring Mvc
Java基础
进阶知识
前端
uniapp
小程序/公众号
JavaScript
HTML/CSS
Vue
PHP
开源软件
商城
营销工具
开发工具
视频/教程
Discuz主题/插件
typecho主题/插件
SEO杂谈
数据库
MongoDB
MySQL
Redis
单片机
概念说明
电路相关
Python
devops
docker
k8s
linux
职场杂谈
登录
/
注册
Search
标签搜索
python
mysql
人人商城
php
java
docker
typecho
插件
微擎
seo
spring boot
discuz
队列
uni-app
phpcms
教程视频
开源系统
源码
工具
css
哈根达斯
累计撰写
100
篇文章
累计收到
154
条评论
首页
栏目
Java
Spring Boot
Spring Mvc
Java基础
进阶知识
前端
uniapp
小程序/公众号
JavaScript
HTML/CSS
Vue
PHP
开源软件
商城
营销工具
开发工具
视频/教程
Discuz主题/插件
typecho主题/插件
SEO杂谈
数据库
MongoDB
MySQL
Redis
单片机
概念说明
电路相关
Python
devops
docker
k8s
linux
职场杂谈
页面
友情链接
精美壁纸
给我留言
关于我
搜索到
15
篇与
Java
的结果
2021-07-29
Java Validation参数校验注解使用
前言Hibernate Validator 是 Bean Validation 的参考实现 。Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint在日常开发中,Hibernate Validator经常用来验证bean的字段,基于注解,方便快捷高效。在SpringBoot中可以使用 @Validated ,注解Hibernate Validator加强版,也可以使用 @Valid 原来Bean Validation java版本内置校验注解Bean Validation 中内置的 constraintHibernate Validator 附加的 constraintmessage支持表达式和EL表达式 ,比如message = "姓名长度限制为{min}到{max} ${1+2}")想把错误描述统一写到properties的话,在classpath下面新建ValidationMessages_zh_CN.properties文件(注意value需要转换为unicode编码),然后用{}格式的占位符hibernate补充的注解中,最后3个不常用,可忽略。主要区分下@NotNull, @NotEmpty ,@NotBlank 3个注解的区别:@NotNull 任何对象的value不能为null@NotEmpty 集合对象的元素不为0,即集合不为空,也可以用于字符串不为null@NotBlank 只能用于字符串不为null,并且字符串trim()以后length要大于0分组校验如果同一个参数,需要在不同场景下应用不同的校验规则,就需要用到分组校验了。比如:新注册用户还没起名字,我们允许name字段为空,但是在更新时候不允许将名字更新为空字符。分组校验有三个步骤:1 .定义一个分组类(或接口)public interface Update extends Default{ }在校验注解上添加groups属性指定分组public class UserVO { @NotBlank(message = "name 不能为空",groups = Update.class) private String name; // 省略其他代码... }Controller方法的@Validated注解添加分组类@PostMapping("update") public ResultInfo update(@Validated({Update.class}) UserVO userVO) { return new ResultInfo().success(userVO); }自定义的Update分组接口继承了Default接口。校验注解(如: @NotBlank)和@validated默认其他注解都属于Default.class分组,这一点在javax.validation.groups.Default注释中有说明/** * Default Jakarta Bean Validation group. * <p> * Unless a list of groups is explicitly defined: * <ul> * <li>constraints belong to the {@code Default} group</li> * <li>validation applies to the {@code Default} group</li> * </ul> * Most structural constraints should belong to the default group. * * @author Emmanuel Bernard */ public interface Default { }在编写Update分组接口时,如果继承了Default,下面两个写法就是等效的:@Validated({Update.class}),@Validated({Update.class,Default.class})如果Update不继承Default,@Validated({Update.class})就只会校验属于Update.class分组的参数字段递归校验如果 UserVO 类中增加一个 OrderVO 类的属性,而 OrderVO 中的属性也需要校验,就用到递归校验了,只要在相应属性上增加 @Valid 注解即可实现(对于集合同样适用)public class OrderVO { @NotNull private Long id; @NotBlank(message = "itemName 不能为空") private String itemName; // 省略其他代码... }public class UserVO { @NotBlank(message = "name 不能为空",groups = Update.class) private String name; //需要递归校验的OrderVO @Valid private OrderVO orderVO; // 省略其他代码... }自定义校验validation 为我们提供了这么多特性,几乎可以满足日常开发中绝大多数参数校验场景了。但是,一个好的框架一定是方便扩展的。有了扩展能力,就能应对更多复杂的业务场景,毕竟在开发过程中,唯一不变的就是变化本身。 Validation允许用户自定义校验实现很简单,分两步:自定义校验注解package cn.soboys.core.validator; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*; /** * 日期验证 约束注解类 */ @Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy = {IsDateTimeValidator.class}) // 标明由哪个类执行校验逻辑 public @interface IsDateTime { // 校验出错时默认返回的消息 String message() default "日期格式错误"; //分组校验 Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; //下面是我自己定义属性 boolean required() default true; String dateFormat() default "yyyy-MM-dd"; }注意: message 用于显示错误信息这个字段是必须的, groups和payload也是必须的@Constraint(validatedBy = { HandsomeBoyValidator.class})用来指定处理这个注解逻辑的类编写校验者类package cn.soboys.core.validator; import cn.hutool.core.util.StrUtil; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; /** * 日期验证器 */ public class IsDateTimeValidator implements ConstraintValidator<IsDateTime, String> { private boolean required = false; private String dateFormat = "yyyy-MM-dd"; /** * 用于初始化注解上的值到这个validator * @param constraintAnnotation */ @Override public void initialize(IsDateTime constraintAnnotation) { required = constraintAnnotation.required(); dateFormat = constraintAnnotation.dateFormat(); } /** * 具体的校验逻辑 * @param value * @param context * @return */ public boolean isValid(String value, ConstraintValidatorContext context) { if (required) { return ValidatorUtil.isDateTime(value, dateFormat); } else { if (StrUtil.isBlank(value)) { return true; } else { return ValidatorUtil.isDateTime(value, dateFormat); } } } }注意这里验证逻辑我抽出来单独写了一个工具类, ValidatorUtilpackage cn.soboys.core.validator; import cn.hutool.core.date.DateUtil; import cn.hutool.core.text.StrFormatter; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import java.lang.reflect.Method; import java.math.BigDecimal; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 验证表达式 */ public class ValidatorUtil { private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}"); private static final Pattern money_pattern = Pattern.compile("^[0-9]+\\.?[0-9]{0,2}$"); /** * 验证手机号 * * @param src * @return */ public static boolean isMobile(String src) { if (StrUtil.isBlank(src)) { return false; } Matcher m = mobile_pattern.matcher(src); return m.matches(); } /** * 验证枚举值是否合法 ,所有枚举需要继承此方法重写 * * @param beanClass 枚举类 * @param status 对应code * @return * @throws Exception */ public static boolean isEnum(Class<?> beanClass, String status) throws Exception { if (StrUtil.isBlank(status)) { return false; } //转换枚举类 Class<Enum> clazz = (Class<Enum>) beanClass; /** * 其实枚举是语法糖 * 是封装好的多个Enum类的实列 * 获取所有枚举实例 */ Enum[] enumConstants = clazz.getEnumConstants(); //根据方法名获取方法 Method getCode = clazz.getMethod("getCode"); Method getDesc = clazz.getMethod("getDesc"); for (Enum enums : enumConstants) { //得到枚举实例名 String instance = enums.name(); //执行枚举方法获得枚举实例对应的值 String code = getCode.invoke(enums).toString(); if (code.equals(status)) { return true; } String desc = getDesc.invoke(enums).toString(); System.out.println(StrFormatter.format("实列{}---code:{}desc{}", instance, code, desc)); } return false; } /** * 验证金额0.00 * * @param money * @return */ public static boolean isMoney(BigDecimal money) { if (StrUtil.isEmptyIfStr(money)) { return false; } if (!NumberUtil.isNumber(String.valueOf(money.doubleValue()))) { return false; } if (money.doubleValue() == 0) { return false; } Matcher m = money_pattern.matcher(String.valueOf(money.doubleValue())); return m.matches(); } /** * 验证 日期 * * @param date * @param dateFormat * @return */ public static boolean isDateTime(String date, String dateFormat) { if (StrUtil.isBlank(date)) { return false; } try { DateUtil.parse(date, dateFormat); return true; } catch (Exception e) { return false; } } }我自定义了补充了很多验证器,包括日期验证,枚举验证,手机号验证,金额验证自定义校验注解使用起来和内置注解无异,在需要的字段上添加相应注解即可校验流程解析使用 Validation API 进行参数效验步骤整个过程如下图所示,用户访问接口,然后进行参数效验 ,如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理
2021年07月29日
1,230 阅读
0 评论
0 点赞
2021-07-21
Mybatis plus 代码生成,提高开发效率
使用mybatis generator 工具可生成代码第一步 pom.xml中 引入依赖下发引入generator 包与模板引擎velocity包 <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.3.1</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.3</version> </dependency>新建类,执行main文件public class GeneratorTest { //代码生成保存目录 private final static String out="J:\\DEMO\\code\\api-server\\tmp"; //JDBC数据源 private final static String jdbc="jdbc:mysql://localhost:3306/java_dev?useUnicode=true&useSSL=false&characterEncoding=utf8"; //数据库账号 private final static String db_user="root"; //数据库密码 private final static String db_paswword="root"; //表前缀 private final static String table_prefix="eb_"; //包名 private static final String package_name="com.gxxblw.web"; private static final String author="哈根达斯"; public static void main(String[] args) { AutoGenerator mpg = new AutoGenerator(); File file=new File(out); File[] files= file.listFiles(); for (int i=0;i<files.length;i++){ files[i].deleteOnExit(); } // 全局配置 GlobalConfig gc = new GlobalConfig(); gc.setOutputDir(out); gc.setAuthor(author); gc.setOpen(true); //自定义Mappter文件名 gc.setMapperName("%sDao"); gc.setSwagger2(true); //实体属性 Swagger2 注解 mpg.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl(jdbc); // dsc.setSchemaName("public"); dsc.setDriverName("com.mysql.jdbc.Driver"); dsc.setUsername(db_user); dsc.setPassword(db_paswword); mpg.setDataSource(dsc); /*TemplateConfig templateConfig = new TemplateConfig(); templateConfig.setMapper("dao"); templateConfig.setEntityKt("domain"); templateConfig.setXml(null); mpg.setTemplate(templateConfig);*/ // 包配置 PackageConfig pc = new PackageConfig(); pc.setModuleName(scanner("模块名")); pc.setParent(package_name); pc.setMapper("dao"); pc.setEntity("model"); mpg.setPackageInfo(pc); // 策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setEntityLombokModel(true); strategy.setRestControllerStyle(true); strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); strategy.setTablePrefix(table_prefix); mpg.setStrategy(strategy); mpg.setTemplateEngine(new VelocityTemplateEngine()); mpg.execute(); } public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotBlank(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } }执行成功后保存目录将生成模板代码,更多参数可查看文档 查看代码生成
2021年07月21日
172 阅读
0 评论
0 点赞
2021-07-20
Spring Boot 中注解@Scheduled实现定时任务
在编写Spring Boot应用中经常会遇到这样的场景,比如:我需要定时地发送一些短信、邮件之类的操作,也可能会定时地检查和监控一些标志、参数等。1.创建定时任务在Spring Boot中编写定时任务是非常简单的事,下面通过实例介绍如何在Spring Boot中创建定时任务,实现每过5秒输出一下当前时间。在Spring Boot的主类中加入 @EnableScheduling 注解,启用定时任务的配置@SpringBootApplication @EnableScheduling public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }2.创建定时任务实现类@Component public class ScheduledTasks { private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); @Scheduled(fixedRate = 5000) public void reportCurrentTime() { log.info("现在时间:" + dateFormat.format(new Date())); } }运行程序,控制台中可以看到类似如下输出,定时任务开始正常运作了。2021-07-13 14:56:56.413 INFO 34836 --- [ main] c.d.chapter71.Chapter71Application : Started Chapter71Application in 1.457 seconds (JVM running for 1.835) 2021-07-13 14:57:01.411 INFO 34836 --- [ scheduling-1] com.didispace.chapter71.ScheduledTasks : 现在时间:14:57:01 2021-07-13 14:57:06.412 INFO 34836 --- [ scheduling-1] com.didispace.chapter71.ScheduledTasks : 现在时间:14:57:06 2021-07-13 14:57:11.413 INFO 34836 --- [ scheduling-1] com.didispace.chapter71.ScheduledTasks : 现在时间:14:57:11 2021-07-13 14:57:16.413 INFO 34836 --- [ scheduling-1] com.didispace.chapter71.ScheduledTasks : 现在时间:14:57:163.@Scheduled 详解在上面的入门例子中,使用了 @Scheduled(fixedRate = 5000) 注解来定义每过5秒执行的任务。对于 @Scheduled 的使用,我们从源码里看看有哪些配置:@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Repeatable(Schedules.class) public @interface Scheduled { String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED; String cron() default ""; String zone() default ""; long fixedDelay() default -1; String fixedDelayString() default ""; long fixedRate() default -1; String fixedRateString() default ""; long initialDelay() default -1; String initialDelayString() default ""; }这些具体配置信息的含义如下:cron:通过cron表达式来配置执行规则zone:cron表达式解析时使用的时区fixedDelay:上一次执行结束到下一次执行开始的间隔时间(单位:ms)fixedDelayString:上一次任务执行结束到下一次执行开始的间隔时间,使用java.time.Duration#parse解析fixedRate:以固定间隔执行任务,即上一次任务执行开始到下一次执行开始的间隔时间(单位:ms),若在调度任务执行时,上一次任务还未执行完毕,会加入worker队列,等待上一次执行完成后立即执行下一次任务fixedRateString:与fixedRate逻辑一致,只是使用java.time.Duration#parse解析initialDelay:首次任务执行的延迟时间initialDelayString:首次任务执行的延迟时间,使用java.time.Duration#parse解析4.思考与进阶是不是这样实现定时任务很简单呢?那么继续思考一下这种实现方式是否存在什么弊端呢?可能初学者不太容易发现问题,但如果你已经有一定的线上项目经验的话,问题也是显而易见的:这种模式实现的定时任务缺少在集群环境下的协调机制。什么意思呢?假设,我们要实现一个定时任务,用来每天网上统计某个数据然后累加到原始数据上。我们开发测试的时候不会有问题,因为都是单进程在运行的。但是,当我们把这样的定时任务部署到生产环境时,为了更高的可用性,启动多个实例是必须的。此时,时间一到,所有启动的实例就会同时开始执行这个任务。那么问题也就出现了,因为有累加操作,最终我们的结果就会出现问题。解决这样问题的方式很多种,比较通用的就是采用分布式锁的方式,让同类任务之前的时候以分布式锁的方式来控制执行顺序,比如:使用Redis、Zookeeper等具备分布式锁功能的中间件配合就能很好的帮助我们来协调这类任务在集群模式下的执行规则。参考文献原文链接
2021年07月20日
166 阅读
0 评论
0 点赞
2021-07-10
Springboot以jar包方式部署运行
springboot打成jar包后。我们需要将jar在linux环境部署,前提linux系统已安装jdk等运行环境。文章目录索引启动方式一:java -jar app.jar 前台启动启动方式二:nohup java -jar app.jar & 后台启动区别:前台启动ctrl+c就会关闭程序,后台启动ctrl+c不会关闭程序制定控制台的标准输出java -jar app.jar > catalina.out 2>&1 &catalina.out将标准输出指向制定文件catalina.out2>&1 输出所有的日志文件& 后台启动对于上面的命令的解释:bash 中 0、1、2 三个数字分别代表 STDIN_FILENO 、 STDOUT_FILENO 、STDERR_FILENO ,即标准输入(一般是键盘),标准输出(一般是显示屏,准确的说是用户终端控制台),标准错误(出错信息输出)。数字 含义0 标准输入(一般是键盘)1 标准输出(一般是显示屏,准确的说是用户终端控制台)2 标准错误(出错信息输出)启动方式三:编写shell脚本启动在app.jar 同目录下编辑app.sh脚本文件内容如下:#!/bin/sh #功能简介:启动app.jar 文件 #注意:在sh文件中=赋值,左右两侧不能有空格 APP=app APP_NAME=${APP}".jar" log_dir=/home/jar_logs/ log_file=/home/jar_logs/app.log command=$1 # 启动 function start(){ # 日志文件夹不存在,则创建 if [ ! -d "${log_dir}" ];then mkdir "${log_dir}" fi rm -f tpid nohup java -jar ${APP_NAME} 1>/dev/null 2>"${log_file}" & echo $! > tpid check } # 停止 function stop(){ tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'` if [ ${tpid} ]; then echo 'stop process...' kill -15 $tpid fi sleep 5 tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'` if [ ${tpid} ]; then echo 'Kill Process!' kill -9 $tpid else echo 'Stop Success!' fi } # 检查 function check(){ tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'` if [ ${tpid} ]; then echo 'App is running.' else echo 'App is NOT running.' fi } # 强制kill进程 function forcekill(){ tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'` if [ ${tpid} ]; then echo 'Kill Process!' kill -9 $tpid fi } # 输出进程号 function showtpid(){ tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'` if [ ${tpid} ]; then echo 'process '$APP_NAME' tpid is '$tpid else echo 'process '$APP_NAME' is not running.' fi } if [ "${command}" == "start" ]; then start elif [ "${command}" == "stop" ]; then stop elif [ "${command}" == "check" ]; then check elif [ "${command}" == "kill" ]; then forcekill elif [ "${command}" == "tpid" ];then showtpid else echo "Unknow argument...." fi编写完成后需要将脚本文件设置超级管理员权限chmod +x app.sh之后使用./app.sh start/stop等命令启动即可./app.sh start/stop作者:蓝胖子CC7链接:https://www.jianshu.com/p/717afc0e1708来源:简书
2021年07月10日
140 阅读
0 评论
0 点赞
2021-07-09
Spring Cache 缓存 Cacheable注解使用
篇我来向大家介绍一种兼容所有缓存中间件的方案,不论我们是使用 Redis 还是 Ehcache,都不需要关心如何操作 Redis 或者 Ehcache,这套方案统统帮你搞定。这套方案就是大名鼎鼎的 Spring Cache。什么?你没有听过,没关系,本篇带你一起探索。文章目录索引一、揭开 Spring Cache 的面纱1.1 现有缓存方案的痛点试想一种场景:用户 A 打开 APP,进入到了秒杀商品的详情页,那这个商品数据我们会先去数据库查询,然后返回给客户端。因为有大量用户短时间内进入到了详情页,所以可以把活动列表缓存起来,直接读缓存就可以了。那下次再查询商品时,直接去缓存查询就可以了。如果秒杀商品下架了,缓存的数据不会用到了,就把缓存删掉就可以了。上面几步看起来也没啥问题,但是放缓存,删除缓存这两步是需要我们去手动写代码实现的。有没有一种方式不用写操作缓存的代码?假如现在用的缓存中间件是 Redis,领导说要换成 Ehcache,操作缓存的代码是不是又得重新撸一遍?总结下上面场景的痛点:需要手写操作缓存代码,如添加缓存、更新缓存、删除缓存。切换缓存组件并不容易,或者说没有对缓存层进行抽象封装,依赖具体的缓存中间件。哪有没有一种方案可以帮助解决上面的两个痛点呢?这就是今天要介绍的 Spring Cache。1.2 Spring Cache 介绍Spring Cache 是 Spring 提供的一整套的缓存解决方案。虽然它本身并没有提供缓存的实现,但是它提供了一整套的接口和代码规范、配置、注解等,这样它就可以整合各种缓存方案了,比如 Redis、Ehcache,我们也就不用关心操作缓存的细节。Spring 3.1 开始定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术,并支持使用注解来简化我们开发。Cache 接口它包含了缓存的各种操作方式,同时还提供了各种xxxCache缓存的实现,比如 RedisCache 针对 Redis,EhCacheCache 针对 EhCache,ConcurrentMapCache 针对 ConCurrentMap,具体有哪几种,后面实战中会介绍。1.3 Spring Cache 有什么功效每次调用某方法,而此方法又是带有缓存功能时,Spring 框架就会检查指定参数的那个方法是否已经被调用过,如果之前调用过,就从缓存中取之前调用的结果;如果没有调用过,则再调用一次这个方法,并缓存结果,然后再返回结果,那下次调用这个方法时,就可以直接从缓存中获取结果了。1.4 Spring Cache 的原理是什么?Spring Cache 主要是作用在类上或者方法上,对类中的方法的返回结果进行缓存。那么如何对方法增强,来实现缓存的功能?学过 Spring 的同学,肯定能一下子就反应过来,就是用 AOP(面向切面编程)。面向切面编程可以简单地理解为在类上或者方法前加一些说明,就是我们常说的注解。Spring Cache 的注解会帮忙在方法上创建一个切面(aspect),并触发缓存注解的切点(poinitcut),听起来太绕了,简单点说就是:Spring Cache 的注解会帮忙在调用方法之后,去缓存方法调用的最终结果,或者在方法调用之前拿缓存中的结果,或者删除缓存中的结果,这些读、写、删缓存的脏活都交给 Spring Cache 来做了,是不是很爽,再也不用自己去写缓存操作的逻辑了。1.5 缓存注解Spring 提供了四个注解来声明缓存规则。@Cacheable,@CachePut,@CacheEvict,@Caching。大家先有个概念,后面我们再来看怎么使用这些缓存注解。二、使用缓存2.1 引入 Spring Cache 依赖在 pom 文件中引入 spring cache 依赖,如下所示:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>2.2 配置使用哪种缓存Spring Cache 支持很多缓存中间件作为框架中的缓存,总共有 9 种选择:caffeine:Caffeine 是一种高性能的缓存库,基于 Google Guava。couchbase:CouchBase 是一款非关系型 JSON 文档数据库。generic:由泛型机制和 static 组合实现的泛型缓存机制。hazelcast:一个高度可扩展的数据分发和集群平台,可用于实现分布式数据存储、数据缓存。infinispan:分布式的集群缓存系统。jcache:JCache 作为缓存。它是 JSR107 规范中提到的缓存规范。none:没有缓存。redis:用 Redis 作为缓存simple:用内存作为缓存。我们还是用最熟悉的 Redis 作为缓存吧。配置 Redis 作为缓存也很简单,在配置文件 application.properties 中设置缓存的类型为 Redis 就可以了, 如:spring.cache.type=redis当然,别忘了还要在 pom 文件中 引入 Redis 的依赖,不然用不了 Redis。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>2.3 测试缓存那基础的配置已经做好了,现在就是看怎么使用 Spring Cache 了。(1)启动类上添加 @EnableCaching注解。本文案例就是在 启动类 PassjavaQuestionApplication 添加 @EnableCaching 注解。(2)指定某方法开启缓存功能。在方法上添加 @Cacheable 缓存注解就可以了。@Cacheable 注解中,可以添加四种参数:value,key,condition,unless。首先我们来看下 value 参数。下面的代码出于演示作用,用了最简单的逻辑,test 方法直接返回一个数字,连数据库查询都没有做。不过没关系,我们主要验证 Spring Cache 是否对方法的结果进行了缓存。@RequestMapping("/test") @Cacheable({"hot"}) public int test() { return 222; }大家注意,@Cacheable 注解中小括号里面还含有大括号,大括号里面还有 “hot” 字符串,这个 hot 字符串你可以把它当作一个缓存的名字,然后将 test 方法返回的结果存到 hot 缓存中。我们也可以用 value="hot" 的方式。第一次调用 test 方法前,既没有 hot 缓存,更没有 test 的结果缓存。调用 test 方法后,Redis 中就创建出了 hot 缓存了,然后缓存了一个 key,如下图所示:第二次调用 test 方法时,就从缓存 hot 中将 test 方法缓存的结果 222 取出来了,为了验证没有执行 test 中的方法,大家可以在 test 方法中打下 log 或者断点。最后的验证结果肯定是没有走 test 方法的,而是直接从缓存中获取的。那我们再来测试一个方法,方法名改为 test2,且请求路径也改为 test2 了。@RequestMapping("/test2") @Cacheable({"hot"}) public int test2() { return 456; }大家觉得这两个方法的结果都会缓存吗?还是只会缓存第一个被调用的方法。经过测试,执行第一个 test 方法后,再执行 test2 方法,缓存结果一直是 222 不会变。因为他们的 key 都是 默认的 SimpleKey[],所以两个方法对应的缓存的 key 都叫这个,所以得到的缓存值是一样的。(3)加上数据库查询的测试。有的同学可能觉得上面的测试太简单了,test 方法里面啥都没做,还缓存啥呢,完全没必要啊。没关系,大家的顾虑是对的,我们来加上数据库查询,安排~先说下场景:前端需要查询某个题目的详情,正常逻辑是查询数据库后返回结果。假定这个查询操作非常频繁,我们需要将题目详情进行缓存。我们先看看常规 Redis 缓存方案:先从 Redis 缓存中查看缓存中是否有该题目,如果缓存中有,则返回缓存中的题目;如果没有,就从数据库中查。查询出题目后,就用 Redis 存起来,然后返回。这里就要写操作 Redis 的代码了:查询 Redis 缓存、更新 Redis 缓存。 // 查询缓存,假定该题目详情缓存的 key=question1 redisTemplate.opsForValue().get("question1"); // 更新缓存 redisTemplate.opsForValue().set("question1", questionEntity); 那如果用 Spring Cache 注解的话,上面两行代码可以直接干掉了。如下所示,加一个 @Cacheable 注解搞定。 @Cacheable({"question", "hot"}) public QuestionEntity info(Long id) { return getById(id); // 查询数据库操作 }其中 question 和 hot 是缓存的名字,我们可以将结果放到不同的缓存中。结论:如果没有指定请求参数,则缓存生成的 key name,是默认自动生成的,叫做 SimpleKey[]。如果指定了请求参数,则缓存的 key name 就是请求参数,比如上面 info 方法,key 等于我传入的 id = 1。缓存中 key 对应的 value 默认使用 JDK 序列化后的数据。value 的过期时间为 -1,表示永不过期。2.4 自定义配置类上面保存的缓存数据都是默认设置,我们也可以自己定义配置,如下所示,在配置文件 application.properties 添加如下配置:# 使用 Redis 作为缓存组件 spring.cache.type=redis # 缓存过期时间为 3600s spring.cache.redis.time-to-live=3600000 # 缓存的键的名字前缀 spring.cache.redis.key-prefix=passjava_ # 是否使用缓存前缀 spring.cache.redis.use-key-prefix=true # 是否缓存控制,防止缓存穿透 spring.cache.redis.cache-null-values=true然后需要加一个配置类:MyCacheConfig。可以在我的开源项目 passjava 获取完整源码。RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { }2.5 自定义 key然后我们可以指定 key 的值,可以在 @Cacheable 注解里面加上 key 的值 #root.method.name。这是一种特有的表达式,称作 SpEL 表达式,这里代表用方法名作为缓存 key 的名字。@Cacheable(value = {"hot"}, key = "#root.method.name") 接下来就是见证奇迹的时刻,调用 test 方法和 test2 方法,发现有两个不同的 key,一个是 passjava_test1,另外一个 passjava_test2,它们的 key 就是前缀 passjava_ + 方法名 组成。SpEL 表达式还有很多其它规则,如下所示:可以根据项目需要选择合适的表达式来自定义 key。2.6 自定义条件除了设置缓存条目的 key,我们还可以自定义条件来决定是否将缓存功能关闭。这里就要用到 @Cacheable 另外两个属性:condition 和 unless,它俩的格式还是用 SpEL 表达式。对应的四个属性总结如下:代码示例如下:@Cacheable(value = "hot", unless = "#result.message.containss('NoCache')") 当放回的结果 message 字段包含有 NoCache 就不会进行缓存。2.7 更新注解@CachePut 也是用来更新缓存,和 @Cacheable 非常相似,不同点是 @CachePut 注解的方法始终都会执行,返回值也会也会放到缓存中。通常用在保存的方法上。保存成功后,可以将 key 设置保存实例的 id。这个怎么做呢?之前我们说过 key 可以通过 SpEL 表达式来指定,这里就可以搭配 #result.id 来实现。这里还是用个例子来说明用法:创建题目的方法,返回题目实例,其中包含有题目 id。@RequestMapping("/create") @CachePut(value = "hot", key = "#result.id") public QuestionEntity create(@Valid @RequestBody QuestionEntity question){ return IQuestionService.createQuestion(question); }保存的 id 是自增的,值为 123,所以缓存中的 key = passjava_123。2.8 删除缓存注解@CacheEvict 注解的方法在调用时不会在缓存中添加任何东西,但是会从从缓存中移除之前的缓存结果。示例代码如下:@RequestMapping("/remove/{id}") @CacheEvict(value = "hot") public R remove(@PathVariable("id") Long id){ IQuestionService.removeById(id); return R.ok(); }删除条目的 key 与传递进来的 id 相同。我测试的时候传的 id = 123,经过前缀 passjava_组装后就是 passjava_123,所以将之前缓存的 passjava_123 删除了。重复执行也不会报错。注意:@CacheEvict 和 @Cacheable、@CachePut 不同,它能够应用在返回值为 void 的方法上。@CacheEvict 还有些属性可供使用,总结如下:三、 总结本文通过传统使用缓存的方式的痛点引出 Spring 框架中的 Cache 组件。然后详细介绍了 Spring Cache 组件的用法:五大注解。 @Cacheable、@CachePut、@CacheEvict、@Caching,、@CacheConfig。如何自定义缓存条目的 key。如何自定义 Cache 配置。如何自定义缓存的条件。参考资料:www.passjava.cnSpring in Action版权声明: 本文为 InfoQ 作者【悟空聊架构】的原创文章。原文链接:【https://xie.infoq.cn/article/26c54c246279306df0c291327】。
2021年07月09日
198 阅读
0 评论
0 点赞
1
2
3