您的位置:首页 > 数据库 > Redis

Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)

2018-03-01 12:02 1371 查看
       上一篇,我们已经讲过了,在Windows-64位系统下的redis3.0环境的搭建,其实很简单,就是一个解压缩文件的时间加上鼠标click几下的功夫就可以嗨皮的使用redis了,任何技术都是服务于应用的,没有应用场景,技术也敢叫技术?因此,本篇将结合上一篇,利用Spring-Boot框架,集成mybatis(数据操作用mybatis的通用mapper)+redis(数据缓存)来实现一个简单的中等数据量的查询且如何做到查询的优化以及减少数据库open session的开销。

本篇有点长,因为贴代码很占篇幅。

一、项目目录结构图



demo资源会在文章最后,放在GitHub上

二、Pom依赖

pom.xml(默认是jar包,习惯性,我打成war包)

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion>
<groupId>com.appleyk</groupId>
<artifactId>spring-boot-redis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<description>spring-boot 集成redis,并利用AOP切面(切注解)的方式实现数据缓存操作</description>
<!-- 继承官网最新父POM【假设当前项目不再继承其他POM】 -->
<!-- http://projects.spring.io/spring-boot/#quick-start -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
</parent>
<!-- 使用Java8,嘗試使用新特新【stream和lambda】 -->
<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<!-- Starter POMs是可以包含到应用中的一个方便的依赖关系描述符集合 -->
<!-- 该Starters包含很多你搭建项目, 快速运行所需的依赖, 并提供一致的, 管理的传递依赖集。 -->
<!-- 大多数的web应用都使用spring-boot-starter-web模块进行快速搭建和运行。 -->
<!-- spring-boot-starter-web -->
<!-- 对全栈web开发的支持, 包括Tomcat和 spring-webmvc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency><!-- 添加Mybatis、Spring-Mybatis依赖 -->
<!-- mybatis-spring-boot-starter继承树那是相当全面 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<!-- jackson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.41</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>

<!-- MySql驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- 添加分页插件PageHelper的依赖 -->
<!-- pagehelper-spring-boot-starter的继承树那也是相当丰富啊 -->
<!-- 使用的是PageHelper5.0.1 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<!-- Spring 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 添加热部署 devtools:监听文件变动 -->
<!-- 当Java文件改动时,Spring-boo会快速重新启动 -->
<!-- 最简单的测试,就是随便找一个文件Ctrl+S一下,就可以看到效果 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<!-- optional=true,依赖不会传递 -->
<!-- 本项目依赖devtools;若依赖本项目的其他项目想要使用devtools,需要重新引入 -->
<optional>true</optional>
</dependency>
<!-- JUnit单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>

<!-- mybatis通用mapepr -->
<!-- https://mvnrepository.com/artifact/tk.mybatis/mapper-spring-boot-starter -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>1.1.5</version>
</dependency>
<!-- aop面向切面编程 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.20</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.3.2.RELEASE</version>
</dependency>
</dependencies>
<!-- Spring Boot包含一个Maven插件, 它可以将项目打包成一个可执行jar -->
<build>
<!-- 解决配置资源文件被漏掉问题 -->
<resources>
<!-- 如果出现thymeleaf无法渲染html模板,请加上这个 -->
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
<plugins>
<!-- boot-maven插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*Documentation.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</project>

三、资源文件

(1)



(2)xxx.properties

application-dev.properties

#####开发环境

server.port=8081
server.session.timeout=10
server.tomcat.uri-encoding=utf8

#随机字符串
appleyk.name = ${random.value}
#随机整数
appleyk.age = ${random.int}
#10以内的随机数
appleyk.size = ${random.int(10)}

spring.datasource.max-idle=10
spring.datasource.max-wait=10000
spring.datasource.min-idle=5
spring.datasource.initial-size=5

######MySql连接参数#############
jdbc.url=jdbc\:mysql\://localhost\:3306/test?useUnicode\=true&autoReconnect=true&useSSL=false&characterEncoding\=utf-8&useSSL=true
jdbc.username=root
jdbc.password=root
jdbc.driverClassName=com.mysql.jdbc.Driver

application-prod.properties

#####生产环境

server.port=8088
server.session.timeout=10
server.tomcat.uri-encoding=utf8

#随机字符串
appleyk.name = ${random.value}
#随机整数
appleyk.age = ${random.int}
#10以内的随机数
appleyk.size = ${random.int(10)}

