远离办公室的多人运动,看懂这篇Spring的IoC,AOP让你远离996
Posted 公众号-JavaEdge
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了远离办公室的多人运动,看懂这篇Spring的IoC,AOP让你远离996相关的知识,希望对你有一定的参考价值。
Spring最核心的就是IoC和AOP,它们的初衷都是解耦和扩展。
什么是 IoC?
一种设计思想,将设计好的对象交给Spring容器控制,而不是直接在对象内部控制。
为什么要让容器来管理对象呢?你这程序员咋就知道甩锅呢?
低级的码农,可能只是觉着使用IoC方便、就用来解耦的。这还远不是容器的益处。
以容器为依托来管理所有的框架、业务对象,我们可以做到:
- 无侵入调整对象的关系
- 无侵入地随时调整对象的属性
- 实现对象的替换
这使得框架开发者在后续实现一些扩展就很容易。
什么是AOP?
AOP体现了高内聚、低耦合,在切面集中实现横切关注点(缓存、权限、日志等),然后通过切点配置把代码注入合适位置。
比如,期望找到一个合适地方把奶油注入蛋胚,应该如何指导机器人的程序完成操作呢?
- 连接点(Join point)
就是方法执行 - 切点(Pointcut)
Spring AOP默认使用AspectJ查询表达式,通过在连接点运行查询表达式来匹配切点 - 增强(Advice)
也叫作通知,定义了切入切点后增强的方式,包括前、后、环绕等。Spring AOP中,把增强定义为拦截器 - 切面(Aspect)
切面=切点+增强
单例的Bean如何注入Prototype的Bean?
如果要为单例的Bean注入Prototype的Bean,绝不是仅仅修改Scope属性这么简单。由于单例的Bean在容器启动时就会完成一次性初始化。最简单的解决方案是,把Prototype的Bean设置为通过代理注入,也就是设置proxyMode属性为TARGET_CLASS。
比如有如下抽象类,可以认为LearnService是有状态的,如果LearnService是单例的话,那必然会OOM
实际开发时,很多人不假思索把LearnGo和LearnJava类加上了**@Service**,让它们成为了Bean,也没有考虑到父类其实有状态
@Service
@Slf4j
public class LearnJava extends LearnService {
@Override
public void learn() {
super.learn();
log.info("java");
}
}
@Service
@Slf4j
public class LearnGo extends LearnService {
@Override
public void learn() {
super.learn();
log.info("go");
}
}
因为很多人认为 @Service 意义就在于,能通过**@Autowired**让Spring自动注入对象,就比如可以直接使用注入的List获取到LearnJava和LearnGo,而没想过类的生命周期:
@Autowired
List<LearnService> learnServiceList;
@GetMapping("test")
public void test() {
log.info("====================");
learnServiceList.forEach(LearnService::learn);
}
当年开发父类的人将父类设计为有状态的,但并不关心子类是怎么使用父类的;而开发子类的同学,没多想就直接添加 @Service,让类成为Bean,通过 @Autowired注入这个服务。但这样设置后,有状态的父类就可能产生内存泄露或线程安全问题。
那怎么做才能避免问题产生呢?
在为类添加 @Service把类型交由容器管理前,首先考量类是否有状态,然后为Bean设置合适的Scope。比如该案例,就为我们的两个类添加 @Scope即可,设置了PROTOTYPE生命周期,即多例:
但这样还是会内存泄漏,说明修改是无效的。
从日志可以看到,第一次调用和第二次调用的时候,SayBye对象都是4c0bfe9e,SayHello也是一样的问题。从日志第7到10行还可以看到,第二次调用后List的元素个数变为了2,说明父类SayService维护的List在不断增长,不断调用必然出现OOM:
[17:12:45.798] [http-nio-30666-exec-1] [INFO ] [c.j.s.beansingletonandorder.LearnService:26 ] - I'm com.javaedge.springpart1.beansingletonandorder.LearnGo@4a5fe6a2 size:1
[17:12:45.798] [http-nio-30666-exec-1] [INFO ] [c.j.s.beansingletonandorder.LearnGo:20 ] - go
[17:12:45.839] [http-nio-30666-exec-1] [INFO ] [c.j.s.beansingletonandorder.LearnService:26 ] - I'm com.javaedge.springpart1.beansingletonandorder.LearnJava@6cb46b0 size:1
[17:12:45.840] [http-nio-30666-exec-1] [INFO ] [c.j.s.beansingletonandorder.LearnJava:17 ] - java
[17:12:57.380] [http-nio-30666-exec-2] [INFO ] [c.j.s.b.BeanSingletonAndOrderController:25 ] - ====================
[17:12:57.416] [http-nio-30666-exec-2] [INFO ] [c.j.s.beansingletonandorder.LearnService:26 ] - I'm com.javaedge.springpart1.beansingletonandorder.LearnGo@b859c00 size:2
[17:12:57.416] [http-nio-30666-exec-2] [INFO ] [c.j.s.beansingletonandorder.LearnGo:20 ] - go
[17:12:57.452] [http-nio-30666-exec-2] [INFO ] [c.j.s.beansingletonandorder.LearnService:26 ] - I'm com.javaedge.springpart1.beansingletonandorder.LearnJava@5426300 size:2
[17:12:57.452] [http-nio-30666-exec-2] [INFO ] [c.j.s.beansingletonandorder.LearnJava:17 ] - java
所以问题就是单例Bean如何注入Prototype Bean?
Controller标记了 @RestController
@RestController = @Controller + @ResponseBody,又因为 @Controller标记了 @Component元注解,所以 @RestController也是一个Spring Bean。
Bean默认是单例的,所以单例的Controller注入的Service也是一次性创建的,即使Service本身标识了prototype的范围,也不会起作用。
修复方案就是让Service以代理方式注入。这样虽然Controller是单例的,但每次都能从代理获取Service。这样一来,prototype范围的配置才能真正生效:
调试发现,注入的Service都是Spring生成的代理类:
如果不希望走代理,还有一种方案,每次直接从ApplicationContext中获取Bean:
这里Spring注入的LearnService的List,第一个元素是LearnGo,第二个元素是LearnJava。但我们更希望的是先执行Java再执行Go,所以注入一个List Bean时, 还要能控制Bean的顺序。
一般来说,顺序如何都无所谓,但对AOP,顺序可能会引发致命问题。
监控切面因为顺序问题导致Spring事务失效
通过AOP实现一个整合日志记录、异常处理和方法耗时打点为一体的统一切面。但后来发现,使用了AOP切面后,这个应用的声明式事务处理居然都是无效的。
现在分析AOP实现的监控组件和事务失效有什么关系,以及通过AOP实现监控组件是否还有其他坑。
先定义一个自定义注解Metrics,打上该注解的方法可以实现各种监控功能:
然后,实现一个切面完成Metrics注解提供的功能。这个切面可以实现标记了@RestController注解的Web控制器的自动切入,如果还需要对更多Bean进行切入的话,再自行标记@Metrics注解。
测试MetricsAspect的功能。
Service中实现创建用户的时候做了事务处理,当用户名包含test字样时会抛出异常,导致事务回滚。为Service中的createUser添加@Metrics注解。
还可以手动为类或方法添加@Metrics注解,实现Controller之外的其他组件的自动监控。
@Slf4j
@RestController //自动进行监控
@RequestMapping("metricstest")
public class MetricsController {
@Autowired
private UserService userService;
@GetMapping("transaction")
public int transaction(@RequestParam("name") String name) {
try {
userService.createUser(new UserEntity(name));
} catch (Exception ex) {
log.error("create user failed because {}", ex.getMessage());
}
return userService.getUserCount(name);
}
}
@Service
@Slf4j
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
@Metrics //启用方法监控
public void createUser(UserEntity entity) {
userRepository.save(entity);
if (entity.getName().contains("test"))
throw new RuntimeException("invalid username!");
}
public int getUserCount(String name) {
return userRepository.findByName(name).size();
}
}
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
List<UserEntity> findByName(String name);
}
使用用户名“test”测试一下注册功能,自行测试可以观察到日志中打出了整个调用的出入参、方法耗时:
但之后性能分析觉得默认的 @Metrics配置不太好,优化点:
- Controller的自动打点,不要自动记录入参和出参日志,避免日志量过大
- Service中的方法,最好可以自动捕获异常
优化调整:
-
MetricsController手动添加 @Metrics,设置logParameters和logReturn为false
-
Service中的createUser方法的@Metrics注解,设置了ignoreException属性为true
可是实际上线发现日志量并没有减少,而且事务回滚还失效了,从输出看到最后查询到了名为test的用户。
执行Service的createUser方法时,Spring 的 TransactionAspectSupport并没有捕获到异常,所以自然无法回滚事务。因为异常被MetricsAspect吞了。
切面本身是一个Bean,Spring对不同切面增强的执行顺序是由Bean优先级决定的,具体规则是:
- 入操作(Around(连接点执行前)、Before),切面优先级越高,越先执行
一个切面的入操作执行完,才轮到下一切面,所有切面入操作执行完,才开始执行连接点(方法) - 出操作(Around(连接点执行后)、After、AfterReturning、AfterThrowing)
切面优先级越低,越先执行。一个切面的出操作执行完,才轮到下一切面,直到返回到调用点。 - 同一切面的Around比After、Before先执行
对于Bean可以通过 @Order 设置优先级:默认情况下Bean的优先级为最低优先级,其值是Integer的最大值。值越大优先级越低。
增强的执行顺序
新建一个TestAspectWithOrder10切面,通过 @Order注解设置优先级为10,在内部定义@Before、@After、@Around三类增强,做简单的日志输出,切点是TestController所有方法;
然后再定义一个类似的TestAspectWithOrder20切面,设置优先级为20:
调用TestController的方法后,通过日志输出可以看到,增强执行顺序符合切面执行顺序的三个规则:
TestAspectWithOrder10 @Around before
TestAspectWithOrder10 @Before
TestAspectWithOrder20 @Around before
TestAspectWithOrder20 @Before
TestAspectWithOrder20 @Around after
TestAspectWithOrder20 @After
TestAspectWithOrder10 @Around after
TestAspectWithOrder10 @After
Spring的事务管理同样基于AOP,默认,优先级最低的,会先执行出操作,但自定义切面MetricsAspect默认情况下也是最低优先级,这时就会产生问题:若出操作先执行捕获了异常,则Spring事务就会因为无法catch异常而无法回滚。
所以要指定MetricsAspect的优先级,可设置为最高优先级,即最先执行入操作最后执行出操作:
切入的连接点是方法,注解定义在类上是无法直接从方法上获取到注解的。所以要改为优先从方法获取,若方法上获取不到再从类获取,若还是获取不到则使用默认注解:
Metrics metrics = signature.getMethod().getAnnotation(Metrics.class);
if (metrics == null) {
metrics = signature.getMethod().getDeclaringClass().getAnnotation(Metrics.class);
}
修正完后,事务就可以正常回滚了,并且Controller的监控日志也不再出现入参、出参。
监控平台如果想生产可用,需修改:
- 日志打点,改为对接Metrics监控系统
- 各种监控开关,从注解属性获取改为通过配置中心实时获取
以上是关于远离办公室的多人运动,看懂这篇Spring的IoC,AOP让你远离996的主要内容,如果未能解决你的问题,请参考以下文章
入门JavaScript?看懂这篇文章就够了!——Web前端系列学习笔记