ZB-046-03AOP在Spring中应用

AOP和Spring

  • 在Spring项目里使用 AOP完成Redis缓存
  • Spring是如何切换 JDK动态代理和CGLIB的?
    • spring.aop.proxy-target-class=true
  • @Aspect声明切片
    • @Before
    • @After
    • @Around

实战:获取 Rank数据实现Redis缓存功能

RankService.doService 实现缓存功能

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class RankService {
@Autowired
private UserMapper userMapper;

@Autowired
RankDao rankDao;

public List<RankItem> doService(){
return rankDao.getRank();
}
}

仓库地址

https://github.com/slTrust/spring_ecosystem 从零开始可以从 spring生态系统开新分支

  • 搜索springboot aop starter
  • 引入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
  • 我们知道spring aop有两种方式一个是 JDK动态代理(对应接口) 一个是 CGLIB(对应类)

    • 而当我们需要使用CGLIB来实现AOP的时候,需要配置spring.aop.proxy-target-class=true,不然默认使用的是标准Java的实现。
  • 在application.properties里添加这句话 spring.aop.proxy-target-class=true
    • 这一点在文档里有提到
  • 新建 CacheAspect.java
    1
    2
    3
    4
    5
    6
    // @Aspect 声明它是一个切面
    // @Configuration 是跟 spring有关的配置 运行的时候把它拷贝进来
    @Aspect
    @Configuration
    public class CacheAspect {
    }

接下来处理拦截

  • src/main/java/hello/anno/Cache.java 注解
1
2
3
4
5
6
7
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {

}

这样任何带有@Cache 的地方都可以被我们拦截到

RankService.java里添加为这个doService() 添加这个@Cache注解,来在它调用的时候拦截到它

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class RankService {
@Autowired
private UserMapper userMapper;

@Autowired
RankDao rankDao;

@Cache
public List<RankItem> doService(){
return rankDao.getRank();
}
}

回到CacheAspect.java 里指定拦截 @Cache的注解

1
2
3
4
5
6
7
8
9
10
11
// @Aspect 声明它是一个切面
// @Configuration 是跟 spring有关的配置 运行的时候把它拷贝进来
@Aspect
@Configuration
public class CacheAspect {
@Around("@annotation(hello.anno.Cache)")
public Object cache(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("method is called");
return joinPoint.proceed();
}
}

运行项目 打开地址 http://localhost:8080/rank 如果打印method is called 代表配置成功

先用 Map 做缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// @Aspect 声明它是一个切面
// @Configuration 是跟 spring有关的配置 运行的时候把它拷贝进来
@Aspect
@Configuration
public class CacheAspect {
Map<String,Object> cache = new HashMap<>();

@Around("@annotation(hello.anno.Cache)")
public Object cache(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("method is called");

MethodSignature signature = (MethodSignature)joinPoint.getSignature();
String methodName = signature.getName();
Object cacheValue = cache.get(methodName);

if(cacheValue != null){
System.out.println("cacheValue");
return cacheValue;
}else{
System.out.println("read realValue");
Object realValue = joinPoint.proceed();
cache.put(methodName,realValue);
return realValue;
}

}
}
  • 代码仓库 选择 spring-aop基于map的缓存实现 分支

Redis

  • 广泛使用的内存缓存
  • 常见的数据结构
    • String/List/Set/Hash/ZSet
  • Redis 为什么那么快
    • 完全基于内存
    • 优秀的数据结构设计
    • 单一线程,避免上下文切换开销
    • 事件驱动,非阻塞

如何用呢? 当然是 Docker Redis

  • google redis docker
    • 直接一句话就能运行 docker run --name some-redis -d redis
    • 如果使用缓存你就docker run --name some-redis -d redis redis-server --appendonly yes
  • 由于 redis是有 端口的所以 我们实际应该运行 docker run -p 6379:6379 --name some-redis -d redis

配置 redis 在 springboot 里

  • google springboot redis
  • 在application.properties 添加

    1
    2
    spring.redis.host=localhost
    spring.redis.port=6379
  • 引入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  • 参考文档配置 RedisTemplate

  • 新建类 hello/config/AppConfig.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    public class AppConfig {
    @Bean
    RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
    RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(factory);
    return redisTemplate;
    }
    }
  • 修改 CacheAspect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CacheAspect {
@Autowired
RedisTemplate<String,Object> redisTemplate;

@Around("@annotation(hello.anno.Cache)")
public Object cache(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("method is called");

MethodSignature signature = (MethodSignature)joinPoint.getSignature();
String methodName = signature.getName();
Object cacheValue = redisTemplate.opsForValue().get(methodName);

if(cacheValue != null){
System.out.println("cacheValue");
return cacheValue;
}else{
System.out.println("read realValue");
Object realValue = joinPoint.proceed();
redisTemplate.opsForValue().set(methodName,realValue);
return realValue;
}

}
}
  • 运行项目 访问 http://localhost:8080/rank
    • 结果报错了 java.io.NotSerializableException: hello.entity.RankItem 就是没法序列号
    • 我们知道java跟 redis 通讯 肯定是通过网络通讯的,那么传递的一定是字节流
    • 如何把 Java对象 变成 字节流呢?
      • 这个过程叫做 序列化
  • 给 RankItem 实现 Serializable 接口public class RankItem implements Serializable {...}
  • 给 User 也实现这个接口 public class User implements Serializable {...}
  • 此时 再次运行项目打开网址 http://localhost:8080/rank2
  • 至此我们就完成了 基于AOP和Spring实现Redis的缓存功能

为什么要用 Redis

  • 在使用上 Redis 和 Map 没有任何区别

但是为什么要用 Redis?

  • 分布式部署,你的应用 在机器里跑上面着JVM,处理请求
    • 问题来了,如果一台机子,你无法保证服务不中断,不升级,假如压力大了很容易把机器压垮了
    • 因此我们需要分布式部署,所谓分布式部署,多台机器多个 JVM 它们可以轮流处理请求,
      • 此时好处就是:容灾一个机器挂了,还能保证服务运行,分担压力
      • 在者就是 Map和 Redis的区别了
      • 多个个 JVM运行你的 应用,Map是在各自的内存里的 无法实现共享
      • 而 Redis 则是一个中心化的缓存在多个机器上共享缓存

代码仓库master分支

https://github.com/slTrust/spring_ecosystem