
计划在 Team 的开源项目里加入 Redis 实现缓存处理,因为业务功能已经实现了一部分,通过写 Redis 工具类,然后引用,改动量较大,而且不可以实现解耦合,所以想到了 Spring 框架的 AOP(面向切面编程)。 开源项目: https://github.com/u014427391/jeeplatform 欢迎 star(收藏)
Spring 框架作为 JavaEE 框架领域的一款重要的开源框架,在企业应用开发中有着很重要的作用,同时 Spring 框架及其子框架很多,所以知识量很广。 SpringBoot:一款 Spring 框架的子框架,也可以叫微框架,是 2014 年推出的一款使 Spring 框架开发变得容易的框架。学过 Spring 框架的都知识,Spring 框架难以避免地需要配置不少 XMl,而使用 SpringBoot 框架的话,就可以使用注解开发,极大地简化基于 Spring 框架的开发。SpringBoot 充分利用了 JavaConfig 的配置模式以及“约定优于配置”的理念,能够极大的简化基于 SpringMVC 的 Web 应用和 REST 服务开发。
Redis 安装部署的可以参考我的博客(Redis 是基于 C 编写的,所以安装前先安装 gcc 编译器): http://blog.csdn.net/u014427391/article/details/71210989
Redis 如今已经成为 Web 开发社区最火热的内存数据库之一,随着 Web2.0 的快速发展,再加上半结构数据比重加大,网站对高效性能的需求也越来越多。 而且大型网站一般都有几百台或者更多 Redis 服务器。Redis 作为一款功能强大的系统,无论是存储、队列还是缓存系统,都有其用武之地。
SpringBoot 框架入门的可以参考我之前的博客: http://blog.csdn.net/u014427391/article/details/70655332
项目结构图:
添加 resource 下面的 application.yml 配置,这里主要配置 mysql,druid,redis
spring: datasource: # 主数据源 shop: url: jdbc:mysql://127.0.0.1:3306/jeeplatform?autoRecOnnect=true&useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&useSSL=false username: root password: root driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource # 连接池设置 druid: initial-size: 5 min-idle: 5 max-active: 20 # 配置获取连接等待超时的时间 max-wait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 time-between-eviction-runs-millis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 min-evictable-idle-time-millis: 300000 # Oracle 请使用 select 1 from dual validation-query: SELECT 'x' test-while-idle: true test-on-borrow: false test-on-return: false # 打开 PSCache,并且指定每个连接上 PSCache 的大小 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 # 配置监控统计拦截的 filters,去掉后监控界面 sql 无法统计,'wall'用于防火墙 filters: stat,wall,slf4j # 通过 connectProperties 属性来打开 mergeSql 功能;慢 SQL 记录 connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 合并多个 DruidDataSource 的监控数据 use-global-data-source-stat: true jpa: database: mysql hibernate: show_sql: true format_sql: true ddl-auto: none naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl mvc: view: prefix: /WEB-INF/jsp/ suffix: .jsp #Jedis 配置 jedis : pool : host : 127.0.0.1 port : 6379 password : password timeout : 0 config : maxTotal : 100 maxIdle : 10 maxWaitMillis : 100000 编写一个配置类启动配置 JedisConfig.java:
package org.muses.jeeplatform.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; @Configuration //@ConfigurationProperties(prefix = JedisConfig.JEDIS_PREFIX ) public class JedisConfig { //public static final String JEDIS_PREFIX = "jedis"; @Bean(name= "jedisPool") @Autowired public JedisPool jedisPool(@Qualifier("jedisPoolConfig") JedisPoolConfig config, @Value("${spring.jedis.pool.host}")String host, @Value("${spring.jedis.pool.port}")int port, @Value("${spring.jedis.pool.timeout}")int timeout, @Value("${spring.jedis.pool.password}")String password) { return new JedisPool(config, host, port,timeout,password); } @Bean(name= "jedisPoolConfig") public JedisPoolConfig jedisPoolConfig (@Value("${spring.jedis.pool.config.maxTotal}")int maxTotal, @Value("${spring.jedis.pool.config.maxIdle}")int maxIdle, @Value("${spring.jedis.pool.config.maxWaitMillis}")int maxWaitMillis) { JedisPoolConfig cOnfig= new JedisPoolConfig(); config.setMaxTotal(maxTotal); config.setMaxIdle(maxIdle); config.setMaxWaitMillis(maxWaitMillis); return config; } } 编写一个元注解类 RedisCache.java,被改注解定义的类都自动实现 AOP 缓存处理
package org.muses.jeeplatform.annotation; import org.muses.jeeplatform.common.RedisCacheNamespace; import java.lang.annotation.*; /** * 元注解 用来标识查询数据库的方法 */ @Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RedisCache { // RedisCacheNamespace nameSpace(); } JDK 5 提供的注解,除了 Retention 以外,还有另外三个,即 Target、Inherited 和 Documented。基于这个,我们可以实现自定义的元注解 我们设置 RedisCache 基于 Method 方法级别引用。
1.RetentionPolicy.SOURCE 这种类型的 Annotations 只在源代码级别保留,编译时就会被忽略 2.RetentionPolicy.CLASS 这种类型的 Annotations 编译时被保留,在 class 文件中存在,但 JVM 将会忽略 3.RetentionPolicy.RUNTIME 这种类型的 Annotations 将被 JVM 保留,所以他们能在运行时被 JVM 或其他使用反射机制的代码所读取和使用.
package org.muses.jeeplatform.cache; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import javax.annotation.Resource; @Component("redisCache") public class RedisCache { @Autowired private JedisPool jedisPool; private JedisPool getJedisPool(){ return jedisPool; } public void setJedisPool(JedisPool jedisPool){ this.jedisPool = jedisPool; } /** * 从 Redis 缓存获取数据 * @param redisKey * @return */ public Object getDataFromRedis(String redisKey){ Jedis jedis = jedisPool.getResource(); byte[] byteArray = jedis.get(redisKey.getBytes()); if(byteArray != null){ return SerializeUtil.unSerialize(byteArray); } return null; } /** * 保存数据到 Redis * @param redisKey */ public String saveDataToRedis(String redisKey,Object obj){ byte[] bytes = SerializeUtil.serialize(obj); Jedis jedis = jedisPool.getResource(); String code = jedis.set(redisKey.getBytes(), bytes); return code; } } 对象序列化的工具类:
package org.muses.jeeplatform.cache; import java.io.*; public class SerializeUtil { /** * 序列化对象 * @param obj * @return */ public static byte[] serialize(Object obj){ ObjectOutputStream oos = null; ByteArrayOutputStream baos = null; try{ baos = new ByteArrayOutputStream(); oos = new ObjectOutputStream(baos); oos.writeObject(obj); byte[] byteArray = baos.toByteArray(); return byteArray; }catch(IOException e){ e.printStackTrace(); } return null; } /** * 反序列化对象 * @param byteArray * @return */ public static Object unSerialize(byte[] byteArray){ ByteArrayInputStream bais = null; try { //反序列化为对象 bais = new ByteArrayInputStream(byteArray); ObjectInputStream ois = new ObjectInputStream(bais); return ois.readObject(); } catch (Exception e) { e.printStackTrace(); } return null; } } 这里记得 Vo 类都要实现 Serializable 例如菜单信息 VO 类,这是一个 JPA 映射的实体类
package org.muses.jeeplatform.core.entity.admin; import javax.persistence.*; import java.io.Serializable; import java.util.List; /** * @description 菜单信息实体 * @author Nicky * @date 2017 年 3 月 17 日 */ @Table(name="sys_menu") @Entity public class Menu implements Serializable { /** 菜单 Id**/ private int menuId; /** 上级 Id**/ private int parentId; /** 菜单名称**/ private String menuName; /** 菜单图标**/ private String menuIcon; /** 菜单 URL**/ private String menuUrl; /** 菜单类型**/ private String menuType; /** 菜单排序**/ private String menuOrder; /**菜单状态**/ private String menuStatus; private List<Menu> subMenu; private String target; private boolean hasSubMenu = false; public Menu() { super(); } @Id @GeneratedValue(strategy=GenerationType.IDENTITY) public int getMenuId() { return this.menuId; } public void setMenuId(int menuId) { this.menuId = menuId; } @Column(length=100) public int getParentId() { return parentId; } public void setParentId(int parentId) { this.parentId = parentId; } @Column(length=100) public String getMenuName() { return this.menuName; } public void setMenuName(String menuName) { this.menuName = menuName; } @Column(length=30) public String getMenuIcon() { return this.menuIcon; } public void setMenuIcon(String menuIcon) { this.menuIcon = menuIcon; } @Column(length=100) public String getMenuUrl() { return this.menuUrl; } public void setMenuUrl(String menuUrl) { this.menuUrl = menuUrl; } @Column(length=100) public String getMenuType() { return this.menuType; } public void setMenuType(String menuType) { this.menuType = menuType; } @Column(length=10) public String getMenuOrder() { return menuOrder; } public void setMenuOrder(String menuOrder) { this.menuOrder = menuOrder; } @Column(length=10) public String getMenuStatus(){ return menuStatus; } public void setMenuStatus(String menuStatus){ this.menuStatus = menuStatus; } @Transient public List<Menu> getSubMenu() { return subMenu; } public void setSubMenu(List<Menu> subMenu) { this.subMenu = subMenu; } public void setTarget(String target){ this.target = target; } @Transient public String getTarget(){ return target; } public void setHasSubMenu(boolean hasSubMenu){ this.hasSubMenu = hasSubMenu; } @Transient public boolean getHasSubMenu(){ return hasSubMenu; } } 先从 Redis 里获取缓存,查询不到,就查询 MySQL 数据库,然后再保存到 Redis 缓存里,下次查询时直接调用 Redis 缓存
package org.muses.jeeplatform.cache; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; /** * AOP 实现 Redis 缓存处理 */ @Component @Aspect public class RedisAspect { private static final Logger LOGGER = LoggerFactory.getLogger(RedisAspect.class); @Autowired @Qualifier("redisCache") private RedisCache redisCache; /** * 拦截所有元注解 RedisCache 注解的方法 */ @Pointcut("@annotation(org.muses.jeeplatform.annotation.RedisCache)") public void pointcutMethod(){ } /** * 环绕处理,先从 Redis 里获取缓存,查询不到,就查询 MySQL 数据库, * 然后再保存到 Redis 缓存里 * @param joinPoint * @return */ @Around("pointcutMethod()") public Object around(ProceedingJoinPoint joinPoint){ //前置:从 Redis 里获取缓存 //先获取目标方法参数 long startTime = System.currentTimeMillis(); String applId = null; Object[] args = joinPoint.getArgs(); if (args != null && args.length > 0) { applId = String.valueOf(args[0]); } //获取目标方法所在类 String target = joinPoint.getTarget().toString(); String className = target.split("@")[0]; //获取目标方法的方法名称 String methodName = joinPoint.getSignature().getName(); //redis 中 key 格式: applId:方法名称 String redisKey = applId + ":" + className + "." + methodName; Object obj = redisCache.getDataFromRedis(redisKey); if(obj!=null){ LOGGER.info("**********从 Redis 中查到了数据**********"); LOGGER.info("Redis 的 KEY 值:"+redisKey); LOGGER.info("REDIS 的 VALUE 值:"+obj.toString()); return obj; } long endTime = System.currentTimeMillis(); LOGGER.info("Redis 缓存 AOP 处理所用时间:"+(endTime-startTime)); LOGGER.info("**********没有从 Redis 查到数据**********"); try{ obj = joinPoint.proceed(); }catch(Throwable e){ e.printStackTrace(); } LOGGER.info("**********开始从 MySQL 查询数据**********"); //后置:将数据库查到的数据保存到 Redis String code = redisCache.saveDataToRedis(redisKey,obj); if(code.equals("OK")){ LOGGER.info("**********数据成功保存到 Redis 缓存!!!**********"); LOGGER.info("Redis 的 KEY 值:"+redisKey); LOGGER.info("REDIS 的 VALUE 值:"+obj.toString()); } return obj; } } 然后调用 @RedisCache 实现缓存
/** * 通过菜单 Id 获取菜单信息 * @param id * @return */ @Transactional @RedisCache public Menu findMenuById(@RedisCacheKey int id){ return menuRepository.findMenuByMenuId(id); } 登录系统,然后加入 @RedisCache 注解的方法都会实现 Redis 缓存处理
可以看到 Redis 里保存到了缓存
项目代码: https://github.com/u014427391/jeeplatform, 欢迎去 github 上 star(收藏)
1 letitbesqzr 2017-12-15 10:53:40 +08:00 不是有 spring cache 可以做这事,而且可以用 spring data redis 更方便的操作啊 |
2 jack80342 2017-12-16 22:15:11 +08:00 翻译了 Spring Boot 最新的官方文档,https://www.gitbook.com/book/jack80342/spring-boot/details |