@EnableCaching

spring boot提供了比较简单的缓存方案。只要使用 @EnableCaching 即可完成简单的缓存功能。

https://blog.csdn.net/micro_hz/article/details/76599632

参考博客

Spring的面向切面编程(AOP)

Spring AOP 实现 Redis 缓存切面

SpringBoot中通过自定义缓存注解(AOP切面拦截)实现数据库数据缓存到Redis

SpringBoot + Redis:基本配置及使用

Spring中自定义注解支持SpEl表达式(仅限在AOP中使用)

添加依赖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring2.0集成redis所需common-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.8.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>4.3.14.RELEASE</version>
</dependency>

yml配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
spring:
  data:
    redis:
      repositories:
        enabled: false
  redis:
    # Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0
    database: 0
    host: localhost
    port: 6379
    # 连接密码(默认为空)
    password:
    # 连接超时时间(毫秒)
    timeout: 10000ms
    lettuce:
      pool:
        # 连接池最大连接数(使用负值表示没有限制) 默认 8
        max-active: 8
        # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
        max-wait: -1
        # 连接池中的最大空闲连接 默认 8
        max-idle: 8
        # 连接池中的最小空闲连接 默认 0
        min-idle: 0

代码

自定义RedisTemplate

使用fastjson进行序列化

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package njgis.opengms.portal.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @Description
 * @Author bin
 * @Date 2022/07/19
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> template(RedisConnectionFactory factory) {
        // 创建RedisTemplate<String, Object>对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);
        // redis key 序列化方式使用stringSerial
        template.setKeySerializer(new StringRedisSerializer());
        // redis value 序列化方式自定义
        // template.setValueSerializer(new GenericFastJsonRedisSerializer());
        template.setValueSerializer(valueSerializer());
        // redis hash key 序列化方式使用stringSerial
        template.setHashKeySerializer(new StringRedisSerializer());
        // redis hash value 序列化方式自定义
        // template.setHashValueSerializer(new GenericFastJsonRedisSerializer());
        template.setHashValueSerializer(valueSerializer());
        return template;
    }

    private RedisSerializer<Object> valueSerializer() {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
            new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

        // 此项必须配置,否则如果序列化的对象里边还有对象,会报如下错误:
        //     java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
        objectMapper.activateDefaultTyping(
            objectMapper.getPolymorphicTypeValidator(),
            ObjectMapper.DefaultTyping.NON_FINAL,
            JsonTypeInfo.As.PROPERTY);
        // 旧版写法:
        // objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        return jackson2JsonRedisSerializer;
    }

}

redis缓存注解:插入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import njgis.opengms.portal.enums.ItemTypeEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Description 新增redis缓存
 * @Author bin
 * @Date 2022/07/19
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AopCacheEnable {
    //redis缓存key
    String key();
    //redis缓存存活时间默认值(可自定义)
    long expireTime() default 3600;
    //redis缓存的分组
    ItemTypeEnum group() default ItemTypeEnum.PortalItem;
}

redis缓存注解:删除

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Description
 * @Author bin
 * @Date 2022/07/19
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AopCacheEvict {
    //redis中的key值
    String key();
    //redis缓存的分组
    String group();
}

自定义缓存切面具体实现类

CacheEnableAspect.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
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import lombok.extern.slf4j.Slf4j;
import njgis.opengms.portal.enums.ItemTypeEnum;
import njgis.opengms.portal.service.RedisService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;

/**
 * @Description 自定义缓存切面具体实现类
 * @Author bin
 * @Date 2022/07/19
 */
@Slf4j
@Aspect
@Component
public class CacheEnableAspect {


    @Autowired
    RedisService redisService;


    /**
     * 用于SpEL表达式解析.
     */
    private SpelExpressionParser parser = new SpelExpressionParser();
    /**
     * 用于获取方法参数定义名字.
     */
    private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();


    /**
     * Mapper层切点 使用到了我们定义的 AopCacheEnable 作为切点表达式。
     */
    @Pointcut("@annotation(njgis.opengms.portal.component.AopCacheEnable)")
    public void queryCache() {
    }

    /**
     * Mapper层切点 使用到了我们定义的 AopCacheEvict 作为切点表达式。
     */
    @Pointcut("@annotation(njgis.opengms.portal.component.AopCacheEvict)")
    public void ClearCache() {
    }

