-
Cron 表达式的介绍: cron 的表达式是被用来配置 CronTrigger 实例的。 cron 的表达式是字符串,实际上是有七个子表达式,描述个别细节的时间表。 这些子表达式是由空格分开的,分别代表: Seconds Minutes Hours Day-of-Month Month Day-of-Week Year (可选字段) 例如 “0 0 12 ? * WED” 表示在每星期三下午 12:00 执行,个别子表达式可以包含范围; 例如,在前面的例子里 “WED” 可以替换为 “MON-FRI”,”MON,WED,FRI” 甚至 “MON-WED,SAT”。”*” 代表整个时间段。 每一个字段都有一套可以指定有效值,如 Seconds (秒):可以用数字 0-59 表示 Minutes (分):可以用数字 0-59 表示 Hours (时):可以用数字 0-23 表示 Day-of-Month (天):可以用数字 1-31 中的任意一个值,但要注意一些特别的月份 Month (月):可以用 0-11 或用字符串 “JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV and DEC” 表示 Day-of-Week (每周):可以用数字 1-7 表示(1=星期日)或用字符串 “SUN,MON,TUE,WED,THU,FRI and SAT” 表示 “*”:指定所有的值,比如,Minutes 设置为 *,表示每分钟 “/“:为特别单位,表示为 “每”,例如 “0/15” 表示每隔 15 分钟执行一次,”0” 表示为从 0 分开始,”3/20” 表示每隔 20 分钟执行一次,”3” 表示从第 3 分钟开始执行 “?”:表示每月的某一天,或每周的某一天 “L”:用于每月或每周,表示为每月的最后一天,或每个月的最后一个星期几如 “6L” 表示 “每月的最后一个星期五” “W”:表示为最近的工作日,如 “15W” 放在每月(day-of-month)字段上表示为 “到本月 15 日最近的工作日” “#”:是用来指定每月第 n 个工作日,例如在每周(day-of-week)这个字段中内容为 “6#3” or “FRI#3” 则表示 “每月第三个星期五” Cron 表达式的格式: 秒 分 时 日 月 周 年 (可选) 只有日和周可以为 “?”,并且日和周不能同时为 “?”、”*”;否则会报无效错误; 字段名 允许的值 允许的特殊字符 秒 0-59 , - * / 分 0-59 , - * / 小时 0-23 , - * / 日 1-31 , - * ? / L W C 月 1-12 or JAN-DEC , - * / 周几 1-7 or SUN-SAT , - * ? / L C # 年 (可选字段) empty, 1970-2099 , - * / java 项目 from fhadmin.cn “?” 字符:表示不确定的值 “,” 字符:指定数个值 “-” 字符:指定一个值的范围 “/” 字符:指定一个值的增加幅度。n/m 表示从 n 开始,每次增加 m “L” 字符:用在日表示一个月中的最后一天,用在周表示该月最后一个星期 X “W” 字符:指定离给定日期最近的工作日 (周一到周五) “#” 字符:表示该月第几个周 X。6#3 表示该月第 3 个周五 Cron 表达式的范例: 每隔 5 秒执行一次:*/5 * * * * ? 每隔 1 分钟执行一次:0 */1 * * * ? 每天 23 点执行一次:0 0 23 * * ? 每天凌晨 1 点执行一次:0 0 1 * * ? 每月 1 号凌晨 1 点执行一次:0 0 1 1 * ? 每月最后一天 23 点执行一次:0 0 23 L * ? 每周星期天凌晨 1 点实行一次:0 0 1 ? * L 在 26 分、29 分、33 分执行一次:0 26,29,33 * * * ? 每天的 0 点、13 点、18 点、21 点都执行一次:0 0 0,13,18,21 * * ? 每天的 7 点到 21 点都执行一次:0 0 7-21 * * ?
-
1. pom <!-- 达梦数据库驱动 --> <dependency> <groupId>com.dm</groupId> <artifactId>DmJdbcDriver18</artifactId> <version>1.8</version> </dependency>maven 中央仓库里面没有,需要手动安装到 maven 本地仓库2. druid 配置 application.properties#数据源1datasource.no1.driver-class-name: dm.jdbc.driver.DmDriverdatasource.no1.url=jdbc:dm://192.168.0.11:5238/SYSDBA?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=utf-8datasource.no1.username=SYSDBAdatasource.no1.password=root1234567893. 配置类package org.fh.config;import java.sql.SQLException;import javax.sql.DataSource;import org.apache.ibatis.session.SqlSessionFactory;import org.mybatis.spring.SqlSessionFactoryBean;import org.mybatis.spring.annotation.MapperScan;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.core.io.DefaultResourceLoader;import org.springframework.core.io.support.PathMatchingResourcePatternResolver;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import com.alibaba.druid.pool.DruidDataSource;/** * 说明:第一数据源配置 * 作者:FH Admin * from:fhadmin.cn */@Configuration@MapperScan(basePackages = MasterDataSourceConfig.PACKAGE, sqlSessionFactoryRef = "masterSqlSessionFactory") //扫描 Mapper 接口并容器管理public class MasterDataSourceConfig {…… //其中的 dataSource.addFilters("wall"); 去掉,否则访问不了数据库 @Bean(name = "masterDataSource") @Primary public DataSource masterDataSource() throws SQLException { DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName(driverClass); dataSource.setUrl(url); dataSource.setUsername(user); dataSource.setPassword(password); dataSource.addFilters("stat"); //dataSource.addFilters("wall"); return dataSource; } ……}
-
最近几年,Java 的技术栈发展的非常快,成百上千的技术工具正不断地涌出来,这也造成了一个问题:我们作为开发者,到底应该选哪些工具搭建出最合适的技术栈呢?今天我就推荐一波我常用的、我了解的工具和框架。一、项目工具1.1 IDE主流的 Java 开发工具现在非 IntelliJ IDEA 莫属。前几年,可能 Eclipse 还能和 IDEA 一争高下,到了现在已经基本是 IDEA 的天下了。就拿我自己来说吧,我最早用 IDEA,后来用了几年 Eclipse,再后来又用回了 IDEA。包括我身边的程序员,之前用 Eclipse 的人,这几年不少人都换成用 IDEA 了。如果你问我用 IDEA 到底哪最爽,我觉得有 3 点:代码智能提示,爽!代码自动生成,爽!代码调试,爽!而这 3 点,恰恰就是能极大提高开发人员开发效率的 3 点。所以建议做 Java 后端开发的,可以优先考虑 IDEA 作为开发工具。eclipse 也有有点,免安装,不花钱,打开多个项目方便,配置上简单轻便1.2 版本管理工具对于项目中的代码版本管理工具,Git 已经处于垄断地位了,新项目的话不需要再考虑 SVN、CVS 了。之所以 Git 现在处于垄断地位,主要胜在 2 点:Git 是分布式的,不会因为版本管理服务器崩溃导致完整的代码历史版本丢失。Git 创建分支是非常廉价的操作,可以随意创建分支,从而使并行开发很容易落地。而 SVN、CVS 这些版本管理工具创建分支则非常笨拙,并行开发非常麻烦。上述第 1 点大大提升了代码资产的安全可靠程度;第 2 点则完美适应当代的敏捷开发需求。也因此,Git 大行其道就不足为怪了。1.3 构建工具Java 项目的构建工具现在是龙争虎斗,业内一般有两个选择:Maven 和 Gradle。如果是后端的 Java 项目,那绝大部分用的还是 Maven 去构建项目。如果是前端的 Android 项目,则选择 Gradle。Gradle 本身要比 Maven 先进很多:它配置灵活,性能优秀,真的是个非常优秀的构建工具。那为什么在后端 Java 项目构建的时候,大部分用的还是 Maven 呢?因为 Gradle 本身太过灵活了,这种灵活带来了两个和后端项目构建特性不太匹配的问题:Gradle 因为灵活,所以用法规则多变,导致学习门槛过高 —— 后端项目本身的构建流程,套路比较死板,变化非常少,所以不需要太多的构建特性、构建规则。也就是说,灵活本身引入的多种用法、规则、特性对后端项目意义不大,为了构建工具本身的使用,去投入时间学习,本身性价比不高。上面说了,后端项目本身的构建流程是比较套路化的,需要进行一些强约束,去保证这种套路的可靠与稳定。而 Gradle 因为本身比较灵活的配置规则,反而失去了 Maven 的那种强约束,这就很可能因为失去了约束,从而造成团队在使用 Gradle 的时候,出现各种冲突和潜在的错误,造成项目构建的不稳定,这对后端项目来说是得不偿失的。二、开发框架2.1 Web 框架现在的 Web 项目开发,大部分都转向了 SpringBoot 了。使用 SpringBoot 有三个最大的好处:配置非常少,可以说是即插即用基于 Spring 构建,入手门槛非常低直接运行,不需要再考虑 Web 容器的问题SpringBoot 大部分人都很熟了,不再赘述了。springboot springcloud 项目大全 fhadmin.cn2.2 持久层框架项目开发中用到的持久层框架,基本有两类:Mybatis 系列衍生框架JPA 系列衍生框架在国内来讲,大部分持久层框架还是首选 Mybatis,貌似在国外大部分项目是用的 JPA 框架。在我看来,互联网项目、toC 的项目更适合 Mybatis,toB 的项目更适合 JPA。toC 项目的业务需求经常是灵活多变的,所以,往往它需要项目的技术也要跟着灵活多变,而 Mybatis 本身就是 SQL 的简单封装,很容易加表加字段、改 SQL。而 toB 项目则不一样,需求基本比较稳定,设计好的数据模型不会频繁变化,所以不太需要 Mybatis 的灵活性的,反而更需要对随意修改模型进行一系列的强约束。而这也是 JPA 自身的特性:非常规范,且有众多约束,要改 JPA 的数据模型成本比较大。因此,大家选持久层框架的时候,要看清项目的特性,根据实际情况选择用 Mybatis 还是 JPA。2.3 RPC 框架现在 Java 项目的架构,基本都在转向分布式架构。分布式系统的整合,核心就是 RPC,因此很多项目中都引入了 RPC 框架。RPC 框架,现在用的比较多的是 Dubbo 框架。Dubbo 性能非常好:很多 RPC 框架底层使用的通信协议是 HTTP,而 Dubbo 则选择了 TCP 协议作为通信协议。仅从性能上来说,TCP 的性能肯定要比 HTTP 好上许多。而且 Dubbo 自身还大量使用了 NIO 异步编程去进一步做了性能优化。所以,如果项目中需要使用 RPC,可以首先考虑 Dubbo 框架。三、中间件3.1 Web 服务器现在的 Java 开发,由于大部分使用了 SpringBoot,所以以前大家常用的什么 Tomcat、Jetty、Resin 等 Web 容器都不怎么单独部署使用了。但是,有一个 Web 容器反而还愈加兴旺起来,这就是 Nginx。Nginx 在 Java 项目开发里,地位是非常特殊的。它在 Java 项目架构里起到了两个作用:处理静态资源请求的 web 容器 ——Nginx 在 Java 项目中,专门负责处理对图片、html、js、css 等这类静态资源的 Http 请求。反向代理做分发 —— 除了做专门处理静态资源请求的 Web 容器之外,Nginx 同时还会把对 servlet、controller 等这些动态资源的请求,转发给后面的 SpringBoot 中内置的 Tomcat 容器。多说一句,因为反向代理这个特性,Nginx 后面会被部署上集群,Nginx 在转发请求的时候,同时也会做负载均衡的请求分发的反向代理。3.2 消息队列如今,大家做架构越来越趋向分布式架构。分布式架构里,常用的通信手段,除了网络请求,就是消息队列了。现在主流的消息队列框架有 RabbitMQ、RocketMQ、Kafka 等。我之前写过一篇 RabbitMQ 和 Kafka 对比的文章,RabbitMQ 性能虽然低一些,但是容易上手,更适合用在中小项目。另外,做金融领域相关项目,用消息队列的话可以优先考虑 RabbitMQ,原因有以下两点:RabbitMQ 是 AMQP 协议的实现,而 AMQP 协议本身就是来自于金融行业的软件专家们联手制定的,非常成熟和全面,已经成了工业标准。RabbitMQ 是 Erlang 写的,Erlang 的虚拟机对内存和 CPU 过载的保护非常成熟,也因此塑造了 Erlang 应用本身的可靠和健壮。大项目、非金融项目,大家可以在 RocketMQ、Kafka 这两者之间选择。RocketMQ 和 Kafka 差不多 90% 的功能和概念都是相通的,只是 RocketMQ 在 Kafka 理念的基础上做了一些改进,更适用的业务场景也更广泛。在流数据处理上,大家应该优先考虑 Kafka,原因是 Kafka 的流数据处理生态更加的完善周全。3.3 数据库互联网领域,主流数据库就是 MySQL。在一些传统行业,比如银行,Oracle 用的不少。Oracle 贵,互联网项目的一个特点就是数据库服务器数量贼多,如果用 Oracle 的话,成本太高了。而且大家越来越有版权意识,国家对这方面也抓的越来越紧,所以,在互联网领域几乎都在用 MySQL。使用 MySQL,常见的有 MHA 方案 ——MySQL 的高可用方案,基本架构就是一主两从。当主机出故障了,从机就会被提升为主机。3.4 外置缓存对于高并发的架构,外置缓存不可或缺,其中最最最常见的就是 Redis。之所以大家都采用 Redis 做外置缓存,原因有三点:Redis 本身性能非常好。Redis 有很多数据结构去适配不同的业务缓存需求。Redis 的集群高可用方案和分片存储的高性能方案相对成熟。以上,就是 Java 开发中经常遇到的主流技术工具了。由于篇幅所限,我也只列出了一些最核心(或者说每个人都会用到的)工具和中间件。有一些重要的中间件,我觉得不是所有人都会用到,就没有提及,比如 ElasticSearch、MongoDB、Zookeeper 等————————————————原文作者:FH-Admin转自链接:https://learnku.com/articles/66647版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请保留以上作者信息和原文链接。
-
jar 包启动 后面覆盖或追加参数1. D= : set a system property 设置系统属性。2. 官方解释:Set a system property value. If value is a string that contains spaces, you must enclose the string in double quotes:* 首先理解参数的意义,java 命令引入 jar 时可以 - cp 参数,但时 - cp 不能用通配符 (多个 jar 时什么烦要一个个写,不能.jar),面通常的 jar 都在同一目录,且多于 1 个。)-Djava.ext.dirs 就可以解决 多个 jar 的问题如:java -jar -Dserver.port=8555 -Dxxl.job.executor.port=9998 democlient-0.0.1-SNAPSHOT.jar实战案例:java -jar -Dserver.port=8555 -Dxxl.job.executor.port=9998 democlient-0.0.1-SNAPSHOT.jar
-
1 自然排序sorted ():自然排序,流中元素需实现 Comparable 接口 package com.entity;import lombok.*;@Data@ToString@AllArgsConstructor@NoArgsConstructorpublic class Student implements Comparable<Student> { private int id; private String name; private int age; @Override public int compareTo(Student ob) { return name.compareTo(ob.getName()); } @Override public boolean equals(final Object obj) { if (obj == null) { return false; } final Student std = (Student) obj; if (this == std) { return true; } else { return (this.name.equals(std.name) && (this.age == std.age)); } } @Override public int hashCode() { int hashno = 7; hashno = 13 * hashno + (name == null ? 0 : name.hashCode()); return hashno; } } 2 定制排序 sorted (Comparator com):定制排序,自定义 Comparator 排序器 3 升序 3.1 自然排序 list = list.stream().sorted().collect(Collectors.toList()); 3.2 定制排序 根据年龄降序排序。 list = list.stream().sorted(Comparator.comparing(Student::getAge)).collect(Collectors.toList());4 降序4.1 自然排序使用 Comparator 提供的 reverseOrder () 方法#list = list.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());4.2 定制排序根据年龄降序排序。list = list.stream().sorted(Comparator.comparing(Student::getAge).reversed()).collect(Collectors.toList());5 多字段排序先按姓名升序,姓名相同则按年龄升序。list = list.sorted(Comparator.comparing(Student::getName).thenComparing(Student::getAge)).collect(Collectors.toList转载自https://learnku.com/articles/69631
-
在开发中有些业务代码中需要判断当前是测试环境还是正式环境来做出不同的逻辑处理。 今天说一下如何在 static 静态方法中获取到 yml 配置文件中的配置值,从而获得当前的环境是测试环境还是正式环境。 首先创建一个类,编写所需方法 @Getterpublic class Result<T> { private static String env; public static void setEnv(String env) { Result.env = env; } public static String getEnv() { return Result.env; }}接着创建读取配置文件的类package com.sktk.keepAccount.config;import com.sktk.keepAccount.common.core.vo.Result;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;/** * 该类使用场景在:当需要在静态方法中获取配置时,因静态方法无法直接获取配置 * https://jingyan.baidu.com/article/215817f7c907835fdb142348.html */@Configurationpublic class EnvConfig { // 定义要读取的配置key @Value("${spring.profiles.active}") private String env; @Bean public int initStatic() { Result.setEnv(env); return 0; }}接下来,就可以直接在任意地方使用 Result.getEnv 来获取当前的环境了。
-
@Configuration 详解 场景 新建两个 bean:user 和 pet。 若要将这两个 bean 的实例注入到容器之中,在曾经 spring 阶段我们的做法是使用 xml 进行配置。 在项目的 resource 文件夹下新建 bean.xml 并进行配置,配置内容如下: <bean id="user01" class="xt.naru.learn.bean.User"> <property name="name" value="naru"></property> <property name="age" value="21"></property> </bean> <bean id="cat" class="xt.naru.learn.bean.Pet"> <property name="name" value="tomcat"></property> </bean>当项目需求越来越大时,我们时常会陷入 xml 配置地狱,因此为了解决这一问题, springboot 可以通过注解的方式进行实例注入而无需编写 xml。 使用 在 springboot 中我们不再使用 xml 进行配置。 修改为 新建配置类,并将配置类加上 Configuration 注解,在配置类中进行 bean 实例的注入。 代码如下: @Configuration public class myconfig { @Bean public User user01(){ return new User("naru",21); } @Bean public Pet tomcat(){ return new Pet("tomcat"); } }相比起 xml 的变化对比: 其实就是将 xml 中的配置项改为了注解,还是蛮好理解的。 检测注入结果 在 main 启动器中进行 beanname 的打印即可。 @SpringBootApplication public class MainApplication { public static void main(String[] args) { ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); String[] beannames = run.getBeanDefinitionNames(); for (String name : beannames){ System.out.println(name); } } }可以看到我们的 user01 以及 cat 成功注入。 一些特性 bean 实例名称自定义 直接在 bean 注解中加入想要自定义的 bean 的名称就可以了。 通过阅读上面的代码可发现,默认 bean 的名称和方法名相同。 看!原来叫做 cat 的 bean,现在名字变成了 tom-cat! 从容器中获取的实例默认为单实例 来试试吧,在 main 启动器中获取组件并打印其 boolean 结果: Pet Tom01 = run.getBean("tom-cat",Pet.class); Pet Tom02 = run.getBean("tom-cat",Pet.class); System.out.println("组件实例是否是同一个:"+ (Tom01 == Tom02));你将神奇的发现,是同一个实例: 配置类本身也是组件 直接来实践一下吧,通过以下代码进行打印: System.out.println(run.getBean(myconfig.class)); 结果显而易见: xt.naru.learn.config.myconfig$$EnhancerBySpringCGLIB$$f891ec73@15f193b8 proxyBeanMethods springboot2 的版本中,configration 注解中有这样一个方法: proxyBeanMethods 它的默认值为 true。 它是干嘛用的呢? 直译过来是代理 bean 方法的意思。 或许这句话有些难以理解,这样去想吧,我们在 myconfig 中进行了两个 bean 的实例化。 而有参、无参构造,每调用一次我们都可以生成新的实例化对象。那这是否意味着,我从容器中或许到的组件,每次都是不同的呢? 我们来进行测试: 调用两次无参构造,生成新对象。看看二者是否相同。 myconfig bean = run.getBean(myconfig.class); System.out.println(run.getBean(myconfig.class)); User user = bean.user01(); User user1= bean.user01(); System.out.println("user和user1是否为同一个实例:"+(user1 == user));会发现, 是相同的。 那么,proxyBeanMethods 到底做了什么呢? 先卖个关子,我们将它改为 false,再进行一次打印。看看结果。 会发现,myconfig 的 bean 名变了,实例也不再是同一个实例。 那么我们可以总结一下 proxyBeanMethods 所做的事情: **** 当 proxyBeanMethods 为 true 时: 将配置类变成增强型的代理类。保证 @bean 的方法被调用多少次,返回的组件均为单实例。 此模式被称为 Full 模式。即每次启动时,SpringBoot 总会检查这个组件是否在容器中存在,保持组件单实例。 保持组件单实例有什么好处呢? 保证组件之间的依赖关系不被破坏。 设想一下吧,如果不是单实例的组件,当我们的 User 小明,想要养一只叫 tom 的猫时,有很多只叫做 tom 的猫,他将分不清到底哪只属于他。 而单实例情况下,将不会出现这种问题,因为我们每次调用构造方法生成的实例对象一直都是同一只叫做 tom 的猫。 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用 Full 模式 当 proxyBeanMethods 为 false 时: 配置类为普通的类,此时每个 @Bean 方法被调用多少次返回的组件都是新创建的。 此模式被称为 Lite 模式,即 springboot 将不再会去判断组件是否已经在容器中存在,而是直接新建组件。(省去了检查流程,此模式下 springboot 启动会非常的快。) 总结 好了,现在让我们来总结一下关于 @Configuration 注解的要点。 存在为了解决怎样的问题? 方便 bean 实例的注入,我们不再需要使用 xml 进行配置了!只需要使用 java 代码就可以完成 bean 实例的注入了。 特性有哪些呢? 1、配置类里面使用 @Bean 标注在方法上给容器注册组件,默认也是单实例的 2、配置类本身也是组件 3、proxyBeanMethods:代理 bean 的方法 Full (proxyBeanMethods = true)、【保证每个 @Bean 方法被调用多少次返回的组件都是单实例的】 Lite (proxyBeanMethods = false)【每个 @Bean 方法被调用多少次返回的组件都是新创建的】 组件依赖必须使用 Full 模式默认。其他默认是否 Lite 模式 ———————————————— 原文作者:kudoForever 转自链接:https://learnku.com/articles/71782 版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请保留以上作者信息和原文链接。
-
一、前言 在企业的信息系统中,报表处理一直占比较重要的作用,本文将介绍一种生成 PDF 报表的 Java 组件–iText。通过在服务器端使用 Jsp 或 JavaBean 生成 PDF 报表,客户端采用超级连接显示或下载得到生成的报表,这样就很好的解决了 B/S 系统的报表处理问题。 二、iText 简介 iText 是著名的开放源码的站点 sourceforge 一个项目,是用于生成 PDF 文档的一个 java 类库。通过 iText 不仅可以生成 PDF 或 rtf 的文档,而且可以将 XML、Html 文件转化为 PDF 文件。 iText 的安装非常方便,在 www.lowagie.com/iText/download.html 网站上下载 iText.jar 文件后,只需要在系统的 CLASSPATH 中加入 iText.jar 的路径,在程序中就可以使用 iText 类库了。 * 三、建立第一个 PDF 文档 * 用 iText 生成 PDF 文档需要 5 个步骤: ①建立com.lowagie.text.Document对象的实例。 Document document = new Document(); ②建立一个书写器(Writer)与document对象关联,通过书写器(Writer)可以将文档写入到磁盘中。 PDFWriter.getInstance(document, new FileOutputStream("Helloworld.PDF")); ③打开文档。 document.open(); ④向文档中添加内容。 document.add(new Paragraph("Hello World")); ⑤关闭文档。 document.close(); 通过上面的 5 个步骤,就能产生一个 Helloworld.PDF 的文件,文件内容为”Hello World”。 建立 com.lowagie.text.Document 对象的实例 com.lowagie.text.Document 对象的构建函数有三个,分别是: public Document(); public Document(Rectangle pageSize); public Document(Rectangle pageSize, int marginLeft, int marginRight, int marginTop, int marginBottom); 构建函数的参数 pageSize 是文档页面的大小,对于第一个构建函数,页面的大小为 A4,同 Document (PageSize.A4) 的效果一样;对于第三个构建函数,参数 marginLeft、marginRight、marginTop、marginBottom 分别为左、右、上、下的页边距。 通过参数 pageSize 可以设定页面大小、面背景色、以及页面横向 / 纵向等属性。iText 定义了 A0-A10、AL、LETTER、HALFLETTER、_11x17、LEDGER、NOTE、B0-B5、ARCH_A-ARCH_E、FLSA 和 FLSE 等纸张类型,也可以通过 Rectangle pageSize = new Rectangle (144, 720); 自定义纸张。通过 Rectangle 方法 rotate () 可以将页面设置成横向。 书写器(Writer)对象 一旦文档 (document) 对象建立好之后,需要建立一个或多个书写器 (Writer) 对象与之关联。通过书写器 (Writer) 对象可以将具体文档存盘成需要的格式,如 com.lowagie.text.PDF.PDFWriter 可以将文档存成 PDF 文件,com.lowagie.text.html.HtmlWriter 可以将文档存成 html 文件。 设定文档属性 在文档打开之前,可以设定文档的标题、主题、作者、关键字、装订方式、创建者、生产者、创建日期等属性,调用的方法分别是: public boolean addTitle(String title) public boolean addSubject(String subject) public boolean addKeywords(String keywords) public boolean addAuthor(String author) public boolean addCreator(String creator) public boolean addProducer() public boolean addCreationDate() public boolean addHeader(String name, String content)public boolean setPageSize(Rectangle pageSize) public boolean add(Watermark watermark) public void removeWatermark() public void setHeader(HeaderFooter header) public void resetHeader() public void setFooter(HeaderFooter footer) public void resetFooter() public void resetPageCount() public void setPageCount(int pageN)package junit.test;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import java.net.MalformedURLException;import org.junit.Test;import com.lowagie.text.Document;import com.lowagie.text.DocumentException;import com.lowagie.text.Font;import com.lowagie.text.Image;import com.lowagie.text.Paragraph;import com.lowagie.text.pdf.BaseFont;import com.lowagie.text.pdf.PdfWriter;import com.lowagie.text.rtf.RtfWriter2;/** * 导出图片、word、pdf * * @author 林计钦 * @version 1.0 Feb 7, 2014 9:02:38 AM */public class PdfAndWordTest { /** * 导出pdf */ @Test public void exportPdf() { Document document=null; try { BaseFont bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);// 设置中文字体 Font headFont = new Font(bfChinese, 10, Font.NORMAL);// 设置字体大小 //第一步:创建一个document对象。 document = new Document(); //第二步:创建一个PdfWriter实例,将文件输出流指向一个文件。 PdfWriter.getInstance(document, new FileOutputStream("D:/test/123.pdf")); //第三步:打开文档。 document.open(); Paragraph title = new Paragraph("你好,Pdf!", headFont); //第四步:在文档中增加一个段落。 document.add(title); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (DocumentException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally{ if(document!=null){ //第五步:关闭文档。 document.close(); } } } /** * 导出word */ @Test public void exportWord() { Document document=null; try { document = new Document(); RtfWriter2.getInstance(document, new FileOutputStream("D:/test/word.doc")); document.open(); Paragraph title = new Paragraph("你好,Word!"); document.add(title); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (DocumentException e) { e.printStackTrace(); }finally{ if(document!=null){ document.close(); } } } /** * 导出图片 */ @Test public void exportImg() { Document document=null; try { BaseFont bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);// 设置中文字体 Font headFont = new Font(bfChinese, 10, Font.NORMAL);// 设置字体大小 document = new Document(); PdfWriter.getInstance(document, new FileOutputStream("D:/test/img.pdf")); //设定文档的作者 document.addAuthor("林计钦"); //测试无效 document.open(); document.add(new Paragraph("你好,Img!", headFont)); //读取一个图片 Image image = Image.getInstance("D:/test/1.gif"); //插入一个图片 document.add(image); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (DocumentException e) { e.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally{ if(document!=null){ document.close(); } } } }导出例子:转载自https://learnku.com/articles/73823
-
itext 工具 jar 包实现图片转 pdf,可以根据不同的方案来等比伸缩图片的大小,保证正常情况下图片不失真。以下是个人写的工具类:记得导入 itext.jar 包package com.bestone.lawaid.opm.controller;import com.lowagie.text.Document;import java.io.File;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import com.lowagie.text.DocumentException;import com.lowagie.text.Image;import com.lowagie.text.pdf.PdfWriter;public class TestCreatePDF { /** * 591227491@qq.com */ public void creatPDF(String imgName) { //创建一个文档对象 Document doc = new Document(); try { //定义输出文件的位置 PdfWriter.getInstance(doc, new FileOutputStream("D:/Test/"+imgName+".pdf")); //开启文档 doc.open(); //设定字体 为的是支持中文 //BaseFont bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); // Font FontChinese = new Font(bfChinese, 12, Font.NORMAL); //向文档中加入图片 String path = "D:/EclipseSpace/ImagFile"; //遍历文件下的文件 File file = new File(path); File [] files = file.listFiles(); for(int i=0;i<files.length;i++) { File file1 = files[i]; //根据后缀判断是否是图片 String[] imgTrue = file1.getName().split("\\."); if("png".equals(imgTrue[1])){ //取得图片~~~图片格式: System.out.println("---"+file1.getName()); Image jpg1 = Image.getInstance(path+"/"+imgTrue[0]+".png"); //原来的图片的路径 //获得图片的高度 float heigth=jpg1.getHeight(); float width=jpg1.getWidth(); System.out.println("heigth:"+imgTrue[0]+"----"+heigth); System.out.println("width:"+imgTrue[0]+"-----"+width); //合理压缩,h>w,按w压缩,否则按w压缩 //int percent=getPercent(heigth, width); //统一按照宽度压缩 int percent=getPercent2(heigth, width); System.out.println("--"+percent); //设置图片居中显示 jpg1.setAlignment(Image.MIDDLE); //直接设置图片的大小~~~~~~~第三种解决方案,按固定比例压缩 //jpg1.scaleAbsolute(210.0f, 297.0f); //按百分比显示图片的比例 jpg1.scalePercent(percent);//表示是原来图像的比例; //可设置图像高和宽的比例 //jpg1.scalePercent(50, 100); doc.add(jpg1); } } //关闭文档并释放资源 doc.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (DocumentException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } /** * 第一种解决方案 * 在不改变图片形状的同时,判断,如果h>w,则按h压缩,否则在w>h或w=h的情况下,按宽度压缩 * @param h * @param w * @return */ public int getPercent(float h,float w) { int p=0; float p2=0.0f; if(h>w) { p2=297/h*100; } else { p2=210/w*100; } p=Math.round(p2); return p; } /** * 第二种解决方案,统一按照宽度压缩 * 这样来的效果是,所有图片的宽度是相等的,自我认为给客户的效果是最好的 * @param args */ public int getPercent2(float h,float w) { int p=0; float p2=0.0f; p2=530/w*100; System.out.println("--"+p2); p=Math.round(p2); return p; } /** * 第三种解决方案,就是直接压缩,不安像素比例,全部压缩到固定值,如210*297 * * @param args */ public static void main(String[] args) { TestCreatePDF pt=new TestCreatePDF(); pt.creatPDF("imgCreatPdf"); }}
-
概述 今天我们要进一步的的学习如何自定义配置 Spring Security 我们已经多次提到了 WebSecurityConfigurerAdapter ,而且我们知道 Spring Boot 中的自动配置实际上是通过自动配置包下的 SecurityAutoConfiguration 总配置类上导入的 Spring Boot Web 安全配置类 SpringBootWebSecurityConfiguration 来配置的。 SpringBootWebSecurityConfiguration源码 package org.springframework.boot.autoconfigure.security.servlet; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration( proxyBeanMethods = false ) @ConditionalOnClass({WebSecurityConfigurerAdapter.class}) @ConditionalOnMissingBean({WebSecurityConfigurerAdapter.class}) @ConditionalOnWebApplication( type = Type.SERVLET ) public class SpringBootWebSecurityConfiguration { public SpringBootWebSecurityConfiguration() { } @Configuration( proxyBeanMethods = false ) @Order(2147483642) static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter { DefaultConfigurerAdapter() { } } } 常用方法 WebSecurityConfigurerAdapter类图 常用配置 下面贴一下WebSecurityConfigurerAdapter在项目中的常用配置 package cn.wideth.framework.config; import cn.wideth.framework.security.handle.AuthenticationEntryPointImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 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; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.web.filter.CorsFilter; import cn.wideth.framework.security.filter.JwtAuthenticationTokenFilter; import cn.wideth.framework.security.handle.LogoutSuccessHandlerImpl; /** * spring security配置 * */ @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 自定义用户认证逻辑 */ @Autowired private UserDetailsService userDetailsService; /** * 认证失败处理类 */ @Autowired private AuthenticationEntryPointImpl unauthorizedHandler; /** * 退出处理类 */ @Autowired private LogoutSuccessHandlerImpl logoutSuccessHandler; /** * token认证过滤器 */ @Autowired private JwtAuthenticationTokenFilter authenticationTokenFilter; /** * 跨域过滤器 */ @Autowired private CorsFilter corsFilter; /** * 解决 无法直接注入 AuthenticationManager * * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 强散列哈希加密实现 */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } /** * 身份认证接口 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //在这里关联数据库和security auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } /** * anyRequest | 匹配所有请求路径 * access | SpringEl表达式结果为true时可以访问 * anonymous | 匿名可以访问 * denyAll | 用户不能访问 * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 * hasRole | 如果有参数,参数表示角色,则其角色可以访问 * permitAll | 用户可以任意访问 * rememberMe | 允许通过remember-me登录的用户访问 * authenticated | 用户登录后可访问 */ @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity // CSRF禁用,因为不使用session .csrf().disable() // 认证失败处理类 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() // 基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 过滤请求 .authorizeRequests() // 对于登录login 验证码captchaImage 允许匿名访问 .antMatchers("/login", "/captchaImage").anonymous() .antMatchers( HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js" ).permitAll() .antMatchers("/profile/**").permitAll() .antMatchers("/common/download**").permitAll() .antMatchers("/common/download/resource**").permitAll() .antMatchers("/swagger-ui.html").permitAll() .antMatchers("/swagger-resources/**").permitAll() .antMatchers("/webjars/**").permitAll() .antMatchers("/*/api-docs").permitAll() .antMatchers("/druid/**").permitAll() .antMatchers("/flowable/**").permitAll() .antMatchers("/socket/**").permitAll() .antMatchers("/api/common/**").permitAll() .antMatchers("/api/contract/**").permitAll() .antMatchers("/api/project/**").permitAll() .antMatchers("/api/document/**").permitAll() .antMatchers("/api/purchase/**").permitAll() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated() .and() .headers().frameOptions().disable(); httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 添加JWT filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加CORS filter httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class); httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class); } /*** * 核心过滤器配置方法 * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { super.configure(web); } } 相信已经有人注意到了上面 WebSecurityConfigurerAdapter 中我覆写(@Override)了三个configure方法,我们一般会通过自定义配置这三个方法来自定义我们的安全访问策略。 认证管理器配置方法 AuthenticationManagerBuilder(身份验证管理生成器) void configure(AuthenticationManagerBuilder auth) 用来配置认证管理器AuthenticationManager。说白了就是所有 UserDetails 相关的它都管,包含 PasswordEncoder 密码等。如果你不清楚可以通过 Spring Security 中的 UserDetail 进行了解。本文对 AuthenticationManager 不做具体分析讲解,后面会有专门的文章来讲这个东西 。 常见用法 /** * 身份认证接口 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } 核心过滤器配置方法 WebSecurity(WEB安全) void configure(WebSecurity web) 用来配置 WebSecurity 。而 WebSecurity 是基于 Servlet Filter 用来配置 springSecurityFilterChain 。而 springSecurityFilterChain 又被委托给了 Spring Security 核心过滤器 Bean DelegatingFilterProxy 。 相关逻辑你可以在 WebSecurityConfiguration 中找到。我们一般不会过多来自定义 WebSecurity , 使用较多的使其ignoring() 方法用来忽略 Spring Security 对静态资源的控制。 安全过滤器链配置方法 HttpSecurity(HTTP请求安全处理) void configure(HttpSecurity http) 这个是我们使用最多的,用来配置 HttpSecurity 。 HttpSecurity 用于构建一个安全过滤器链 SecurityFilterChain 。SecurityFilterChain 最终被注入核心过滤器 。 HttpSecurity 有许多我们需要的配置。我们可以通过它来进行自定义安全访问策略。所以我们单独开一章来讲解这个东西。 HttpSecurity 配置 HttpSecurity 是后面几篇文章的重点,我们将实际操作它来实现一些实用功能。所以本文要着重介绍它。 HttpSecurity 类图 默认配置 默认配置 protected void configure(HttpSecurity http) throws Exception { this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); ((HttpSecurity)((HttpSecurity((AuthorizedUrl) http. authorizeRequests(). anyRequest()).authenticated(). and()). formLogin(). and()). httpBasic(); } 上面是 Spring Security 在 Spring Boot 中的默认配置。通过以上的配置,你的应用具备了一下的功能: 所有的请求访问都需要被授权。 使用 form 表单进行登陆(默认路径为/login),也就是前几篇我们见到的登录页。 防止 CSRF 攻击、 XSS 攻击。 启用 HTTP Basic 认证 常用方法解读 HttpSecurity 使用了builder 的构建方式来灵活制定访问策略。最早基于 XML 标签对 HttpSecurity 进行配置。现在大部分使用 javaConfig方式。常用的方法解读如下: 方法 说明 openidLogin() 用于基于 OpenId 的验证 headers() 将安全标头添加到响应,比如说简单的 XSS 保护 cors() 配置跨域资源共享( CORS ) sessionManagement() 允许配置会话管理 portMapper() 允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443 jee() 配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理 x509() 配置基于x509的认证 rememberMe() 允许配置“记住我”的验证 authorizeRequests() 允许基于使用HttpServletRequest限制访问 requestCache() 允许配置请求缓存 exceptionHandling() 允许配置错误处理 securityContext() 在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将自动应用 servletApi() 将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用 csrf() 添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用 logout() 添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success” anonymous() 允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS” formLogin() 指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面 oauth2Login() 根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证 requiresChannel() 配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射 httpBasic() 配置 Http Basic 验证 addFilterBefore() 在指定的Filter类之前添加过滤器 addFilterAt() 在指定的Filter类的位置添加过滤器 addFilterAfter() 在指定的Filter类的之后添加过滤器 and() 连接以上策略的连接器,用来组合安全策略。实际上就是"而且"的意思 本文小结 本文详细介绍了WebSecurityConfigurerAdapter相关的知识与内容,后续会对HttpSecurity相关的知识进行详细介绍,这个也是在平时的开发中使用的最多的地方。 ———————————————— 原文链接:https://blog.csdn.net/qq_31960623/article/details/120829127
-
表单验证码登录表单登录验证码验证,一般在用户名、密码提交登录前,添加过滤器,先验证验证码的有效性(开发中一般用的这种),然后再提交用户名、密码。文章下面还会使用另一种方法:验证码和用户名、密码一起同时提交登录。在Spring Security中,两种实现方式为:使用自定义过滤器(Filter),在提交用户名、密码前,先验证验证码的有效性验证码和用户名、密码一起在Spring Security中进行验证一、验证码生成新建一个包validateCode放置所有验证码相关的类。1.1、验证码实体对象@Datapublic class ValidateCode { private BufferedImage image; private String code; private LocalDateTime expireTime; /** * @param expirtSecond 设置过期时间,单位秒 */ public ValidateCode(BufferedImage image, String code, int expirtSecond){ this.image = image; this.code = code; // expireSecond秒后的时间 this.expireTime = LocalDateTime.now().plusSeconds(expirtSecond); } /** * 验证码是否过期 */ public boolean isExpired(){ return LocalDateTime.now().isAfter(expireTime); }}1.2、生成验证码:@Servicepublic class ValidateCodeCreateService { public ValidateCode createImageCode() { // 宽度 // 从请求参数中获取数据,否则,读取配置文件配置值 int width = 80; // 高度 int height = 30; // 认证码长度 int charLength = 4; // 过期时间(秒) int expireTime = 60; BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); // 获取图形上下文 Graphics g = image.getGraphics(); // 生成随机类 Random random = new Random(); // 设定背景色 g.setColor(getRandColor(200, 250)); g.fillRect(0, 0, width, height); // 设定字体 g.setFont(new Font("Times New Roman", Font.PLAIN, 18)); // 随机产生155条干扰线,使图象中的认证码不易被其它程序探测到 g.setColor(getRandColor(160, 200)); for (int i = 0; i < 155; i++) { int x = random.nextInt(width); int y = random.nextInt(height); int xl = random.nextInt(12); int yl = random.nextInt(12); g.drawLine(x, y, x + xl, y + yl); } // 取随机产生的认证码 String sRand = ""; for (int i = 0; i < charLength; i++) { String rand = String.valueOf(random.nextInt(10)); sRand += rand; // 将认证码显示到图象中 g.setColor(new Color(20 + random.nextInt(110), 20 + random .nextInt(110), 20 + random.nextInt(110))); // 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成 g.drawString(rand, 13 * i + 6, 16); } // 图象生效 g.dispose(); return new ValidateCode(image, sRand, expireTime); } /** * 给定范围获得随机颜色 */ private Color getRandColor(int fc, int bc) { Random random = new Random(); if (fc > 255) { fc = 255; } if (bc > 255) { bc = 255; } int r = fc + random.nextInt(bc - fc); int g = fc + random.nextInt(bc - fc); int b = fc + random.nextInt(bc - fc); return new Color(r, g, b); }}验证码图片生成接口@RestControllerpublic class ValidateCodeController { @Autowired private ValidateCodeCreateService validateCodeCreateService; @GetMapping("/get-validate-code") public void getImageCode(HttpServletRequest request, HttpServletResponse response) throws IOException { // 创建验证码 ValidateCode validateCode = validateCodeCreateService.createImageCode(); // 将验证码放到session中(也可放在Redis中,可设置过期时间) request.getSession().setAttribute("validate-code", validateCode); // 返回验证码给前端 ImageIO.write(validateCode.getImage(), "JPEG", response.getOutputStream()); }}二、登录页面配置修改resources/templates下登录页面,添加验证码选项:<!DOCTYPE html><html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"><head> <meta charset="UTF-8"> <title>登录页面</title></head><body><form th:action="@{/my-login}" method="post"> <div><label> 用户名 : <input type="text" name="username"/> </label></div> <div><label> 密码: <input type="password" name="password"/> </label></div> <div>验证码: <input type="text" class="form-control" name="validateCode" required="required" placeholder="验证码"> <img src="get-validate-code" title="看不清,请点我" onclick="refresh(this)" /> </div> <button type="submit" class="btn">登录</button></form><script> function refresh(obj) { obj.src = "get-validate-code"; }</script></body></html>WebSecurityConfig配置:@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { // ... @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() // 获取验证码允许匿名访问 .antMatchers("/get-validate-code").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/user-login").permitAll() .loginProcessingUrl("/my-login") // ... }}正常项目已经配置好,启动项目,访问localhost:8080/hello跳转到自定义的登录页面:可以看到,这里Spring Security默认只验证用户名、密码,没有验证验证码是否正确。所以下面开始实现登录验证码验证,有以下两种种实现方式:使用自定义过滤器(Filter),在校验用户名、密码前判断验证码合法性,验证通过后,通过用户名和密码登录验证码和用户名、密码一起提交到后台登录三、过滤器验证原理:在 Spring Security 处理登录请求前,先验证验证码,如果正确,放行去登录;如果不正确,返回失败处理。2.1、验证码过滤器自定义一个过滤器,OncePerRequestFilter(该Filter保证每次请求只过滤一次):public class ValidateCodeFilter extends OncePerRequestFilter { // URL正则匹配 private static final PathMatcher pathMatcher = new AntPathMatcher(); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 只有登录请求‘/authentication/form’,并且为'post'请求时,才校验 if ("POST".equals(request.getMethod()) && pathMatcher.match("/anthentication/form", request.getServletPath())) { try { codeValidate(request); } catch (ValidateCodeException e) { // 验证码不通过,跳到错误处理器处理 response.setContentType("application/json;charset=UTF-8"); response.getWriter().append( new ObjectMapper().createObjectNode() .put("status", "500") .put("msg", e.getMessage()) .toString()); // 异常后,不执行后面 return; } } doFilter(request, response, filterChain); } private void codeValidate(HttpServletRequest request) throws JsonProcessingException { // 获取到传入的验证码 String codeInRequest = request.getParameter("validateCode"); ValidateCode codeInSession = (ValidateCode) request.getSession(false).getAttribute("validate-code"); // 校验验证码是否正确 if (StringUtils.isEmpty(codeInRequest)) { throw new ValidateCodeException("验证码的值不能为空"); } if (codeInSession == null) { throw new ValidateCodeException("验证码不存在"); } if (codeInSession.isExpired()) { throw new ValidateCodeException("验证码已过期"); } if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) { throw new ValidateCodeException("验证码不匹配"); } // 校验正确后,移除session中验证码 request.getSession(false).removeAttribute("validate-code"); }}class ValidateCodeException extends AuthenticationException { public ValidateCodeException(String message) { super(message); }}2.2、配置过滤器Spring Security 对于用户名/密码登录验证是通过 UsernamePasswordAuthenticationFilter 处理的,只要在它之前执行验证码过滤器即可:@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... @Override protected void configure(HttpSecurity http) throws Exception { http // 验证码过滤器在用户名、密码校验前 .addFilterBefore(new ValidateCodeFilter(), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers("/get-validate-code").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/user-login").permitAll() .loginProcessingUrl("/my-login") }}2.4、运行程序启动项目,访问localhost:8080/login到登录页,随机输入内容登录:四、和用户名、密码同时验证上面使用过滤器实现了验证码功能,该过滤器是先验证验证码,验证成功就让 Spring Security 验证用户名和密码。如果用户登录是需要多个登录字段,不单单是用户名和密码,这时候可以考虑自定义 Spring Security 的验证逻辑。3.1、WebAuthenticationDetailsSpring security 默认只会处理用户名和密码信息,如果我们需要增加验证码字段验证,则需要拿到验证码。而WebAuthenticationDetails类提供了获取用户登录时携带的额外信息的功能,可以通过该类拿到验证码。所以我们需要自定义类继承该类拿到验证码:public class CustomWebAuthenticationDetails extends WebAuthenticationDetails { @Getter // 设置getter方法,以便拿到验证码 private final String validateCode; public CustomWebAuthenticationDetails(HttpServletRequest request) { super(request); // 拿页面传来的验证码 validateCode = request.getParameter("validateCode"); }}3.2、AuthenticationDetailSource把自定义CustomWebAuthenticationDetails,放入 AuthenticationDetailsSource 中来替换原本的 WebAuthenticationDetails ,因此还得实现自定义 CustomAuthenticationDetailsSource ,设置为我们自定义的 CustomWebAuthenticationDetails:@Component("authenticationDetailsSource")public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> { @Override public WebAuthenticationDetails buildDetails(HttpServletRequest httpRequest) { return new CustomWebAuthenticationDetails(httpRequest); }}3.3、Spring Security配置将 CustomAuthenticationDetailsSource 注入Spring Security中,替换掉默认的 AuthenticationDetailsSource。修改 WebSecurityConfig,将其注入,然后在config()中使用 authenticationDetailsSource(authenticationDetailsSource)方法来指定它。@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 省略其他 @Autowired private AuthenticationDetailsSource authenticationDetailsSource; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/get-validate-code").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/user-login").permitAll() .loginProcessingUrl("/my-login") .authenticationDetailsSource(authenticationDetailsSource); http.csrf().disable(); }}3.4、AuthenticationProvider通过自定义CustomWebAuthenticationDetails和CustomAuthenticationDetailsSource将验证码和用户名、密码一起加入了Spring Security中,但默认的认证中还不会对验证码进行校验,需要重写UserDetailsAuthenticationProvider进行校验。@Componentpublic class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { @Autowired private CustomUserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { // 获取登录提交的用户名和密码 String inputPassword = (String) authentication.getCredentials(); // 获取登录提交的验证码 CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails(); String validateCode = details.getValidateCode(); // 验证码校验 checkValidateCode(validateCode); // 验证用户名 if (!passwordEncoder.matches(inputPassword, userDetails.getPassword())) { throw new BadCredentialsException("密码错误"); } } @Override protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException { return userDetailsService.loadUserByUsername(username); } private void checkValidateCode(String validateCode) { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); ValidateCode codeInSession = (ValidateCode) request.getSession(false).getAttribute("validate-code"); if (StringUtils.isEmpty(validateCode)) { throw new ValidateCodeException("验证码的值不能为空"); } if (codeInSession == null) { throw new ValidateCodeException("验证码不存在"); } if (codeInSession.isExpired()) { // 移除session中验证码 request.getSession(false).removeAttribute("validate-code"); throw new ValidateCodeException("验证码已过期"); } if (!StringUtils.equals(codeInSession.getCode(), validateCode)) { throw new ValidateCodeException("验证码不匹配"); } // 移除session中验证码 request.getSession(false).removeAttribute("validate-code"); }}class ValidateCodeException extends AuthenticationException { ValidateCodeException(String message) { super(message); }}在 WebSecurityConfig 中将其注入,并在 configure(AuthenticationManagerBuilder auth) 方法中通过 auth.authenticationProvider() 指定使用@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomAuthenticationProvider authenticationProvider; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // auth.userDetailsService(userDetailsService); auth.authenticationProvider(authenticationProvider); }}启动程序测试即可。作者:zenghi原文链接:https://www.jianshu.com/p/c4607c27d062
-
前言在SpringSecurity的默认登录支持组件formLogin中没有提供图形验证码的支持,目前大多数的方案都是通过新增Filter来实现。filter的方式可以实现功能,但是没有优雅的解决, 需要重新维护一套和登录相关的url,例如:loginProccessUrl,loginFailUrl,loginSuccessUrl,从软件设计角度来讲功能没有内聚。下面为大家介绍一种优雅的解决方案。解决思路先获取验证码判断图形验证码先要获取到验证码,在UsernamePasswordAuthenticationToken(UPAT)中没有字段来存储验证码,重写UPAT成本太高。可以从details字段中入手,将验证码放在details中。判断验证码是否正确UPAT的认证是在DaoAuthenticationProvider中完成的,如果需要判断验证码直接修改是成本比较大的方式,可以新增AuthenticationProvider来对验证码新增验证。输出验证码常规超过可以通过Controller来输出,但是验证码的管理需要统一,防止各种sessionKey乱飞。代码实现新增验证码容器:CaptchaAuthenticationDetailspublic class CaptchaAuthenticationDetails extends WebAuthenticationDetails { private final String DEFAULT_CAPTCHA_PARAMETER_NAME = "captcha"; private String captchaParameter = DEFAULT_CAPTCHA_PARAMETER_NAME; /** * 用户提交的验证码 */ private String committedCaptcha; /** * 预设的验证码 */ private String presetCaptcha; private final WebAuthenticationDetails webAuthenticationDetails; public CaptchaAuthenticationDetails(HttpServletRequest request) { super(request); this.committedCaptcha = request.getParameter(captchaParameter); this.webAuthenticationDetails = new WebAuthenticationDetails(request); } public boolean isCaptchaMatch() { if (this.presetCaptcha == null || this.committedCaptcha == null) { return false; } return this.presetCaptcha.equalsIgnoreCase(committedCaptcha); } getter ... setter ... }这个类主要是用于保存验证码验证码获取:CaptchaAuthenticationDetailsSourcepublic class CaptchaAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, CaptchaAuthenticationDetails> { private final CaptchaRepository<HttpServletRequest> captchaRepository; public CaptchaAuthenticationDetailsSource(CaptchaRepository<HttpServletRequest> captchaRepository) { this.captchaRepository = captchaRepository; } @Override public CaptchaAuthenticationDetails buildDetails(HttpServletRequest httpServletRequest) { CaptchaAuthenticationDetails captchaAuthenticationDetails = new CaptchaAuthenticationDetails(httpServletRequest); captchaAuthenticationDetails.setPresetCaptcha(captchaRepository.load(httpServletRequest)); return captchaAuthenticationDetails; } }根据提交的参数构建CaptchaAuthenticationDetails,用户提交的验证码(committedCaptcha)从request中获取,预设的验证码(presetCaptcha)从验证码仓库(CaptchaRepostory)获取验证码仓库实现SessionCaptchaRepositorypublic class SessionCaptchaRepository implements CaptchaRepository<HttpServletRequest> { private static final String CAPTCHA_SESSION_KEY = "captcha"; /** * the key of captcha in session attributes */ private String captchaSessionKey = CAPTCHA_SESSION_KEY; @Override public String load(HttpServletRequest request) { return (String) request.getSession().getAttribute(captchaSessionKey); } @Override public void save(HttpServletRequest request, String captcha) { request.getSession().setAttribute(captchaSessionKey, captcha); } /** * @return sessionKey */ public String getCaptchaSessionKey() { return captchaSessionKey; } /** * @param captchaSessionKey sessionKey */ public void setCaptchaSessionKey(String captchaSessionKey) { this.captchaSessionKey = captchaSessionKey; } }这个验证码仓库是基于Session的,如果想要基于Redis只要实现CaptchaRepository即可。对验证码进行认证CaptchaAuthenticationProviderpublic class CaptchaAuthenticationProvider implements AuthenticationProvider { private final Logger log = LoggerFactory.getLogger(this.getClass()); @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) authentication; CaptchaAuthenticationDetails details = (CaptchaAuthenticationDetails) authenticationToken.getDetails(); if (!details.isCaptchaMatch()) { //验证码不匹配抛出异常,退出认证 if (log.isDebugEnabled()) { log.debug("认证失败:验证码不匹配"); } throw new CaptchaIncorrectException("验证码错误"); } //替换details authenticationToken.setDetails(details.getWebAuthenticationDetails()); //返回空交给下一个provider进行认证 return null; } @Override public boolean supports(Class<?> aClass) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass); } }SpringSecurity配置@EnableWebSecurity public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Bean public CaptchaRepository<HttpServletRequest> sessionCaptchaRepository() { return new SessionCaptchaRepository(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest() .authenticated() .and() .formLogin() .loginProcessingUrl("/login") .loginPage("/login.html") .authenticationDetailsSource(new CaptchaAuthenticationDetailsSource(sessionCaptchaRepository())) .failureUrl("/login.html?error=true") .defaultSuccessUrl("/index.html") .and() .authenticationProvider(new CaptchaAuthenticationProvider()) .csrf() .disable(); } @Override public void configure(WebSecurity web) { web.ignoring() .mvcMatchers("/captcha", "/login.html"); } }将CaptchaAuthenticationProvider加入到认证链条中,重新配置authenticationDetailsSource提供图形验证码接口@Controller public class CaptchaController { private final CaptchaRepository<HttpServletRequest> captchaRepository; @Autowired public CaptchaController(CaptchaRepository<HttpServletRequest> captchaRepository) { this.captchaRepository = captchaRepository; } @RequestMapping("/captcha") public void captcha(HttpServletRequest request, HttpServletResponse response) throws IOException { RandomCaptcha randomCaptcha = new RandomCaptcha(4); captchaRepository.save(request, randomCaptcha.getValue()); CaptchaImage captchaImage = new DefaultCaptchaImage(200, 60, randomCaptcha.getValue()); captchaImage.write(response.getOutputStream()); } }将生成的随机验证码(RandomCaptcha)保存到验证码仓库(CaptchaRepository)中,并将验证码图片(CaptchaImage)输出到客户端。至此整个图形验证码认证的全流程已经结束。
-
验证码等额外数据获取逻辑 新增OauthAddUserDetails继承WebAuthenticationDetails,这里从request里拿到额外的参数。对比验证码,我们需要有一个接口去更新验证码,更新完放到session里,只有这个地方可以拿到request,所以我就在这里把session里的验证码也拿出来。 package com.ump.oauth.detail; import java.util.Collection; import javax.servlet.http.HttpServletRequest; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetails; import com.ump.domain.AppUser; public class OauthAddUserDetails extends WebAuthenticationDetails{ /** * */ private static final long serialVersionUID = 6975601077710753878L; private final String token; private final String sessionToken; public OauthAddUserDetails(HttpServletRequest request) { super(request); token = request.getParameter("imgtoken"); sessionToken = (String) request.getSession().getAttribute("imgtoken"); } public String getToken() { return token; } public String getSessionToken() { return sessionToken; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(super.toString()).append("; Token: ").append(this.getToken()); return sb.toString(); } } 增加AuthenticationDetailsSource AuthenticationDetailsSource是需要配置在spring security里的WebSecurityConfigurerAdapter子类里。 package com.ump.oauth.part; import javax.servlet.http.HttpServletRequest; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.stereotype.Component; import com.ump.oauth.detail.OauthAddUserDetails; @Component("oauthAuthenticationDetailsSource") public class OauthAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> { @Override public WebAuthenticationDetails buildDetails(HttpServletRequest context) { return new OauthAddUserDetails(context); } } AuthenticationProvider的认证逻辑增加验证码处理 对比验证码,这里我们做验证码的对比工作。部分代码是上一篇讲到的,按照自己逻辑删减即可。 package com.ump.oauth.part; import java.util.Collection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; import com.ump.oauth.detail.OauthAddUserDetails; import com.ump.oauth.detail.OauthUserDetails; @Component public class OauthAuthenticationProvider implements AuthenticationProvider { @Autowired private OauthUserDetailsService oauthUserDetailsService; /** * 自定义验证方式 */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { //验证码等校验 OauthAddUserDetails details = (OauthAddUserDetails) authentication.getDetails(); System.out.println(details.getToken() + "+++++++++++++++++" + details.getSessionToken()); if (!details.getToken().equalsIgnoreCase(details.getSessionToken())) { throw new BadCredentialsException("验证码错误。"); } //用户名密码校验 OauthUserDetails oauthUserDetails = (OauthUserDetails) oauthUserDetailsService .loadUserByUsername(authentication.getName()); System.out.println(authentication.getName() + "+++++++++++++++++" + authentication.getCredentials()); if (!oauthUserDetails.getUserName().equals(authentication.getName()) || !oauthUserDetails.getPassword().equals(authentication.getCredentials())) { throw new BadCredentialsException("用户名或密码错误。"); } Collection<? extends GrantedAuthority> authorities = oauthUserDetails.getAuthorities(); return new UsernamePasswordAuthenticationToken(oauthUserDetails.getUsername(), oauthUserDetails.getPassword(), authorities); } @Override public boolean supports(Class<?> arg0) { return true; } } 配置authenticationDetailsSource 只需要一个autowired和HttpSecurity一个.authenticationDetailsSource(authenticationDetailsSource)即可。我这里代码多,删减即可。 package com.ump.oauth.config; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.approval.ApprovalStore; import org.springframework.security.oauth2.provider.approval.TokenApprovalStore; import org.springframework.security.oauth2.provider.approval.TokenStoreUserApprovalHandler; import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.authentication.WebAuthenticationDetails; import com.ump.oauth.part.OauthAuthenticationProvider; @Configuration @EnableWebSecurity public class OAuth2SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired @Qualifier("myClientDetailsService") private ClientDetailsService clientDetailsService; @Autowired private OauthAuthenticationProvider oauthAuthenticationProvider; @Autowired @Qualifier("oauthAuthenticationDetailsSource") private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(oauthAuthenticationProvider); } @Override public void configure(HttpSecurity http) throws Exception { SimpleUrlAuthenticationFailureHandler hander = new SimpleUrlAuthenticationFailureHandler(); hander.setUseForward(true); hander.setDefaultFailureUrl("/authlogin.jsp"); http .csrf().disable() .authorizeRequests() .antMatchers("/oauth/token") .permitAll().and() .formLogin().loginPage("/authlogin.jsp") .usernameParameter("userName").passwordParameter("userPwd") .authenticationDetailsSource(authenticationDetailsSource) // .loginProcessingUrl("/login").failureUrl("/index1.jsp") .loginProcessingUrl("/login").failureHandler(hander) .and().logout().logoutUrl("/logout"); http.authorizeRequests().antMatchers("/user/**").authenticated(); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean @Autowired public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore){ TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler(); handler.setTokenStore(tokenStore); handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService)); handler.setClientDetailsService(clientDetailsService); return handler; } @Bean @Autowired public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception { TokenApprovalStore store = new TokenApprovalStore(); store.setTokenStore(tokenStore); return store; } } 这样就可以了,需要其他信息验证,我们就从request里面拿东西就是了。 ———————————————— 原文链接:https://blog.csdn.net/feiyangtianyao/article/details/78728147
-
删除代码:Set keys = redisTemplate.keys("*");redisTemplate.delete(keys);如果删除无效,大概率是redistemplate默认的k使用的是jdk序列化,导致无法正确识别“*”引起的,解决办法:K改为String序列化例如,在项目启动类下面加这个@Beanpublic RedisTemplate redisTemplate(@Autowired RedisTemplate redisTemplate){redisTemplate.setKeySerializer(redisTemplate.getStringSerializer());//@bean方式将K设为String序列化后,V会自动转为String序列化。// 为了保持Object类型的V值,故我这边需将V设回默认的jdk的V,redisTemplate.setValueSerializer(redisTemplate.getDefaultSerializer());return redisTemplate;}————————————————原文链接:https://blog.csdn.net/weixin_44647371/article/details/123337925
-
目的以前在github上找了一个开源的项目,改了改缓存的扩展,让其支持在缓存注解上控制缓存失效时间以及多长时间主动在后台刷新缓存以防止缓存失效( Spring Cache扩展:注解失效时间+主动刷新缓存 )。示意图以下:css那篇文章存在两个问题:html全部的配置是创建在修改缓存容器的名称基础上,与传统缓存注解的写法有所区别,后续维护成本会增长;后台刷新缓存时会存在并发更新的问题另外,当时项目是基于springboot 1.x,如今springboot2.0对缓存这块有所调整,须要从新适配。javaSpringBoot 2.0对缓存的变更RedisCacheManager看看下面的构造函数,与1.x有比较大的改动,这里就不贴代码了。gitpublic RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) { this(cacheWriter, defaultCacheConfiguration, true); } public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, String... initialCacheNames) { this(cacheWriter, defaultCacheConfiguration, true, initialCacheNames); }RedisCache既然上层的RedisCacheManager变更了,这里也就跟着变了。githubprotected RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) { super(cacheConfig.getAllowCacheNullValues()); Assert.notNull(name, "Name must not be null!"); Assert.notNull(cacheWriter, "CacheWriter must not be null!"); Assert.notNull(cacheConfig, "CacheConfig must not be null!"); this.name = name; this.cacheWriter = cacheWriter; this.cacheConfig = cacheConfig; this.conversionService = cacheConfig.getConversionService(); }方案针对上述的三个问题,分别应对。web将缓存配置从注解上转移到初始化缓存的地方建立一个类用来描述缓存配置,避免在缓存注解上经过很是规手段完成特定的功能。redispublic class CacheItemConfig implements Serializable { /** * 缓存容器名称 */ private String name; /** * 缓存失效时间 */ private long expiryTimeSecond; /** * 当缓存存活时间达到此值时,主动刷新缓存 */ private long preLoadTimeSecond; }具体的应用参见下面两步。spring适配springboot 2.0修改CustomizedRedisCacheManager构造函数:缓存public CustomizedRedisCacheManager( RedisConnectionFactory connectionFactory, RedisOperations redisOperations, List<CacheItemConfig> cacheItemConfigList)参数说明:springbootconnectionFactory,这是一个redis链接工厂,用于后续操做redisredisOperations,这个一个redis的操做实例,具体负责执行redis命令cacheItemConfigList,这是缓存的配置,好比名称,失效时间,主动刷新时间,用于取代在注解上个性化的配置。具体实现以下:核心思路就是调用RedisCacheManager的构造函数。private RedisCacheWriter redisCacheWriter; private RedisCacheConfiguration defaultRedisCacheConfiguration; private RedisOperations redisOperations; public CustomizedRedisCacheManager( RedisConnectionFactory connectionFactory, RedisOperations redisOperations, List<CacheItemConfig> cacheItemConfigList) { this( RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory), RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(30)), cacheItemConfigList .stream() .collect(Collectors.toMap(CacheItemConfig::getName,cacheItemConfig -> { RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration .defaultCacheConfig() .entryTtl(Duration.ofSeconds(cacheItemConfig.getExpiryTimeSecond())) .prefixKeysWith(cacheItemConfig.getName()); return cacheConfiguration; })) ); this.redisOperations=redisOperations; CacheContainer.init(cacheItemConfigList); } public CustomizedRedisCacheManager( RedisCacheWriter redisCacheWriter ,RedisCacheConfiguration redisCacheConfiguration, Map<String, RedisCacheConfiguration> redisCacheConfigurationMap) { super(redisCacheWriter,redisCacheConfiguration,redisCacheConfigurationMap); this.redisCacheWriter=redisCacheWriter; this.defaultRedisCacheConfiguration=redisCacheConfiguration; }因为咱们须要主动刷新缓存,因此须要重写getCache方法:主要就是将RedisCache构造函数所须要的参数传递过去。@Override public Cache getCache(String name) { Cache cache = super.getCache(name); if(null==cache){ return cache; } CustomizedRedisCache redisCache= new CustomizedRedisCache( name, this.redisCacheWriter, this.defaultRedisCacheConfiguration, this.redisOperations ); return redisCache; }修改CustomizedRedisCache核心方法就一个,getCache:当获取到缓存时,实时获取缓存的存活时间,若是存活时间进入缓存刷新时间范围即调起异步任务完成缓存动态加载。ThreadTaskHelper是一个异常任务提交的工具类。下面方法中的参数key,并非最终存入redis的key,是@Cacheable注解中的key,要想获取缓存的存活时间就须要找到真正的key,而后让redisOptions去调用ttl命令。在springboot 1.5下面好像有个RedisCacheKey的对象,但在springboot2.0中并未发现,取而代之获取真正key是经过函数this.createCacheKey来完成。public ValueWrapper get(final Object key) { ValueWrapper valueWrapper= super.get(key); if(null!=valueWrapper){ CacheItemConfig cacheItemConfig=CacheContainer.getCacheItemConfigByCacheName(key.toString()); long preLoadTimeSecond=cacheItemConfig.getPreLoadTimeSecond(); String cacheKey=this.createCacheKey(key); Long ttl= this.redisOperations.getExpire(cacheKey); if(null!=ttl&& ttl<=preLoadTimeSecond){ logger.info("key:{} ttl:{} preloadSecondTime:{}",cacheKey,ttl,preLoadTimeSecond); ThreadTaskHelper.run(new Runnable() { @Override public void run() { logger.info("refresh key:{}", cacheKey); CustomizedRedisCache.this.getCacheSupport() .refreshCacheByKey(CustomizedRedisCache.super.getName(), key.toString()); } }); } } return valueWrapper; }CacheContainer,这是一个辅助数据存储,将前面设置的缓存配置放入容器以便后面的逻辑获取。其中包含一个默认的缓存配置,防止 在未设置的状况致使缓存获取异常。public class CacheContainer { private static final String DEFAULT_CACHE_NAME="default"; private static final Map<String,CacheItemConfig> CACHE_CONFIG_HOLDER=new ConcurrentHashMap(){ { put(DEFAULT_CACHE_NAME,new CacheItemConfig(){ @Override public String getName() { return DEFAULT_CACHE_NAME; } @Override public long getExpiryTimeSecond() { return 30; } @Override public long getPreLoadTimeSecond() { return 25; } }); } }; public static void init(List<CacheItemConfig> cacheItemConfigs){ if(CollectionUtils.isEmpty(cacheItemConfigs)){ return; } cacheItemConfigs.forEach(cacheItemConfig -> { CACHE_CONFIG_HOLDER.put(cacheItemConfig.getName(),cacheItemConfig); }); } public static CacheItemConfig getCacheItemConfigByCacheName(String cacheName){ if(CACHE_CONFIG_HOLDER.containsKey(cacheName)) { return CACHE_CONFIG_HOLDER.get(cacheName); } return CACHE_CONFIG_HOLDER.get(DEFAULT_CACHE_NAME); } public static List<CacheItemConfig> getCacheItemConfigs(){ return CACHE_CONFIG_HOLDER .values() .stream() .filter(new Predicate<CacheItemConfig>() { @Override public boolean test(CacheItemConfig cacheItemConfig) { return !cacheItemConfig.getName().equals(DEFAULT_CACHE_NAME); } }) .collect(Collectors.toList()); } }修改CacheManager加载方式因为主动刷新缓存时须要用缓存操做,这里须要加载RedisTemplate,其实就是后面的RedisOptions接口。序列化机制可心随意调整。@Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(mapper); template.setValueSerializer(serializer); template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); return template; }加载CacheManager,主要是配置缓存容器,其他的两个都是redis所须要的对象。@Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory,RedisTemplate<Object, Object> redisTemplate) { CacheItemConfig productCacheItemConfig=new CacheItemConfig(); productCacheItemConfig.setName("Product"); productCacheItemConfig.setExpiryTimeSecond(10); productCacheItemConfig.setPreLoadTimeSecond(5); List<CacheItemConfig> cacheItemConfigs= Lists.newArrayList(productCacheItemConfig); CustomizedRedisCacheManager cacheManager = new CustomizedRedisCacheManager(connectionFactory,redisTemplate,cacheItemConfigs); return cacheManager; }解决并发刷新缓存的问题CustomizedRedisCache的get方法,当判断须要刷新缓存时,后台起了一个异步任务去更新缓存,此时若是有N个请求同时访问同一个缓存,就是发生相似缓存击穿的状况。为了不这种状况的发生最好的方法就是加锁,让其只有一个任务去作更新的事情。Spring Cache提供了一个同步的参数来支持并发更新控制,这里咱们能够模仿这个思路来处理。将正在进行缓存刷新的KEY放入一个容器,其它线程访问时若是发现KEY已经存在就直接跳过;缓存刷新完成后从容器中删除对应的KEY在容器中未发现正在进行缓存刷新的KEY时,利用锁机制确保只有一个任务执行刷新,相似双重检查public ValueWrapper get(final Object key) { ValueWrapper valueWrapper= super.get(key); if(null!=valueWrapper){ CacheItemConfig cacheItemConfig=CacheContainer.getCacheItemConfigByCacheName(key.toString()); long preLoadTimeSecond=cacheItemConfig.getPreLoadTimeSecond(); ; String cacheKey=this.createCacheKey(key); Long ttl= this.redisOperations.getExpire(cacheKey); if(null!=ttl&& ttl<=preLoadTimeSecond){ logger.info("key:{} ttl:{} preloadSecondTime:{}",cacheKey,ttl,preLoadTimeSecond); if(ThreadTaskHelper.hasRunningRefreshCacheTask(cacheKey)){ logger.info("do not need to refresh"); } else { ThreadTaskHelper.run(new Runnable() { @Override public void run() { try { REFRESH_CACKE_LOCK.lock(); if(ThreadTaskHelper.hasRunningRefreshCacheTask(cacheKey)){ logger.info("do not need to refresh"); } else { logger.info("refresh key:{}", cacheKey); CustomizedRedisCache.this.getCacheSupport().refreshCacheByKey(CustomizedRedisCache.super.getName(), key.toString()); ThreadTaskHelper.removeRefreshCacheTask(cacheKey); } } finally { REFRESH_CACKE_LOCK.unlock(); } } }); } } } return valueWrapper; }以上方案是在单机状况下,若是是多机也会出现执行屡次刷新,但这种代码是可接受的,若是作到严格意义的一次刷新就须要引入分布式锁,但同时会带来系统复杂度以及性能消耗,有点得不尝失的感受,因此建议单机方式便可。客户端配置这里不须要在缓存容器名称上动刀子了,像正规使用Cacheable注解便可。@Cacheable(value = "Product",key ="#id") @Override public Product getById(Long id) { this.logger.info("get product from db,id:{}",id); Product product=new Product(); product.setId(id); return product; }原文链接:http://www.javashuo.com/article/p-uuwnpche-ma.html
上滑加载中
推荐直播
-
华为云码道Agent集成与鸿蒙实战2026/08/11 周二 19:00-21:00
王一男-华为云码道产品规划专家;李炎-华为云码道产品专家;彭江敏-华为云鸿蒙端云一体化开发专家
本次直播带你解读华为云码道7月份产品新特性、新功能。更有专家演示码道Agent Space × 钉钉机器集成实战,从0到1打通消息通道;码道鸿蒙端云一体化实战,快速搭建员工签到系统。
回顾中 -
华为云开发者AI素养直播课·第五期2026/09/04 周五 16:00-18:00
林华鼎-华为云AI开发者运营负责人;蒋春阳-华为云AI开发者案例开发专家
本期直播内容: AI工具体验营 · 第5-8课连讲。Agent-Team 多智能体协作完成毕业设计实践
回顾中 -
华为云开发者AI素养ClassRoom·第六期2026/09/08 周二 19:00-20:00
樊渊-2026华为软件挑战赛冠军
高手来了:看软挑高手解析二维排样问题—从工业难题到算法突破
回顾中
热门标签