• [技术干货] SpringBoot static 静态方法获取 yml 配置文件
     在开发中有些业务代码中需要判断当前是测试环境还是正式环境来做出不同的逻辑处理。  今天说一下如何在 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 来获取当前的环境了。
  • springboot @Configuration 注解详解【转】
     @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 版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请保留以上作者信息和原文链接。 
  • iText 导出 pdf、word、图片【转】
     一、前言 在企业的信息系统中,报表处理一直占比较重要的作用,本文将介绍一种生成 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实现图片等比缩小放大转pdf
    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"); }}
  • [技术干货] WebSecurityConfigurerAdapter详解
     概述 今天我们要进一步的的学习如何自定义配置 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表单验证码
    表单验证码登录表单登录验证码验证,一般在用户名、密码提交登录前,添加过滤器,先验证验证码的有效性(开发中一般用的这种),然后再提交用户名、密码。文章下面还会使用另一种方法:验证码和用户名、密码一起同时提交登录。在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
  • [技术干货] Spring Security登录优雅集成图形验证码
    前言在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)输出到客户端。至此整个图形验证码认证的全流程已经结束。
  • [技术干货] spring Security4 和 oauth2整合 注解+xml混合使用
     验证码等额外数据获取逻辑 新增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 
  • [技术干货] 解决springboot下RedisTemplate清空/删除缓存
    删除代码: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
  • Spring Cache扩展:注解失效时间+主动刷新缓存
    目的以前在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
  • [技术干货] springboot redis cache 配置
    在原来的工程基础上加上redis缓存的配置。增加redis依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> 增加redis配置application.yml 上增加:spring: redis: host: localhost port: 6379 配置redis继承 CachingConfigurerSupport,增加 EnableCaching 注解,需要重写 keyGenerator 方法。@Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport在类里面配置 RestTemplate ,需要配置key和value的序列化类。key序列化使用StringRedisSerializer, 不配置的话key前面会出现乱码。value序列化使用 GenericJackson2JsonRedisSerializer ,保存为Json格式。该类目前反序列化还有一些bug,只能反序列化实现了Serialize的类。@Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) { RedisTemplate template = new RedisTemplate(); template.setConnectionFactory(factory); RedisSerializer keySerializer = new StringRedisSerializer(); // 设置key序列化类,否则key前面会多了一些乱码 template.setKeySerializer(keySerializer); template.setHashKeySerializer(keySerializer); // FIXME 有些版本有bug,没有序列化的只能序列化,但无法反序列化 GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(); // 设置内容序列化类 template.setValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; }配置 CacheManager,包括指定缓存和默认缓存的超时时间的配置。@Bean public CacheManager cacheManager(RedisTemplate redisTemplate) { RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate); Map<String, Long> expires = new HashMap<>(); expires.put(CacheNames.CONFIG, 60L); // 设置超时 cacheManager.setExpires(expires); // 没有设置的缓存默认过期时间 cacheManager.setDefaultExpiration(60 * 60); return cacheManager; }重写 keyGenerator,可以支持使用@Cacheable不指定Key。@Override public KeyGenerator keyGenerator() { return new KeyGenerator() { @Override public Object generate(Object target, Method method, Object... params) { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getSimpleName()); sb.append('.').append(method.getName()); // FIXME 参数太长的时候请指定key属性,否则key太长 for (Object obj : params) { if (obj != null) { sb.append(obj.toString()); } } return sb.toString(); } }; }使用缓存保存的时候使用 @Cacheable,清空使用 @CacheEvict ,更新的时候使用 @CachePut 。反序列化有bug,没有实现 Serializable 的只能序列化,无法反序列化。可能后续版本会解决该问题。所以把下面的查询代码修改一下,用 实现了 Serializable 的 ArrayList 包装返回。@Cacheable(CacheNames.CONFIG) @Override public Collection<Config> getAll() { System.out.println("\n----------GetAll----------\n"); return new ArrayList<>(configs.values()); } @CacheEvict(value = CacheNames.CONFIG, allEntries = true) @Override public long add(Config config) { } /** * 删除配置项 */ @CacheEvict(value = CacheNames.CONFIG, allEntries = true) @Override public boolean delete(long id) { }定时清空缓存也可以定时清空cache,使用 @EnableScheduling 和 @Scheduled 注解。@Component @EnableScheduling public class ClearCacheTask { /** * 定时清空缓存 */ @Scheduled(fixedRate = 60 * 1000L) @CacheEvict(value = { CacheNames.CONFIG }, allEntries = true) public void clearCaches() { System.out.println("\n------------ clear caches ------------\n"); } }redis 怎么样保存cache增加2条数据,一个是类型为 zset 的 缓存名~keys , 里面存放了该缓存所有的key, 一个是对应的key,值为序列化后的json。zset 是带权重的有序集合,可以使用 zrange config~keys -1 1 withscores 查看元素,新加入的都是 0.0 。使用 zcount config~keys -1 1 查看个数。可以使用 ttl 命令查看超时时间,单位为秒。安装redishttps://github.com/MicrosoftArchive/redis/releases 下载最新版本,3.2启动redis-server.exe redis.windows.conf也可以使用 Redis Client 查看。dis 比较重要命令keys * / keys cn* 查看数据type keyname 查看数据类型dbsize 查看记录数flushdb 删除【当前数据库】所有记录flushall 删除所有数据库里面的所有数据,注意不是当前数据库,而是所有数据库。expire,ttl 设置和查看超时时间select 选择0-15号数据库===========GITHUB地址===========xwjie/ElementVueSpringbootCodeTemplate
  • [技术干货] Spring Boot Redis- Cache 设置有效时间和自动刷新缓存,注解中设置自动刷新时间
    AsynchronousFunction.java 一个异步更新缓存的方法。  @Component public class AsychronousFunction{     private static final Logger logger = LoggerFactory.getLogger(AsychronousFunction.class);     @Autowired     private ApplicationContext applicationContext;     private final long EXPIRE_SECONDS = 5;     @Autowired     private DistributedLock distributeLock;     private String lockUuid = "kshdfsjdfhsfs43hhj34khj33jkj_";     /**      *刷新缓存      *@param cacheKey      *@Param methodInvoker      *return     **/     @Async     public void refreshCache(String cacheKey,MethodInvoker methodInvoker){         String methodInvokeKey = cacheKey;         if(methodInvoker != null){             Class<?> targetClass = methodInvoker.getTargetClass();             Object target = AopProxyUtils.getSingletonTarget(applicationContext.getBean(targetClass));             methodInvoker.setTargetObject(target);             //枷锁(分布式锁)             if(!distributedLock.getLock(DistributedLock.REDIS_LOCK_KEY+lockUuid,DistributedLock.REDIS_LOCK_KEY+lockUuid,DistributedLock.REDIS_LOCK_TIME) == false){                 try{                     methodInvoker.prepare();                     Object invoke = methodInvoker.invoke();                     //然后设置进缓存和重新设置过期时间                     redisTemplate.opsForValue().set(cacheKey,invoke);                     redisTemplate.expire(cacheKey,EXPIRE_SECONDS,TimeUnit.MINUTES);                 } catch(InvocationTargetException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException e){                     logger.error("刷新缓存失败:"+e.getMessage(),e);                 } finally {                     distributedLock.releaseLock(DistributedLock.REDIS_LOCK_KEY+lockUuid,DistributedLock.REDIS_LOCK_KEY+lockUuid);                 }             }         }     } }  结语 到这里就实现了对缓存@Cacheable注解的扩展,通过注解实现redis缓存异步刷新功能,下面是github项目源码。 ———————————————— 原文链接:https://blog.csdn.net/weixin_43526498/article/details/106621180 
  • [技术干货] RedisTemplate操作Redis
     一、SpringDataRedis简介 1、Redis redis是一款开源的Key-Value数据库,运行在内存中,由C语言编写。企业开发通常采用Redis来实现缓存。同类的产品还有memcache 、memcached 等。  2、Jedis Jedis是Redis官方推出的一款面向Java的客户端,提供了很多接口供Java语言调用。可以在Redis官网下载,当然还有一些开源爱好者提供的客户端,如Jredis、SRP等等,推荐使用Jedis。  3、Spring Data Redis Spring-data-redis是spring大家族的一部分,提供了在srping应用中通过简单的配置访问redis服务,对reids底层开发包(Jedis, JRedis, and RJC)进行了高度封装,RedisTemplate提供了redis各种操作、异常处理及序列化,支持发布订阅,并对spring 3.1 cache进行了实现。 spring-data-redis针对jedis提供了如下功能:  连接池自动管理,提供了一个高度封装的“RedisTemplate”类 针对jedis客户端中大量api进行了归类封装,将同一类型操作封装为operation接口 ValueOperations:简单K-V操作 SetOperations:set类型数据操作 ZSetOperations:zset类型数据操作 HashOperations:针对map类型的数据操作 ListOperations:针对list类型的数据操作 提供了对key的“bound”(绑定)便捷化操作API,可以通过bound封装指定的key,然后进行一系列的操作而无须“显式”的再次指定Key,即BoundKeyOperations: BoundValueOperations BoundSetOperations BoundListOperations BoundSetOperations BoundHashOperations 将事务操作封装,有容器控制。 针对数据的“序列化/反序列化”,提供了多种可选择策略(RedisSerializer) JdkSerializationRedisSerializer:POJO对象的存取场景,使用JDK本身序列化机制,将pojo类通过ObjectInputStream/ObjectOutputStream进行序列化操作,最终redis-server中将存储字节序列。是目前最常用的序列化策略。  StringRedisSerializer:Key或者value为字符串的场景,根据指定的charset对数据的字节序列编码成string,是“new String(bytes, charset)”和“string.getBytes(charset)”的直接封装。是最轻量级和高效的策略。  JacksonJsonRedisSerializer:jackson-json工具提供了javabean与json之间的转换能力,可以将pojo实例序列化成json格式存储在redis中,也可以将json格式的数据转换成pojo实例。因为jackson工具在序列化和反序列化时,需要明确指定Class类型,因此此策略封装起来稍微复杂。【需要jackson-mapper-asl工具支持】  二、RedisTemplate中API使用 1、pom.xml依赖 <!--Redis--> <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> 2、配置文件 # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器地址 spring.redis.host=127.0.0.1 # Redis数据库索引(默认为0) spring.redis.database=0 # Redis服务器连接密码(默认为空) spring.redis.password= # 连接池最大连接数(使用负值表示没有限制) spring.redis.jedis.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.jedis.pool.max-wait=-1ms # 连接池中的最大空闲连接 spring.redis.jedis.pool.max-idle=8 # 连接池中的最小空闲连接 spring.redis.jedis.pool.min-idle=0 # 连接超时时间(毫秒) spring.redis.timeout=5000ms 3、RedisTemplate的直接方法 首先使用@Autowired注入RedisTemplate(后面直接使用,就不特殊说明)  @Autowired private RedisTemplate redisTemplate; 1、删除单个key  //    删除key public void delete(String key){     redisTemplate.delete(key); } 2、删除多个key  //    删除多个key public void deleteKey (String ...keys){     redisTemplate.delete(keys); } 3、指定key的失效时间  //    指定key的失效时间 public void expire(String key,long time){     redisTemplate.expire(key,time,TimeUnit.MINUTES); } 4、根据key获取过期时间  //    根据key获取过期时间 public long getExpire(String key){     Long expire = redisTemplate.getExpire(key);     return expire; } 5、判断key是否存在  //    判断key是否存在 public boolean hasKey(String key){     return redisTemplate.hasKey(key); } 4、String类型相关操作 1)、添加缓存(2/3是1的递进值) //1、通过redisTemplate设置值 redisTemplate.boundValueOps("StringKey").set("StringValue"); redisTemplate.boundValueOps("StringKey").set("StringValue",1, TimeUnit.MINUTES);  //2、通过BoundValueOperations设置值 BoundValueOperations stringKey = redisTemplate.boundValueOps("StringKey"); stringKey.set("StringVaule"); stringKey.set("StringValue",1, TimeUnit.MINUTES);  //3、通过ValueOperations设置值 ValueOperations ops = redisTemplate.opsForValue(); ops.set("StringKey", "StringVaule"); ops.set("StringValue","StringVaule",1, TimeUnit.MINUTES); 2)、设置过期时间(单独设置) redisTemplate.boundValueOps("StringKey").expire(1,TimeUnit.MINUTES); redisTemplate.expire("StringKey",1,TimeUnit.MINUTES); 3)、获取缓存值(2/3是1的递进值) //1、通过redisTemplate设置值 String str1 = (String) redisTemplate.boundValueOps("StringKey").get();  //2、通过BoundValueOperations获取值 BoundValueOperations stringKey = redisTemplate.boundValueOps("StringKey"); String str2 = (String) stringKey.get();  //3、通过ValueOperations获取值 ValueOperations ops = redisTemplate.opsForValue(); String str3 = (String) ops.get("StringKey"); 4)、删除key Boolean result = redisTemplate.delete("StringKey"); 5)、顺序递增 redisTemplate.boundValueOps("StringKey").increment(3L); 6)、顺序递减 redisTemplate.boundValueOps("StringKey").increment(-3L); 5、Hash类型相关操作 1)、添加缓存(2/3是1的递进值) //1、通过redisTemplate设置值 redisTemplate.boundHashOps("HashKey").put("SmallKey", "HashVaue");  //2、通过BoundValueOperations设置值 BoundHashOperations hashKey = redisTemplate.boundHashOps("HashKey"); hashKey.put("SmallKey", "HashVaue");  //3、通过ValueOperations设置值 HashOperations hashOps = redisTemplate.opsForHash(); hashOps.put("HashKey", "SmallKey", "HashVaue"); 2)、设置过期时间(单独设置) redisTemplate.boundValueOps("HashKey").expire(1,TimeUnit.MINUTES); redisTemplate.expire("HashKey",1,TimeUnit.MINUTES); 3)、添加一个Map集合 HashMap<String, String> hashMap = new HashMap<>(); redisTemplate.boundHashOps("HashKey").putAll(hashMap ); 4)、设置过期时间(单独设置) redisTemplate.boundValueOps("HashKey").expire(1,TimeUnit.MINUTES); redisTemplate.expire("HashKey",1,TimeUnit.MINUTES); 5)、提取所有的小key //1、通过redisTemplate获取值 Set keys1 = redisTemplate.boundHashOps("HashKey").keys();  //2、通过BoundValueOperations获取值 BoundHashOperations hashKey = redisTemplate.boundHashOps("HashKey"); Set keys2 = hashKey.keys();  //3、通过ValueOperations获取值 HashOperations hashOps = redisTemplate.opsForHash(); Set keys3 = hashOps.keys("HashKey"); 6)、提取所有的value值 //1、通过redisTemplate获取值 List values1 = redisTemplate.boundHashOps("HashKey").values();  //2、通过BoundValueOperations获取值 BoundHashOperations hashKey = redisTemplate.boundHashOps("HashKey"); List values2 = hashKey.values();  //3、通过ValueOperations获取值 HashOperations hashOps = redisTemplate.opsForHash(); List values3 = hashOps.values("HashKey"); 7)、根据key提取value值 //1、通过redisTemplate获取 String value1 = (String) redisTemplate.boundHashOps("HashKey").get("SmallKey");  //2、通过BoundValueOperations获取值 BoundHashOperations hashKey = redisTemplate.boundHashOps("HashKey"); String value2 = (String) hashKey.get("SmallKey");  //3、通过ValueOperations获取值 HashOperations hashOps = redisTemplate.opsForHash(); String value3 = (String) hashOps.get("HashKey", "SmallKey"); 8)、获取所有的键值对集合 //1、通过redisTemplate获取 Map entries = redisTemplate.boundHashOps("HashKey").entries();  //2、通过BoundValueOperations获取值 BoundHashOperations hashKey = redisTemplate.boundHashOps("HashKey"); Map entries1 = hashKey.entries();  //3、通过ValueOperations获取值 HashOperations hashOps = redisTemplate.opsForHash(); Map entries2 = hashOps.entries("HashKey");  9)、删除 //删除小key redisTemplate.boundHashOps("HashKey").delete("SmallKey"); //删除大key redisTemplate.delete("HashKey");  10)、判断Hash中是否含有该值 Boolean isEmpty = redisTemplate.boundHashOps("HashKey").hasKey("SmallKey"); 6、Set类型相关操作 1)、添加Set缓存(值可以是一个,也可是多个)(2/3是1的递进值) //1、通过redisTemplate设置值 redisTemplate.boundSetOps("setKey").add("setValue1", "setValue2", "setValue3");  //2、通过BoundValueOperations设置值 BoundSetOperations setKey = redisTemplate.boundSetOps("setKey"); setKey.add("setValue1", "setValue2", "setValue3");  //3、通过ValueOperations设置值 SetOperations setOps = redisTemplate.opsForSet(); setOps.add("setKey", "SetValue1", "setValue2", "setValue3"); 2)、设置过期时间(单独设置) redisTemplate.boundValueOps("setKey").expire(1,TimeUnit.MINUTES); redisTemplate.expire("setKey",1,TimeUnit.MINUTES); 3)、根据key获取Set中的所有值 //1、通过redisTemplate获取值 Set set1 = redisTemplate.boundSetOps("setKey").members();  //2、通过BoundValueOperations获取值 BoundSetOperations setKey = redisTemplate.boundSetOps("setKey"); Set set2 = setKey.members();  //3、通过ValueOperations获取值 SetOperations setOps = redisTemplate.opsForSet(); Set set3 = setOps.members("setKey"); 4)、根据value从一个set中查询,是否存在 Boolean isEmpty = redisTemplate.boundSetOps("setKey").isMember("setValue2"); 5)、获取Set缓存的长度 Long size = redisTemplate.boundSetOps("setKey").size(); 6)、移除指定的元素 Long result1 = redisTemplate.boundSetOps("setKey").remove("setValue1"); 7)、移除指定的key Boolean result2 = redisTemplate.delete("setKey"); 7、 LIST类型相关操作 1)、添加缓存(2/3是1的递进值) //1、通过redisTemplate设置值 redisTemplate.boundListOps("listKey").leftPush("listLeftValue1"); redisTemplate.boundListOps("listKey").rightPush("listRightValue2");  //2、通过BoundValueOperations设置值 BoundListOperations listKey = redisTemplate.boundListOps("listKey"); listKey.leftPush("listLeftValue3"); listKey.rightPush("listRightValue4");  //3、通过ValueOperations设置值 ListOperations opsList = redisTemplate.opsForList(); opsList.leftPush("listKey", "listLeftValue5"); opsList.rightPush("listKey", "listRightValue6"); 2)、将List放入缓存 ArrayList<String> list = new ArrayList<>(); redisTemplate.boundListOps("listKey").rightPushAll(list); redisTemplate.boundListOps("listKey").leftPushAll(list); 3)、设置过期时间(单独设置) redisTemplate.boundValueOps("listKey").expire(1,TimeUnit.MINUTES); redisTemplate.expire("listKey",1,TimeUnit.MINUTES); 4)、获取List缓存全部内容(起始索引,结束索引) List listKey1 = redisTemplate.boundListOps("listKey").range(0, 10);  5)、从左或从右弹出一个元素 String listKey2 = (String) redisTemplate.boundListOps("listKey").leftPop();  //从左侧弹出一个元素 String listKey3 = (String) redisTemplate.boundListOps("listKey").rightPop(); //从右侧弹出一个元素 6)、根据索引查询元素 String listKey4 = (String) redisTemplate.boundListOps("listKey").index(1); 7)、获取List缓存的长度 Long size = redisTemplate.boundListOps("listKey").size(); 8)、根据索引修改List中的某条数据(key,索引,值) redisTemplate.boundListOps("listKey").set(3L,"listLeftValue3"); 9)、移除N个值为value(key,移除个数,值) redisTemplate.boundListOps("listKey").remove(3L,"value"); 8、Zset类型的相关操作 1)、向集合中插入元素,并设置分数 //1、通过redisTemplate设置值 redisTemplate.boundZSetOps("zSetKey").add("zSetVaule", 100D); //2、通过BoundValueOperations设置值 BoundZSetOperations zSetKey = redisTemplate.boundZSetOps("zSetKey"); zSetKey.add("zSetVaule", 100D); //3、通过ValueOperations设置值 ZSetOperations zSetOps = redisTemplate.opsForZSet(); zSetOps.add("zSetKey", "zSetVaule", 100D); 2)、向集合中插入多个元素,并设置分数 DefaultTypedTuple<String> p1 = new DefaultTypedTuple<>("zSetVaule1", 2.1D); DefaultTypedTuple<String> p2 = new DefaultTypedTuple<>("zSetVaule2", 3.3D); redisTemplate.boundZSetOps("zSetKey").add(new HashSet<>(Arrays.asList(p1,p2))); 3)、按照排名先后(从小到大)打印指定区间内的元素, -1为打印全部 Set<String> range = redisTemplate.boundZSetOps("zSetKey").range(0, -1); 4)、获得指定元素的分数 Double score = redisTemplate.boundZSetOps("zSetKey").score("zSetVaule"); 5)、返回集合内的成员个数 Long size = redisTemplate.boundZSetOps("zSetKey").size(); 6)、返回集合内指定分数范围的成员个数(Double类型) Long COUNT = redisTemplate.boundZSetOps("zSetKey").count(0D, 2.2D); 7)、返回集合内元素在指定分数范围内的排名(从小到大) Set byScore = redisTemplate.boundZSetOps("zSetKey").rangeByScore(0D, 2.2D); 8)、带偏移量和个数,(key,起始分数,最大分数,偏移量,个数) Set<String> ranking2 = redisTemplate.opsForZSet().rangeByScore("zSetKey", 0D, 2.2D 1, 3); 9)、返回集合内元素的排名,以及分数(从小到大) Set<TypedTuple<String>> tuples = redisTemplate.boundZSetOps("zSetKey").rangeWithScores(0L, 3L);   for (TypedTuple<String> tuple : tuples) {       System.out.println(tuple.getValue() + " : " + tuple.getScore());   }ss 10)、返回指定成员的排名 //从小到大 Long startRank = redisTemplate.boundZSetOps("zSetKey").rank("zSetVaule"); //从大到小 Long endRank = redisTemplate.boundZSetOps("zSetKey").reverseRank("zSetVaule"); 11)、从集合中删除指定元素 redisTemplate.boundZSetOps("zSetKey").remove("zSetVaule"); 12)、删除指定索引范围的元素(Long类型) redisTemplate.boundZSetOps("zSetKey").removeRange(0L,3L); 13)、删除指定分数范围内的元素(Double类型) redisTemplate.boundZSetOps("zSetKey").removeRangeByScorssse(0D,2.2D); 14)、为指定元素加分(Double类型) Double score = redisTemplate.boundZSetOps("zSetKey").incrementScore("zSetVaule",1.1D); ———————————————— 原文链接:https://blog.csdn.net/lydms/article/details/105224210 
  • [技术干货] Spring Boot中使用缓存
    随着时间的积累,应用的使用用户不断增加,数据规模也越来越大,往往数据库查询操作会成为影响用户使用体验的瓶颈,此时使用缓存往往是解决这一问题非常好的手段之一。原始的使用缓存的方式如下:这样的缓存使用方式将数据读取后,主动对缓存进行更新操作,这样的方式使用方便,但是代码的耦合性高,代码侵入性强。 1 /** 2 * 使用缓存以id为字样,如果id所对应的缓存信息已经存在,则不会再读db 3 * @param id 4 * @return 5 */ 6 public UserInfo getUserInfoById(int id){ 7 UserInfo userInfo = (UserInfo) redisTemplate.opsForValue().get(id+""); 8 if(userInfo != null){ 9 return userInfo; 10 } 11 System.out.println("若下面没出现“无缓存的时候调用”字样且能打印出数据表示测试成功"); 12 UserInfo userInfo1 = userInfoDao.findById(id); 13 ValueOperations<String, UserInfo> valueOperations = redisTemplate.opsForValue(); 14 valueOperations.set(id+"", userInfo1); 15 return userInfo1; 16 } 17 18 @Transactional 19 public int saveUserInfo(UserInfo userInfo){ 20 //更新缓存 21 userInfoDao.saveUserInfo(userInfo); 22 int id = userInfo.getId(); 23 //userInfo 里面的id值已经发生了变化 24 System.out.println(userInfo.getId()); 25 ValueOperations<String, UserInfo> valueOperations = redisTemplate.opsForValue(); 26 valueOperations.set(id+"", userInfo); 27 redisTemplate.opsForValue().set(id+"", userInfo); 28 return id; 29 }Spring 3开始提供了强大的基于注解的缓存支持,可以通过注解配置方式低侵入的给原有Spring应用增加缓存功能,提高数据访问性能。在Spring Boot中对于缓存的支持,提供了一系列的自动化配置,使我们可以非常方便的使用缓存。下面我们通过一个简单的例子来展示,我们是如何给一个既有应用增加缓存功能的。引入缓存<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>在application.preoperties中定义redis的配置# REDIS (RedisProperties) # Redis数据库索引(默认为0) spring.redis.database=0 # Redis服务器地址 spring.redis.host=127.0.0.1 # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password= # 连接池最大连接数(使用负值表示没有限制) spring.redis.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.pool.max-wait=-1 # 连接池中的最大空闲连接 spring.redis.pool.max-idle=8 # 连接池中的最小空闲连接 spring.redis.pool.min-idle=0 # 连接超时时间(毫秒) spring.redis.timeout=0自定义缓存,本文使用redis作为缓存,自定义缓存配置,继承CachingConfigurerSupport类 1 /** 2 * 自定义缓存配置文件,继承 CachingConfigurerSupport 3 * Created by huanl on 2017/8/22. 4 */ 5 @Configuration 6 @EnableCaching 7 public class RedisConfig extends CachingConfigurerSupport{ 8 public RedisConfig() { 9 super(); 10 } 11 12 /** 13 * 指定使用哪一种缓存 14 * @param redisTemplate 15 * @return 16 */ 17 @Bean 18 public CacheManager cacheManager(RedisTemplate<?,?> redisTemplate) { 19 RedisCacheManager rcm = new RedisCacheManager(redisTemplate); 20 return rcm; 21 } 22 23 /** 24 * 指定默认的key生成方式 25 * @return 26 */ 27 @Override 28 public KeyGenerator keyGenerator() { 29 KeyGenerator keyGenerator = new KeyGenerator() { 30 @Override 31 public Object generate(Object o, Method method, Object... objects) { 32 StringBuilder sb = new StringBuilder(); 33 sb.append(o.getClass().getName()); 34 sb.append(method.getName()); 35 for (Object obj : objects) { 36 sb.append(obj.toString()); 37 } 38 return sb.toString(); 39 } 40 }; 41 return keyGenerator; 42 } 43 44 @Override 45 public CacheResolver cacheResolver() { 46 return super.cacheResolver(); 47 } 48 49 @Override 50 public CacheErrorHandler errorHandler() { 51 return super.errorHandler(); 52 } 53 54 /** 55 * redis 序列化策略 ,通常情况下key值采用String序列化策略 56 * StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。StringRedisSerializer 57 * RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。JdkSerializationRedisSerializer 58 * @param factory 59 * @return 60 */ 61 @Bean 62 public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory){ 63 RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); 64 redisTemplate.setConnectionFactory(factory); 65 66 // // 使用Jackson2JsonRedisSerialize 替换默认序列化 67 // Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); 68 // ObjectMapper om = new ObjectMapper(); 69 // om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); 70 // om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); 71 // jackson2JsonRedisSerializer.setObjectMapper(om); 72 // 73 // 74 // //设置value的序列化方式 75 // redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); 76 // //设置key的序列化方式 77 // redisTemplate.setKeySerializer(new StringRedisSerializer()); 78 // redisTemplate.setHashKeySerializer(new StringRedisSerializer()); 79 // redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); 80 81 //使用fastJson作为默认的序列化方式 82 GenericFastJsonRedisSerializer genericFastJsonRedisSerializer = new GenericFastJsonRedisSerializer(); 83 redisTemplate.setDefaultSerializer(genericFastJsonRedisSerializer); 84 redisTemplate.setValueSerializer(genericFastJsonRedisSerializer); 85 redisTemplate.setKeySerializer(new StringRedisSerializer()); 86 redisTemplate.setHashValueSerializer(genericFastJsonRedisSerializer); 87 redisTemplate.setHashKeySerializer(new StringRedisSerializer()); 88 redisTemplate.afterPropertiesSet(); 89 90 return redisTemplate; 91 92 } 93 94 /** 95 * 转换返回的object为json 96 * @return 97 */ 98 @Bean 99 public HttpMessageConverters fastJsonHttpMessageConverters(){ 100 // 1、需要先定义一个converter 转换器 101 FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); 102 // 2、添加fastJson 的配置信息,比如:是否要格式化返回的json数据 103 FastJsonConfig fastJsonConfig = new FastJsonConfig(); 104 fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat); 105 // 3、在convert 中添加配置信息 106 fastConverter.setFastJsonConfig(fastJsonConfig); 107 // 4、将convert 添加到converters当中 108 HttpMessageConverter<?> converter = fastConverter; 109 return new HttpMessageConverters(converter); 110 } 111 112 113 }在Spring Boot主类中增加@EnableCaching注解开启缓存功能 1 @SpringBootApplication 2 @Import(RedisConfig.class) 3 @MapperScan("com.redistest.dao") 4 @EnableCaching 5 public class RedisApplication { 6 7 public static void main(String[] args) { 8 SpringApplication.run(RedisApplication.class, args); 9 } 10 }在Service类中使用缓存@Service public class UserInfoService { @Autowired private UserInfoDao userInfoDao; @Autowired private RedisTemplate redisTemplate; /** * 优先从缓存中获取数据,如果缓存中不存在,则从db中读取,读取后将结果按照key存入缓存 * @param id * @return */ @Cacheable(value = "user", key="#id + 'findById'") public UserInfo getUserInfoByIDNew(int id){ return userInfoDao.findById(id); } /** * 使用缓存以id为字样,如果id所对应的缓存信息已经存在,则不会再读db * @param id * @return */ public UserInfo getUserInfoById(int id){ UserInfo userInfo = (UserInfo) redisTemplate.opsForValue().get(id+""); if(userInfo != null){ return userInfo; } System.out.println("若下面没出现“无缓存的时候调用”字样且能打印出数据表示测试成功"); UserInfo userInfo1 = userInfoDao.findById(id); ValueOperations<String, UserInfo> valueOperations = redisTemplate.opsForValue(); valueOperations.set(id+"", userInfo1); return userInfo1; } @Transactional public int saveUserInfo(UserInfo userInfo){ //更新缓存 userInfoDao.saveUserInfo(userInfo); int id = userInfo.getId(); //userInfo 里面的id值已经发生了变化 System.out.println(userInfo.getId()); ValueOperations<String, UserInfo> valueOperations = redisTemplate.opsForValue(); valueOperations.set(id+"", userInfo); redisTemplate.opsForValue().set(id+"", userInfo); return id; } /** * * @param userInfo * @return */ @Transactional //更新后删除指定值的缓存,获取值得时候默认从db中获取 //@CacheEvict(value = "user", key="#userInfo.id+'findById'") //配置于函数上,能够根据参数定义条件来进行缓存,它与@Cacheable不同的是,它每次都会真是调用函数,所以主要用于数据新增和修改操作上。它的参数与@Cacheable类似,具体功能可参考上面对@Cacheable参数的解析 @CachePut(value = "user", key = "#userInfo.id+'findById'") public UserInfo updateUserInfo(UserInfo userInfo){ userInfoDao.updateUserInfo(userInfo); int id = userInfo.getId(); redisTemplate.opsForValue().set(id+"", userInfo); return userInfo; } }使用的MybatisDao@Mapper @Component public interface UserInfoDao { @Select(value = "select * from t_t_user where id=#{id}") public UserInfo findById(int id); public int saveUserInfo(UserInfo userInfo); public int updateUserInfo(UserInfo userInfo); }mapper文件,在application.properties文件中定义mapper文件的位置mybatis.mapper-locations=mapper.xml<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!--命名空间:分类管理sql隔离,方便管理--> <mapper namespace="com.redistest.dao.UserInfoDao"> <!--插入--> <insert id="saveUserInfo" useGeneratedKeys="true" keyProperty="id" keyColumn="id" parameterType="com.redistest.domain.UserInfo"> INSERT INTO t_t_user (name, password, salt, role) VALUES (#{name}, #{password}, #{salt}, #{role}) </insert> <!--更新--> <update id="updateUserInfo" parameterType="com.redistest.domain.UserInfo"> UPDATE t_t_user set name=#{name}, password=#{password}, salt=#{salt}, role=#{role} where id=#{id} </update> </mapper>Cache注解详解回过头来我们再来看,这里使用到的两个注解分别作了什么事情。@CacheConfig:主要用于配置该类中会用到的一些共用的缓存配置。在这里@CacheConfig(cacheNames = "users"):配置了该数据访问对象中返回的内容将存储于名为users的缓存对象中,我们也可以不使用该注解,直接通过@Cacheable自己配置缓存集的名字来定义。@Cacheable:配置了findByName函数的返回值将被加入缓存。同时在查询时,会先从缓存中获取,若不存在才再发起对数据库的访问。该注解主要有下面几个参数:value、cacheNames:两个等同的参数(cacheNames为Spring 4新增,作为value的别名),用于指定缓存存储的集合名。由于Spring 4中新增了@CacheConfig,因此在Spring 3中原本必须有的value属性,也成为非必需项了key:缓存对象存储在Map集合中的key值,非必需,缺省按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式,比如:@Cacheable(key = "#p0"):使用函数第一个参数作为缓存的key值,更多关于SpEL表达式的详细内容可参考官方文档condition:缓存对象的条件,非必需,也需使用SpEL表达式,只有满足表达式条件的内容才会被缓存,比如:@Cacheable(key = "#p0", condition = "#p0.length() < 3"),表示只有当第一个参数的长度小于3的时候才会被缓存,若做此配置上面的AAA用户就不会被缓存,读者可自行实验尝试。unless:另外一个缓存条件参数,非必需,需使用SpEL表达式。它不同于condition参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断。keyGenerator:用于指定key生成器,非必需。若需要指定一个自定义的key生成器,我们需要去实现org.springframework.cache.interceptor.KeyGenerator接口,并使用该参数来指定。需要注意的是:该参数与key是互斥的cacheManager:用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用cacheResolver:用于指定使用那个缓存解析器,非必需。需通过org.springframework.cache.interceptor.CacheResolver接口来实现自己的缓存解析器,并用该参数指定。除了这里用到的两个注解之外,还有下面几个核心注解:@CachePut:配置于函数上,能够根据参数定义条件来进行缓存,它与@Cacheable不同的是,它每次都会真是调用函数,所以主要用于数据新增和修改操作上。它的参数与@Cacheable类似,具体功能可参考上面对@Cacheable参数的解析@CacheEvict:配置于函数上,通常用在删除方法上,用来从缓存中移除相应数据。除了同@Cacheable一样的参数之外,它还有下面两个参数:allEntries:非必需,默认为false。当为true时,会移除所有数据beforeInvocation:非必需,默认为false,会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。缓存配置完成了上面的缓存实验之后,可能大家会问,那我们在Spring Boot中到底使用了什么缓存呢?在Spring Boot中通过@EnableCaching注解自动化配置合适的缓存管理器(CacheManager),Spring Boot根据下面的顺序去侦测缓存提供者:GenericJCache (JSR-107)EhCache 2.xHazelcastInfinispanRedisGuavaSimple除了按顺序侦测外,我们也可以通过配置属性spring.cache.type来强制指定。我们可以通过debug调试查看cacheManager对象的实例来判断当前使用了什么缓存。本文中不对所有的缓存做详细介绍,下面以常用的EhCache为例,看看如何配置来使用EhCache进行缓存管理。在Spring Boot中开启EhCache非常简单,只需要在工程中加入ehcache.xml配置文件并在pom.xml中增加ehcache依赖,框架只要发现该文件,就会创建EhCache的缓存管理器。在src/main/resources目录下创建:ehcache.xml123456789<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:noNamespaceSchemaLocation="ehcache.xsd"><cache name="users"maxEntriesLocalHeap="200"timeToLiveSeconds="600"></cache></ehcache>在pom.xml中加入1234<dependency><groupId>net.sf.ehcache</groupId><artifactId>ehcache</artifactId></dependency>完成上面的配置之后,再通过debug模式运行单元测试,观察此时CacheManager已经是EhCacheManager实例,说明EhCache开启成功了。对于EhCache的配置文件也可以通过application.properties文件中使用spring.cache.ehcache.config属性来指定,比如:1spring.cache.ehcache.config=classpath:config/another-config.xml
  • [技术干货] springboot缓存@Cacheable实现redis缓存机制
     添加依赖:  <!-- redis --> <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- spring2.X集成redis所需common-pool2--> <dependency>     <groupId>org.apache.commons</groupId>     <artifactId>commons-pool2</artifactId>     <version>2.6.0</version> </dependency> 创建RedisConfig @EnableCaching @Configuration public class RedisConfig extends CachingConfigurerSupport {     @Bean     public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {         RedisTemplate<String, Object> template = new RedisTemplate<>();         RedisSerializer<String> redisSerializer = new StringRedisSerializer();         Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);         ObjectMapper om = new ObjectMapper();         om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);         om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);         jackson2JsonRedisSerializer.setObjectMapper(om);         template.setConnectionFactory(factory);         //key序列化方式         template.setKeySerializer(redisSerializer);         //value序列化         template.setValueSerializer(jackson2JsonRedisSerializer);         //value hashmap序列化         template.setHashValueSerializer(jackson2JsonRedisSerializer);         return template;     }     @Bean     public CacheManager cacheManager(RedisConnectionFactory factory) {         RedisSerializer<String> redisSerializer = new StringRedisSerializer();         Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);         //解决查询缓存转换异常的问题         ObjectMapper om = new ObjectMapper();         om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);         om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);         jackson2JsonRedisSerializer.setObjectMapper(om);         // 配置序列化(解决乱码的问题),过期时间600秒         RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()                 .entryTtl(Duration.ofSeconds(600))               .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))                 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))                 .disableCachingNullValues();          RedisCacheManager cacheManager = RedisCacheManager.builder(factory)                 .cacheDefaults(config)                 .build();         return cacheManager;     } } 3、在接口中添加redis缓存 由于首页数据变化不是很频繁,而且首页访问量相对较大,所以我们有必要把首页接口数据缓存到redis缓存中,减少数据库压力和提高访问速度。 改造service-cms模块首页banner接口,首页课程与讲师接口类似 3.1 Spring Boot缓存注解 (1)缓存@Cacheable 根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。 查看源码,属性值如下: 属性/方法名    解释 value    缓存名,必填,它指定了你的缓存存放在哪块命名空间 cacheNames    与 value 差不多,二选一即可 key    可选属性,可以使用 SpEL 标签自定义缓存的key (2)缓存@CachePut 使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。 查看源码,属性值如下: 属性/方法名    解释 value    缓存名,必填,它指定了你的缓存存放在哪块命名空间 cacheNames    与 value 差不多,二选一即可 key    可选属性,可以使用 SpEL 标签自定义缓存的key (3)缓存@CacheEvict 使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上 查看源码,属性值如下: 属性/方法名    解释 value    缓存名,必填,它指定了你的缓存存放在哪块命名空间 cacheNames    与 value 差不多,二选一即可 key    可选属性,可以使用 SpEL 标签自定义缓存的key allEntries    是否清空所有缓存,默认为 false。如果指定为 true,则方法调用后将立即清空所有的缓存 beforeInvocation    是否在方法执行前就清空,默认为 false。如果指定为 true,则在方法执行前就会清空缓存 3.2 启动redis服务 3.3连接redis服务可能遇到的问题 (1)关闭liunx防火墙 (2)找到redis配置文件, 注释一行配置 (3)如果出现下面错误提示 修改 protected-mode yes 改为 protected-mode no 在application.properties添加redis配置信息  spring.redis.host=81.68.112.20 spring.redis.port=6379 spring.redis.database= 0 spring.redis.timeout=1800000  spring.redis.lettuce.pool.max-active=20 spring.redis.lettuce.pool.max-wait=-1 #最大阻塞等待时间(负数表示没限制) spring.redis.lettuce.pool.max-idle=5 spring.redis.lettuce.pool.min-idle=0   下面是一个列子 (2)修改CrmBannerServiceImpl,添加redis缓存注解  @Service public class CrmBannerServiceImpl extends ServiceImpl<CrmBannerMapper, CrmBanner> implements CrmBannerService {      //查询所有banner     @Cacheable(value = "banner", key = "'selectIndexList'")     @Override     public List<CrmBanner> selectAllBanner() {         //根据id进行降序排列,显示排列之后前两条记录         QueryWrapper<CrmBanner> wrapper = new QueryWrapper<>();         wrapper.orderByDesc("id");         //last方法,拼接sql语句         wrapper.last("limit 2");         List<CrmBanner> list = baseMapper.selectList(null);         return list;     }      @Override     public void pageBanner(Page<CrmBanner> pageParam, Object o) {         baseMapper.selectPage(pageParam, null);     }      @Override     public CrmBanner getBannerById(String id) {         return baseMapper.selectById(id);     }      @CacheEvict(value = "banner", allEntries = true)     @Override     public void saveBanner(CrmBanner banner) {         baseMapper.insert(banner);     }      @CacheEvict(value = "banner", allEntries = true)     @Override     public void updateBannerById(CrmBanner banner) {         baseMapper.updateById(banner);     }      @CacheEvict(value = "banner", allEntries = true)     @Override     public void removeBannerById(String id) {         baseMapper.deleteById(id);     } } ———————————————— 原文链接:https://blog.csdn.net/u014496893/article/details/113139275 
总条数:685 到第
上滑加载中