# 主数据源,默认的
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc\:mysql\://localhost\:3306/taotao?useUnicode\=true&autoReconnect=true&useSSL=false&characterEncoding\=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root

## Redis缓存
spring.cache.type=REDIS
spring.redis.database=0
spring.redis.host=localhost
spring.redis.password=
spring.redis.port=6379
spring.redis.pool.max-idle=8
spring.redis.pool.min-idle=0
spring.redis.pool.max-active=100
spring.redis.pool.max-wait=-1
#是否开启redis缓存
spring.redis.cache.on = true

# 下面为连接池的补充设置,应用到上面所有数据源中
# 初始化大小,最小,最大
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
# 配置获取连接等待超时的时间
spring.datasource.maxWait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=true
spring.datasource.testOnReturn=false

#在application.properties文件中引入日志配置文件
#===================================== log =============================
logging.config=classpath:logback-boot.xml


application-test.properties

#####测试环境

server.port=8082
server.session.timeout=10
server.tomcat.uri-encoding=utf8

spring.datasource.max-idle=10
spring.datasource.max-wait=10000
spring.datasource.min-idle=5
spring.datasource.initial-size=5

######MySql连接参数#############
jdbc.url=jdbc\:mysql\://localhost\:3306/taotao?useUnicode\=true&autoReconnect=true&useSSL=false&characterEncoding\=utf-8&useSSL=true
jdbc.username=root
jdbc.password=root
jdbc.driverClassName=com.mysql.jdbc.Driver

application.properties

#SpringApplication将从以下位置加载application.properties文件, 并把它们添加到Spring Environment中:
#1. 当前目录下的一个/config子目录
#2. 当前目录
#3. 一个classpath下的/config包
#4. classpath根路径(root)
#这个列表是按优先级排序的(列表中位置高的将覆盖位置低的) 。
#注:你可以使用YAML('.yml') 文件替代'.properties'

#Spring-Boot多环境配置 -- prod:生产环境
spring.profiles.active = prod

logback-boot.xml(配置日志,控制台和日志记录文件的权限调成error级别,屏蔽掉info和debug信息)

<configuration>
<!-- %m输出的信息,%p日志级别,%t线程名,%d日期,%c类的全名,%i索引【从数字0开始递增】,,, -->
<!-- appender是configuration的子节点,是负责写日志的组件。 -->
<!-- ConsoleAppender:把日志输出到控制台 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %p (%file:%line\)- %m%n</pattern>
<!-- 控制台也要使用UTF-8,不要使用GBK,否则会中文乱码 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- RollingFileAppender:滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
<!-- 以下的大概意思是:1.先按日期存日志,日期变了,将前一天的日志文件名重命名为XXX%日期%索引,新的日志仍然是sys.log -->
<!-- 2.如果日期没有发生变化,但是当前日志的文件大小超过1KB时,对当前日志进行分割 重命名-->
<appender name="syslog"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- <File>log/sys.log</File> -->
<File>opt/spring-boot-web/logs/sys.log</File>
<!-- rollingPolicy:当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名。 -->
<!-- TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 活动文件的名字会根据fileNamePattern的值,每隔一段时间改变一次 -->
<!-- 文件名:log/sys.2017-12-05.0.log -->
<fileNamePattern>log/sys.%d.%i.log</fileNamePattern>
<!-- 每产生一个日志文件,该日志文件的保存期限为30天 -->
<maxHistory>30</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- maxFileSize:这是活动文件的大小,默认值是10MB,本篇设置为1KB,只是为了演示 -->
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<!-- pattern节点,用来设置日志的输入格式 -->
<pattern>
%d %p (%file:%line\)- %m%n
</pattern>
<!-- 记录日志的编码 -->
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
</appender>

<!-- 控制台输出日志级别 -->
<root level="error">
<appender-ref ref="STDOUT" />
</root>
<!-- 指定项目中某个包,当有日志操作行为时的日志记录级别 -->
<!-- com.appley为根包,也就是只要是发生在这个根包下面的所有日志操作行为的权限都是DEBUG -->
<!-- 级别依次为【从高到低】:FATAL > ERROR > WARN > INFO > DEBUG > TRACE -->
<logger name="com.appleyk" level="error">
<appender-ref ref="syslog" />
</logger>

</configuration>

四、annotation(缓存注解)

(1)