    @Around("queryCache()")
    public Object Interceptor(ProceedingJoinPoint pjp) throws Throwable {

        // StringBuilder redisKeySb = new StringBuilder("AOP").append("::");
        StringBuilder redisKeySb = new StringBuilder("AOP");

        // 类
        // String className = pjp.getTarget().toString().split("@")[0];
        // redisKeySb.append(className).append("::");

        //获取当前被切注解的方法名
        Method method = getMethod(pjp);

        //获取当前被切方法的注解
        AopCacheEnable aopCacheEnable = method.getAnnotation(AopCacheEnable.class);
        if (aopCacheEnable == null) {
            return pjp.proceed();
        }

        //获取被切注解方法返回类型
        // Type returnType = method.getAnnotatedReturnType().getType();
        // String[] split = returnType.getTypeName().split("\\.");
        // String type = split[split.length - 1];
        // redisKeySb.append(":").append(type);

        //从注解中获取key
        //通过注解key使用的SpEL表达式获取到SpEL执行结果
        String key = aopCacheEnable.key();
        // redisKeySb.append(args);
        String resV = generateKeyBySpEL(key, pjp).toString();
        redisKeySb.append(":").append(aopCacheEnable.group()).append(":").append(resV);


        //获取方法参数值
        // Object[] arguments = pjp.getArgs();
        // redisKeySb.append(":").append(arguments[0]);

        String redisKey = redisKeySb.toString();
        Object result = redisService.get(redisKey);
        if (result != null) {
            log.info("从Redis中获取数据:{}", result);
            return result;
        } else {
            try {
                result = pjp.proceed();
                log.info("从数据库中获取数据:{}", result);
            } catch (Throwable e) {
                throw new RuntimeException(e.getMessage(), e);
            }

            // 获取失效时间
            long expire = aopCacheEnable.expireTime();
            redisService.set(redisKey, result, expire);
        }

        return result;
    }

    /*** 定义清除缓存逻辑,先操作数据库,后清除缓存*/
    @Around(value = "ClearCache()")
    public Object evict(ProceedingJoinPoint pjp) throws Throwable {
        StringBuilder redisKeySb = new StringBuilder("AOP");

        Method method = getMethod(pjp);
        // 获取方法的注解
        AopCacheEvict cacheEvict = method.getAnnotation(AopCacheEvict.class);
        if (cacheEvict == null) {
            return pjp.proceed();
        }

        //从注解中获取key
        //通过注解key使用的SpEL表达式获取到SpEL执行结果
        String key = cacheEvict.key();
        // redisKeySb.append(args);
        key = generateKeyBySpEL(key, pjp).toString();

        //清楚缓存的group从参数拿
        String group = cacheEvict.group();
        ItemTypeEnum type = (ItemTypeEnum)generateKeyBySpEL(group, pjp);
        redisKeySb.append(":").append(type).append(":").append(key);

        //先操作db
        Object result = pjp.proceed();

        //根据key从缓存中删除
        String redisKey = redisKeySb.toString();
        redisService.delete(redisKey);

        return result;

    }

    /**
     * 获取被拦截方法对象
     */
    public Method getMethod(ProceedingJoinPoint pjp) {
        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = methodSignature.getMethod();
        return targetMethod;
    }



    public Object generateKeyBySpEL(String spELString, ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String[] paramNames = nameDiscoverer.getParameterNames(methodSignature.getMethod());
        Expression expression = parser.parseExpression(spELString);
        EvaluationContext context = new StandardEvaluationContext();
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            context.setVariable(paramNames[i], args[i]);
        }
        return expression.getValue(context);
    }
}

注意这里的queryCache和ClearCache,里面切点表达式

分别对应上面自定义的两个AopCacheEnable和AopCacheEvict。

然后在环绕通知的queryCache方法执行前后时

获取被切方法的参数,参数中的key,然后根据key去redis中去查询

Service层

redis服务接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public interface RedisService {
    void set(String key, Object value);

    void set(String key, Object value, long expire);

    Object get(String key);

    void expire(String key, long expire);

    void delete(String key);


    // 由于dao接口同时继承了MongoRepository和GenericItemDao,
    // 所以这边写接口调用他们防止继承冲突
    PortalItem insertItem(PortalItem item, ItemTypeEnum type);

    PortalItem saveItem(PortalItem item, ItemTypeEnum type);

    void deleteItem(PortalItem item, ItemTypeEnum type);

}

AopCacheEnable测试

