《开发实战》14 | Spring框架:IoC和AOP是扩展的核心

14 | Spring框架:IoC和AOP是扩展的核心

IOC、AOP

IoC,其实就是一种设计思想,为什么要让容器来管理对象呢?或许你能想到的是,使用 IoC 方便、可以实现解耦。但在我看来,相比于这两个原因,更重要的是 IoC 带来了更多的可能性

如果以容器为依托来管理所有的框架、业务对象,我们不仅可以无侵入地调整对象的关系,还可以无侵入地随时调整对象的属性,甚至是实现对象的替换。这就使得框架开发者在程序背后实现一些扩展不再是问题,带来的可能性是无限的。比如我们要监控的对象如果是Bean,实现就会非常简单。所以,这套容器体系,不仅被 Spring Core 和 Spring Boot 大量依赖,还实现了一些外部框架和 Spring 的无缝整合。

AOP,体现了松耦合、高内聚的精髓,在切面集中实现横切关注点(缓存、权限、日志等),然后通过切点配置把代码注入合适的地方。切面、切点、增强、连接点,是 AOP 中非常重要的概念。

单例的 Bean 如何注入 Prototype 的 Bean?

一开始定义了这么一个 SayService 抽象类,其中维护了一个类型是 ArrayList 的字段 data,用于保存方法处理的中间数据。每次调用 say 方法都会往 data 加入新数据,可以认为 SayService 是有状态,如果 SayService 是单例的话必然会 OOM:

复制代码
@Slf4j
public abstract class SayService {
    List<String> data = new ArrayList<>();

    public void say() {
        data.add(IntStream.rangeClosed(1, 1000000)
                .mapToObj(__ -> "a")
                .collect(Collectors.joining("")) + UUID.randomUUID().toString());
        log.info("I'm {} size:{}", this, data.size());
    }
}

开发同学没有过多思考就把 SayHello 和 SayBye 类加上了 @Service注解,让它们成为了 Bean,也没有考虑到父类是有状态的:

复制代码
@Service
@Slf4j
public class SayHello extends SayService {
    @Override
    public void say() {
        super.say();
        log.info("hello");
    }
}

@Service
@Slf4j
public class SayBye extends SayService {
    @Override
    public void say() {
        super.say();
        log.info("bye");
    }
}

@Autowired
List<SayService> sayServiceList;

@GetMapping("test")
public void test() {
    log.info("====================");
    sayServiceList.forEach(SayService::say);
}

许多开发同学认为,@Service 注解的意义在于,能通过 @Autowired 注解让 Spring 自动注入对象,就比如可以直接使用注入的 List获取到 SayHello 和 SayBye,而没想过类的生命周期,这一个点非常容易忽略。开发基类的架构师将基类设计为有状态的,但并不知道子类是怎么使用基类的;而开发子类的同学,没多想就直接标记了 @Service,让类成为了 Bean,通过 @Autowired 注解来注入这个服务。但这样设置后,有状态的基类就可能产生内存泄露或线程安全问题。

正确的方式是,在为类标记上 @Service 注解把类型交由容器管理前,首先评估一下类是否有状态,然后为 Bean 设置合适的 Scope 。设置成多例
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
但上线后还是出现了内存泄漏,证明修改是无效的

这就引出了单例的 Bean 如何注入 Prototype 的 Bean 这个问题。Controller 标记了@RestController 注解,而 @RestController 注解 =@Controller 注解+@ResponseBody 注解,又因为 @Controller 标记了 @Component 元注解,所以@RestController 注解其实也是一个 Spring Bean:

复制代码
//@RestController注解=@Controller注解+@ResponseBody注解@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {}

//@Controller又标记了@Component元注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {}

Bean 默认是单例的,所以单例的 Controller 注入的 Service 也是一次性创建的,即使Service 本身标识了 prototype 的范围也没用

修复方式是,让 Service 以代理方式注入。这样虽然 Controller 本身是单例的,但每次都能从代理获取 Service。这样一来,prototype 范围的配置才能真正生效:

@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)

如果不希望走代理的话还有一种方式是,每次直接从 ApplicationContext 中获取Bean:

复制代码
@Autowired
private ApplicationContext applicationContext;
@GetMapping("test2")
public void test2() {
  applicationContext.getBeansOfType(SayService.class).values().forEach(SayService::say);
}

发现另一个潜在的问题。这里 Spring 注入的 SayService 的 List,第一个元素是 SayBye,第二个元素是 SayHello。但,我们更希望的是先执行 Hello 再执行 Bye,所以注入一个 List Bean 时,需要进一步考虑 Bean 的顺序或者说优先级。

大多数情况下顺序并不是那么重要,但对于 AOP,顺序可能会引发致命问题。

监控切面因为顺序问题导致 Spring 事务失效

看过一个不错的 AOP 实践,通过AOP 实现了一个整合日志记录、异常处理和方法耗时打点为一体的统一切面。但后来发现,使用了 AOP 切面后,这个应用的声明式事务处理居然都是无效的。

相关推荐
工业甲酰苯胺20 小时前
实现 json path 来评估函数式解析器的损耗
java·前端·json
老前端的功夫20 小时前
Web应用的永生之术:PWA落地与实践深度指南
java·开发语言·前端·javascript·css·node.js
@forever@20 小时前
【JAVA】LinkedList与链表
java·python·链表
程序员爱钓鱼21 小时前
Python编程实战:面向对象与进阶语法——类型注解与代码规范(PEP 8)
后端·python·ipython
程序员爱钓鱼21 小时前
Python实战:用高德地图API批量获取地址所属街道并写回Excel
后端·python·ipython
LilySesy21 小时前
ABAP+WHERE字段长度不一致报错解决
java·前端·javascript·bug·sap·abap·alv
六件套是我21 小时前
redission实现延时队列
android·java·servlet
Yeats_Liao21 小时前
时序数据库系列(三):InfluxDB数据写入Line Protocol详解
数据库·后端·时序数据库
王元_SmallA21 小时前
Redis Desktop Manager(Redis可视化工具)安装
java·后端
ᐇ95921 小时前
Java HashMap深度解析:数据结构、原理与实战指南
java·开发语言·数据结构