(2)CacheNameSpace.java

package com.appleyk.result;

/**
* 缓存key的拼接前缀
* @author yukun24@126.com
* @blob http://blog.csdn.net/appleyk * @date 2018年3月1日-上午11:22:06
*/
public enum CacheNameSpace {
ITEM
}


(2)DeleteCache.java

package com.appleyk.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.appleyk.result.CacheNameSpace;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DeleteCache {
CacheNameSpace nameSpace();
}


(3)QueryCache.java

package com.appleyk.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.appleyk.result.CacheNameSpace;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface QueryCache{
CacheNameSpace nameSpace();
}


(4)QueryCacheKey.java

package com.appleyk.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 注解 QueryCacheKey 是参数级别的注解,用来标注要查询数据的主键,会和上面的nameSpace组合做缓存的key值
* @author yukun24@126.com
* @blob   http://blog.csdn.net/appleyk * @date   2018年2月28日-下午2:01:52
*/

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
@Documented
public @interface QueryCacheKey {

}


关于缓存注解的使用,可以先看一张图(提前放送)



方法加了注解,接下来,如何进行数据缓存操作呢?  别急,AOP切面编程马上来帮忙

五、AOP(本篇精髓所在)

(1)



(2)先来看第一个,ControllerInterceptor.java

package com.appleyk.aop;

import java.util.HashMap;
import java.util.Map;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;

@Aspect
@Component
/**
* 拦截器
* @author yukun24@126.com
* @blob http://blog.csdn.net/appleyk * @date 2018年3月1日-下午1:04:56
* 开启对AspectJ自动代理技术
*
* boolean proxyTargetClass() default false;

描述:启用cglib代理,proxyTargetClass默认为false。

boolean exposeProxy() default false;

描述:如果在一个方法中,调用内部方法,需要在调用内部方法时也能够进行代理,比如内部调用时,使用

(IService)AopContext.currentProxy().sayHello(),需要将exposeProx设置为true,默认为false。
*/
@EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)
public class ControllerInterceptor {

static Logger logger = LoggerFactory.getLogger(ControllerInterceptor.class);
//ThreadLocal 维护变量 避免同步
//ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal<Long> startTime = new ThreadLocal<>();// 开始时间

/**
* map1存放方法被调用的次数O
*/
ThreadLocal<Map<String, Long >> map1 = new ThreadLocal<>();

/**
* map2存放方法总耗时
*/
ThreadLocal<Map<String, Long >> map2 = new ThreadLocal<>();

/**
* 定义一个切入点. 解释下:
*
* ~ 第一个 * 代表任意修饰符及任意返回值. ~ 第二个 * 定义在web包或者子包 ~ 第三个 * 任意方法 ~ .. 匹配任意数量的参数.
*/
static final String pCutStr = "execution(* com.appleyk.*..*(..))";

@Pointcut(value = pCutStr)
public void logPointcut() {
}

@Around("logPointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {

//初始化 一次
if(map1.get() ==null ){
map1.set(new HashMap<>());

}

if(map2.get() == null){
map2.set(new HashMap<>());
}

long start = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();

long end = System.currentTimeMillis();

logger.info("===================");
String tragetClassName = joinPoint.getSignature().getDeclaringTypeName();
String MethodName = joinPoint.getSignature().getName();

Object[] args = joinPoint.getArgs();// 参数
int argsSize = args.length;
String argsTypes = "";
String typeStr = joinPoint.getSignature().getDeclaringType().toString().split(" ")[0];
String returnType = joinPoint.getSignature().toString().split(" ")[0];
logger.info("类/接口:" + tragetClassName + "(" + typeStr + ")");
logger.info("方法:" + MethodName);
logger.info("参数个数:" + argsSize);
logger.info("返回类型:" + returnType);
if (argsSize > 0) {
// 拿到参数的类型
for (Object object : args) {
argsTypes += object.getClass().getTypeName().toString() + " ";
}
logger.info("参数类型:" + argsTypes);
}

Long total = end - start;
logger.info("耗时: " + total + " ms!");

if(map1.get().containsKey(MethodName)){
Long count = map1.get().get(MethodName);
map1.get().remove(MethodName);//先移除,在增加
map1.get().put(MethodName, count+1);

count = map2.get().get(MethodName);
map2.get().remove(MethodName);
map2.get().put(MethodName, count+total);
}else{

map1.get().put(MethodName, 1L);
map2.get().put(MethodName, total);
}

return result;

} catch (Throwable e) {
long end = System.currentTimeMillis();
logger.info("====around " + joinPoint + "\tUse time : " + (end - start) + " ms with exception : "
+ e.getMessage());
throw e;
}

}