1
2
3
4
public interface ModelItemDao extends MongoRepository<ModelItem,String>, GenericItemDao<ModelItem> {
    @AopCacheEnable(key = "#id", group = ItemTypeEnum.ModelItem)
    ModelItem findFirstById(String id);
}

AopCacheEvict测试

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Service("redisService")
public class RedisServiceImpl implements RedisService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    GenericService genericService;

    @Override
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    @Override
    public void set(String key, Object value, long expire) {
        redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
    }

    @Override
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    @Override
    public void expire(String key, long expire) {
        redisTemplate.expire(key, expire, TimeUnit.SECONDS);
    }

    @Override
    public void delete(String key) {
        redisTemplate.delete(key);
    }

    @Override
    public PortalItem insertItem(PortalItem item, ItemTypeEnum type) {
        GenericItemDao itemDao = (GenericItemDao)genericService.daoFactory(type).get("itemDao");
        PortalItem result = (PortalItem)itemDao.insert(item);
        return result;
    }

    @Override
    @AopCacheEvict(key = "#item.id", group = "#type")
    public PortalItem saveItem(PortalItem item, ItemTypeEnum type) {
        GenericItemDao itemDao = (GenericItemDao)genericService.daoFactory(type).get("itemDao");
        PortalItem result = (PortalItem)itemDao.save(item);
        return result;
    }

    @Override
    public void deleteItem(PortalItem item, ItemTypeEnum type) {
        GenericItemDao itemDao = (GenericItemDao)genericService.daoFactory(type).get("itemDao");
        itemDao.delete(item);
    }
}

测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Test
public void aopFindTest(){
    for (int i = 0; i < 5; i++) {
        ModelItem modelItem = modelItemDao.findFirstById("3f6857ba-c2d2-4e27-b220-6e5367803a12");
        System.out.println(modelItem);
    }
}

@Test
public void aopSaveTest(){
    ModelItem modelItem = modelItemDao.findFirstById("3f6857ba-c2d2-4e27-b220-6e5367803a12");
    modelItem.setThumbsUpCount(50);
    redisService.saveItem(modelItem,ItemTypeEnum.ModelItem);
}

遇到的问题

问题:

Could not safely identify store assignment for repository candidate interface

条件: 使用了Spring data jpa 作为持久层框架并同时使用starter引入了Elasticsearch或Redis依赖包。

原因: RedisRepositoriesAutoConfiguration或ElasticsearchRepositoriesAutoConfiguration 里面的注解@ConditionalOnProperty会判断 spring.data.redis/elasticsearch.repositories.enabled 这个配置项是否存在。若存在会自动扫描继承org.springframework.data.repository.Repository的实体Repository接口。

解决办法:

1
2
3
4
5
6
7
8
spring:
  data:
    redis:
      repositories:
        enabled: false
    elasticsearch:
      repositories:
        enabled: false

问题:

Redis获取缓存异常

Resolved [java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.alibaba.fastjson.JSONObject]

出现场景:

SpringBoot项目中使用Redis来进行缓存。把数据放到缓存中时没有问题,但从缓存中取出来反序列化为对象时报错:“java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to xxx”。(xxx为反序列化的目标对象对应的类。)

只有这个类里有其他对象字段才会报这个问题,如果这个类里都是初始的类型(比如:Integer,String)则不会报这个错误。

只要用到Redis序列化反序列化的地方都会遇到这个问题,比如:RedisTemplate,Redisson,@Cacheable注解等。

原因:

SpringBoot 的缓存使用 jackson 来做数据的序列化与反序列化,如果默认使用 Object 作为序列化与反序列化的类型,则其只能识别 java 基本类型,遇到复杂类型时,jackson 就会先序列化成 LinkedHashMap ,然后再尝试强转为所需类别,这样大部分情况下会强转失败。

解决方法:

出现这种异常,需要自定义ObjectMapper,设置一些参数,而不是直接使用Jackson2JsonRedisSerializer类中黙认的ObjectMapper,看源代码可以知道,Jackson2JsonRedisSerializer中的ObjectMapper是直接使用new ObjectMapper()创建的,这样ObjectMapper会将Redis中的字符串反序列化为java.util.LinkedHashMap类型,导致后续Spring对其进行转换成报错。其实我们只要它返回Object类型就可以了。

修改RedisTemplate这个bean的valueSerializer,设置默认类型。

参考博客:

https://blog.51cto.com/knifeedge/5010643