//对Controller下面的方法执行前进行切入,初始化开始时间
@Before(value = "execution(* com.appleyk.controller.*.*(..))")
public void beforMehhod(JoinPoint jp) {
startTime.set(System.currentTimeMillis());
}

//对Controller下面的方法执行后进行切入,统计方法执行的次数和耗时情况
//注意,这里的执行方法统计的数据不止包含Controller下面的方法,也包括环绕切入的所有方法的统计信息
@AfterReturning(value = "execution(* com.appleyk.controller.*.*(..))")
public void afterMehhod(JoinPoint jp) {
long end = System.currentTimeMillis();
long total = end - startTime.get();
String methodName = jp.getSignature().getName();
logger.info("连接点方法为:" + methodName + ",执行总耗时为:" +total+"ms");

//重新new一个map
Map<String, Long> map = new HashMap<>();

//从map2中将最后的 连接点方法给移除了,替换成最终的,避免连接点方法多次进行叠加计算
//由于map2受ThreadLocal的保护,这里不支持remove,因此,需要单开一个map进行数据交接
for(Map.Entry<String, Long> entry:map2.get().entrySet()){
if(entry.getKey().equals(methodName)){
map.put(methodName, total);

}else{
map.put(entry.getKey(), entry.getValue());
}
}

for (Map.Entry<String, Long> entry :map1.get().entrySet()) {
for(Map.Entry<String, Long> entry2 :map.entrySet()){
if(entry.getKey().equals(entry2.getKey())){
System.err.println(entry.getKey()+",被调用次数:"+entry.getValue()+",综合耗时:"+entry2.getValue()+"ms");
}
}

}

System.err.println("=======================方法执行效率统计切面结束");
}

}

以上功能有两个,一个是环绕切面,根据切点表达式:execution(* com.appleyk.*..*(..))我们可以看出来,方法



比如,我们放开日志的权限,调成info级别如下



当我们启动Spring-Boot项目的时候,我们的第一个切面方法doAround就起作用了(方法分析统计),效果如下:



如果我们把logback的日志输出级别调成:error



则我们关掉项目,再次启动的时候,就看不到之前上面的切面输出的效果了(因为error级别比info高,info不会出现)



以上说明,logback日志权限级别不是乱设置的,是有讲究的,哈哈,我们继续

上面是第一个切点的编程,第二是什么呢?



上述图中注释说明的很清楚了,两个注解,一个是前置通知@Before,在方法调用前,记录开始时间,一个是方法执行后的通知@AfterReturning,这个实现方法的效率统计,比如xxx方法总共被调用了多少次、耗费了多少时间(毫秒),效果如下:



我们继续看另一个AOP编程,本篇的重点

(3)RedisCacheAspect.java

package com.appleyk.aop;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

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.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.appleyk.annotation.DeleteCache;
import com.appleyk.annotation.QueryCache;
import com.appleyk.annotation.QueryCacheKey;
import com.appleyk.result.CacheNameSpace;

@Aspect
@Component

/**
* 利用AOP配合注解,实现redis缓存的写入和删除
* @author yukun24@126.com
* @blob   http://blog.csdn.net/appleyk * @date   2018年3月1日-下午1:35:30
*/
public class RedisCacheAspect {

static Logger logger = LoggerFactory.getLogger(ControllerInterceptor.class);

@Autowired
private RedisTemplate redisTemplate;

/**
* 是否开启redis缓存,将查询的结果写入value
*/
@Value("${spring.redis.cache.on}")
private boolean isOn;

/**
* 定义拦截规则:拦截所有@QueryCache注解的方法 -- 查询。
*/
@Pointcut("@annotation(com.appleyk.annotation.QueryCache)")
public void queryCachePointcut() {
}

/**
* 拦截器具体实现
*
* @param point
* @return
* @throws Throwable
*/
@Around("queryCachePointcut()")
public Object InterceptorByQuery(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
if (!isOn) {
// 如果不开启redis缓存的话,直接走原方法进行查询
Object object = point.proceed();
return object;
}

System.err.println("AOP 缓存切面处理 >>>> start ");
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod(); // 获取被拦截的方法
// 拿到方法上标注的注解的namespace的值
CacheNameSpace cacheType = method.getAnnotation(QueryCache.class).nameSpace();
String key = null;
int i = 0;

// 循环所有的参数
for (Object value : point.getArgs()) {

MethodParameter methodParam = new SynthesizingMethodParameter(method, i);
Annotation[] paramAnns = methodParam.getParameterAnnotations();
// 循环参数上所有的注解
for (Annotation paramAnn : paramAnns) {
if (paramAnn instanceof QueryCacheKey) { //
QueryCacheKey requestParam = (QueryCacheKey) paramAnn;
key = cacheType.name() + "_" + value; // 取到QueryCacheKey的标识参数的值
}
}
i++;
}

/**
* 如果没有参数的话,设置Key值
*/
if (key == null) {
key = cacheType.name();
}

// 获取不到key值,抛异常
if (StringUtils.isEmpty(key))
throw new Exception("缓存key值不存在");

ValueOperations<String, Object> operations = redisTemplate.opsForValue();
boolean hasKey = redisTemplate.hasKey(key);
if (hasKey) {

// 缓存中获取到数据,直接返回。
Object object = operations.get(key);
System.err.println("本次查询缓存命中,从缓存中获取到数据 >>>> key = " + key);
System.err.println("AOP 缓存切面处理 >>>> end 耗时:" + (System.currentTimeMillis() - beginTime) + "ms");
return object;
}

// 缓存中没有数据,调用原始方法查询数据库
Object object = point.proceed();

operations.set(key, object, 30, TimeUnit.SECONDS); // 设置超时时间30s

System.err.println("本次查询缓存未命中,DB取到数据并存入缓存 >>>> key =" + key);
System.err.println("AOP 缓存切面处理 >>>> end 耗时:" + (System.currentTimeMillis() - beginTime) + "ms");
// redisTemplate.delete(key);
return object;

}

/**
* 定义拦截规则:拦截所有@DeleteCache注解的方法 -- 用于修改表数据时,删除redis缓存中的key值。
* 也可以使用切面表达式execution(* com.appleyk.*..*(..)) 切和更新数据相关的方法
*/
@Pointcut("@annotation(com.appleyk.annotation.DeleteCache)")
public void deleteCachePointcut() {
}

/**
* 拦截器具体实现
*
* @param point
* @return
* @throws Throwable
*/
@Around("deleteCachePointcut()")
public Object InterceptorBySave(ProceedingJoinPoint point) throws Throwable {

long beginTime = System.currentTimeMillis();
if (!isOn) {
// 如果不开启redis缓存的话,直接走原方法进行查询
Object object = point.proceed();
return object;
}
System.err.println("AOP 缓存切面处理 【清除key】>>>> start ");
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod(); // 获取被拦截的方法

// 拿到方法上标注的注解的namespace的值
CacheNameSpace cacheType = method.getAnnotation(DeleteCache.class).nameSpace();

String key = cacheType.name();
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
boolean hasKey = redisTemplate.hasKey(key);
if (hasKey) {

System.err.println("key存在,执行删除 >>>> key = " + key);
/**
* 删除key
*/
redisTemplate.delete(key);
}
System.err.println("AOP 缓存切面处理 【清除key】>>>> end 耗时:" + (System.currentTimeMillis() - beginTime) + "ms");
Object object = point.proceed();
return object;

}

}


注意以下几点

A.
RedisTemplate -- spring 封装了 RedisTemplate 对象来进行对redis的各种操作,它支持所有的 redis 原生的 api。



这里我们可以大胆的使用@Autowired注入我们需要的RedisTemplate 对象,是因为我们在Pom文件中依赖了如下



而Spring-Boot启动的时候,也会将redis相关的资源添加到Spring容器中(至于资源是否存在,还有待验证)

我们可以看一下spring-boot-starter-redis的依赖树,如下



B.

#是否开启redis缓存
spring.redis.cache.on = true



C. 
redis缓存key的生成策略(当然下面是简单的组成,实际应用中,key的值要比下面的复杂的多,本篇知道就行)



D. 

缓存命中与否



E. 

删除缓存(保证,在修改数据的时候,重刷相应redis缓存,避免缓存数据和实际DB数据不同步)



说完AOP,我们在来说一下,本篇的数据源配置

六、Mybatis数据源配置(Bean注入)