修改配置类

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> template(RedisConnectionFactory factory) {
        // 创建RedisTemplate<String, Object>对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);
        // redis key 序列化方式使用stringSerial
        template.setKeySerializer(new StringRedisSerializer());
        // redis value 序列化方式自定义,使用jackson会出现转换类型的错误
        // template.setValueSerializer(new GenericFastJsonRedisSerializer());
        template.setValueSerializer(valueSerializer());
        // redis hash key 序列化方式使用stringSerial
        template.setHashKeySerializer(new StringRedisSerializer());
        // redis hash value 序列化方式自定义,使用jackson会出现转换类型的错误
        // template.setHashValueSerializer(new GenericFastJsonRedisSerializer());
        template.setHashValueSerializer(valueSerializer());
        return template;
    }


    private RedisSerializer<Object> valueSerializer() {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
            new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

        // 此项必须配置,否则如果序列化的对象里边还有对象,会报如下错误:
        //     java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
        objectMapper.activateDefaultTyping(
            objectMapper.getPolymorphicTypeValidator(),
            ObjectMapper.DefaultTyping.NON_FINAL,
            JsonTypeInfo.As.PROPERTY);
        // 旧版写法:
        // objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        return jackson2JsonRedisSerializer;
    }

}

问题:

注解不生效

==注解生效代码==

 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
28
@UserCacheEnable(key = "#email")
public JSONObject getItemUserInfoByEmail(String email) {
    User user = null;
    JSONObject userJson = new JSONObject();
    if (email.contains("@")){
        user = userDao.findFirstByEmail(email);
    } else {
        user = userDao.findFirstByAccessId(email);
        if (user != null){
            email = user.getEmail();
        } else {
            userJson.put("name", email);
            userJson.put("email", null);
            userJson.put("accessId", null);
            userJson.put("image", null);
            return userJson;
        }

    }
    JSONObject userInfo = getInfoFromUserServer(email);
    userJson.put("name", userInfo.getString("name"));
    // userJson.put("id", user.getId());
    userJson.put("email", user.getEmail());
    userJson.put("accessId", user.getAccessId());
    // userJson.put("image", user.getAvatar().equals("") ? "" : htmlLoadPath + user.getAvatar());
    userJson.put("image", userInfo.getString("avatar"));
    return userJson;
}

==注解失效代码==

 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
28
29
30
31
32
33
@UserCacheEnable(key = "#email")
public JSONObject getInfoFromUserServer(String email){
    // return getInfoFromUserServerPart(email);
    JSONObject jsonObject = new JSONObject();

    try {
        RestTemplate restTemplate = new RestTemplate();
        String userInfoUrl = "http://" + userServer + "/user/" + email + "/" + userServerCilent + "/" + userServerCilentPWD;
        HttpHeaders headers = new HttpHeaders();
        MediaType mediaType = MediaType.parseMediaType("application/json;charset=UTF-8");
        headers.setContentType(mediaType);
        headers.set("user-agent", "portal_backend");
        HttpEntity httpEntity = new HttpEntity(headers);
        ResponseEntity<JSONObject> response = restTemplate.exchange(userInfoUrl, HttpMethod.GET, httpEntity, JSONObject.class);
        JSONObject userInfo = response.getBody().getJSONObject("data");

        String avatar = userInfo.getString("avatar");
        if(avatar!=null){
            // avatar = "/userServer" + avatar;
            //修正avatar前面加了/userServer
            // avatar = avatar.replaceAll("/userServer","");
            genericService.formatUserAvatar(avatar);
        }
        userInfo.put("avatar",avatar);
        userInfo.put("msg","suc");
        return userInfo;
    }catch(Exception e){
        log.error(e.getMessage());
        // System.out.println(e.fillInStackTrace());
        jsonObject.put("msg","no user");
    }
    return jsonObject;
}

原因:

Spring AOP 注解为什么失效?

如下面几种场景

1、Controller直接调用Service B方法:Controller > Service A

在Service A 上加@Transactional的时候可以正常实现AOP功能。

2、==Controller调用Service A方法,A再调用B方法:Controller > Service A > Service B==

在Service B上加@Transactional的时候不能实现AOP功能,因为==在Service A方法中调用Service B方法想当于使用this.B(),this代表的是Service类本身,并不是真实的代理Service对象==,所以这种不能实现代理功能。

所以,如果不是直接调用的方式,是不能实现代理功能的,非常需要注意。