(1)



(2)MybatisConfig.java
package com.appleyk.config;

import java.util.Properties;

import javax.sql.DataSource;

import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.github.pagehelper.PageInterceptor;

@Configuration
@EnableTransactionManagement//开启事务
@EnableConfigurationProperties(DataSourceProperties.class)
//扫描一切和Mapper有关的bean,因此,下面对整个项目进行"全身"扫描
@MapperScan("com.appleyk")
public class MybatisConfig {

@Bean(name = "dataSource")
//Spring 允许我们通过 @Qualifier注释指定注入 Bean 的名称
@Qualifier(value = "dataSource")
@ConfigurationProperties(prefix="spring.datasource")
@Primary
public DataSource dataSource()
{
return DataSourceBuilder.create().build();
}

//创建SqlSessionFactory
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactoryBean(@Qualifier("dataSource") DataSource dataSource){

SqlSessionFactoryBean bean = new SqlSessionFactoryBean();

//1.设置数据源
bean.setDataSource(dataSource);
//2.给包中的类注册别名,注册后可以直接使用类名,而不用使用全限定的类名(就是不用包含包名)
bean.setTypeAliasesPackage("com.appleyk.database");

// 设置MyBatis分页插件 【PageHelper 5.0.1设置方法】
PageInterceptor pageInterceptor = new PageInterceptor();
Properties properties = new Properties();
properties.setProperty("helperDialect", "mysql");
properties.setProperty("offsetAsPageNum", "true");
properties.setProperty("rowBoundsWithCount", "true");
pageInterceptor.setProperties(properties);

//添加插件
bean.setPlugins(new Interceptor[]{pageInterceptor});

//添加XML目录,进行Mapper.xml扫描
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
try {
//项目中的xxxMapper.xml位于包com.appleyk.mapepr下面
bean.setMapperLocations(resolver.getResources("classpath*:com/appleyk/mapepr/*.xml"));
return bean.getObject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}

//创建SqlSessionTemplate
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}

@Bean(name = "transactionManager")
@Primary
public DataSourceTransactionManager testTransactionManager(@Qualifier("dataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}

这个没什么好说的,不懂的地方,留言备注

七、MySql数据示例

(1)使用taotao数据库里面的tb_item表里的数据作为本篇的查询依据



(2)没有的,可以自己新建一个表,批量插入XXX条数据,扩展自己的查询数据集

八、使用Mybatis通用mapper完成DAO层的设计和编写

(1)依赖的包,在Pom文件中体现如下:



(2)tb_item表Java实体类映射



Tbitem.java
package com.appleyk.entity;

import java.io.Serializable;
import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.Table;

import org.apache.ibatis.type.JdbcType;

import tk.mybatis.mapper.annotation.ColumnType;

@Table(name = "tb_item")
public class TbItem implements Serializable{

@Id
@Column(name = "id")
@ColumnType(jdbcType = JdbcType.BIGINT)
private Long id;
private String title;
private String sell_point;
private Long price;
private Integer num;
private String barcode;
private String image;
private Integer cid;
private Integer status;
private Date created;
private Date updated;

public TbItem(){

}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getSell_point() {
return sell_point;
}

public void setSell_point(String sell_point) {
this.sell_point = sell_point;
}

public Long getPrice() {
return price;
}

public void setPrice(Long price) {
this.price = price;
}

public Integer getNum() {
return num;
}

public void setNum(Integer num) {
this.num = num;
}

public String getBarcode() {
return barcode;
}

public void setBarcode(String barcode) {
this.barcode = barcode;
}

public String getImage() {
return image;
}

public void setImage(String image) {
this.image = image;
}

public Integer getCid() {
return cid;
}

public void setCid(Integer cid) {
this.cid = cid;
}

public Integer getStatus() {
return status;
}

public void setStatus(Integer status) {
this.status = status;
}

public Date getCreated() {
return created;
}

public void setCreated(Date created) {
this.created = created;
}

public Date getUpdated() {
return updated;
}

public void setUpdated(Date updated) {
this.updated = updated;
}

}

需要注意的地方



其他的比葫芦画瓢,可以扩展自己的实体类,


(3)tb_item实体类对应的mapper操作



TbItemMapper.java

package com.appleyk.mapper;

import com.appleyk.entity.TbItem;

import tk.mybatis.mapper.common.Mapper;

public interface TbItemMapper extends Mapper<TbItem>{

}

什么叫通用mapper,也就是帮我们省去了基本的增删改查语句,无需配置mapepr.xml,无需我们写一句代码,mybatis就可以帮助我们实现tb_item这个表的简单数据操作(其实也不能说是简单,因为单表操作也就那回事,



有了DAO层,接下来,我们就需要借助Service层来调用了

九、Service层(缓存注解的使用)

(1)



(2)TbItemService.java

package com.appleyk.service;

import java.util.List;

import com.appleyk.entity.TbItem;

public interface TbItemService {

/**
* 查询全部商品
* @return
*/
List<TbItem> GetTbItems();

/**
* 根据商品ID查询
* @param id
* @return
*/
TbItem GetTbItem(Long id);

/**
* 保存商品
* @param tbItem
* @return
*/
boolean SaveTbItems(TbItem tbItem);
}


(3)TbItemServiceImpl.java

package com.appleyk.service.Impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

import com.appleyk.annotation.DeleteCache;
import com.appleyk.annotation.QueryCache;
import com.appleyk.entity.TbItem;
import com.appleyk.mapper.TbItemMapper;
import com.appleyk.result.CacheNameSpace;
import com.appleyk.service.TbItemService;

import tk.mybatis.mapper.entity.Example;

@Service
@Primary
public class TbItemServiceImpl implements TbItemService {

@Autowired
private TbItemMapper tbItemMapper ;

/**
*
* 获取商品: 如果缓存存在,从缓存中获取商品信息 如果缓存不存在,从DB中获取商品信息,然后插入缓存
*
*/
@Override
@QueryCache(nameSpace = CacheNameSpace.ITEM)
public List<TbItem> GetTbItems() {

Example example = new Example(TbItem.class);

List<TbItem> tbItems = tbItemMapper.selectByExample(example);
return tbItems;
}

@Override
public TbItem GetTbItem(Long id) {
//直接根据主键返回商品实体
return tbItemMapper.selectByPrimaryKey(id);
}

@Override
@DeleteCache(nameSpace = CacheNameSpace.ITEM)
public boolean SaveTbItems(TbItem tbItem) {
/**
* 这里不做操作,只是模拟,到这一步的时候,切面执行删除查询的时候写入缓存中的key
*/

return true;
}

}

这个没什么好说的,就是两个查询,和一个没有实现保存效果的方法(写代码很累的.....)

但是别小看他们,他们可是加了缓存注解的,你要知道,我们上面再讲AOP的时候,可提到过,有一个AOP编程切的就是带有这个缓存注解的方法,从而实现redis缓存操作的,不信,我们继续往下走,Service层有了,该轮到我们的Controller层了

十、Controller层(提供Restful API风格的接口)

(1)



(1)TbItemController.java

package com.appleyk.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.appleyk.entity.TbItem;
import com.appleyk.result.ResponseMessage;
import com.appleyk.result.ResponseResult;
import com.appleyk.service.TbItemService;

@RestController
@RequestMapping("/rest/v1.0.1/database/tbitem")
public class TbItemController {

@Autowired
private TbItemService itemService;

@GetMapping("/query")
public ResponseResult GetTbItems() {
List<TbItem> result = itemService.GetTbItems();
return new ResponseResult(200, "查询成功,size = " + result.size(), result);
}

@PostMapping("/save")
public ResponseResult SaveTbItem() {
TbItem tbItem = new TbItem();
if (itemService.SaveTbItems(tbItem)) {
return new ResponseResult(ResponseMessage.OK);
}

return new ResponseResult(ResponseMessage.INTERNAL_SERVER_ERROR);

}
}

这个也没有上面好说的,万事俱备,只欠东风!

接下来,我们实际演示一下,走个调用

十一、Spring-Boot启动(run)

(1)前提一定要保证,本机的redis-server是开着的





十二、API专业测试工具Insomnia的使用

(1)先来个查询的



第一次查询,我们后台AOP切入的结果输出:



第二次查询,我们后台AOP切入的结果输出:



第三次查询,我们后台AOP切入的结果输出:



如果我们在第一次查询数据的时候,紧接着来了一个保存商品信息的操作,会看到如下效果输出(具体调用不在放出)



十三、关闭缓存支持

(1)



(2)效果如下



项目GitHUb资源链接:https://github.com/kobeyk/appleyk-spring-boot.git
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: