Spring中IOC和AOP使用注意事项


IOC和AOP初衷是解耦和扩展

1. IOC是一种设计思想,使用Spring来实现IOC,是将你设计好的对象交给Spring容器控制,而不是在对象内部控制。
2. 使用IOC方便、可以实现解耦,并带来更多的可能性。
3. 如果以容器为依托管理所有的框架、业务对象,不仅可以无侵入地调整对象的关系,还可以无侵入地调整对象的熟悉,甚至是实现对象的替换。
4. 因此扩展不再是问题,带来无限可能性。比如监控的对象如果是Bean实现就会非常简单。
5. 所以,这套容器体系,不仅被Spring Core和Spring Boot大量依赖,还实现了一些外部框架和Spring的无缝整合。
6. AOP是松耦合、高内聚的精髓,在切面集中实现横切关注点(缓存、权限、日志等),然后通过切点配置把代码注入合适的地方。
7. 切面、切点、增强、连接点,是AOP中非常重要的概念。

Spring AOP中核心概念

理解:把Spring AOP计数看作为蛋糕做奶油夹层的工序。如果希望找到一个合适的地方把奶油注入蛋糕坯子中,如何指导完成操作?
1. 连接点(Join point)
//对于Spring AOP来说,连接点就是方法执行
//只能往蛋糕坯子里面加奶油,而不能上面或下面加奶油。
2. 切点(Pointcut)
//Spring AOP中默认使用ASpectJ查询表达式,通过在连接点运行查询表达式来匹配切入点
//在什么点切开蛋糕加奶油?可以在蛋糕坯子中间加一层奶油,在中间切一次;也可以中间加两层,1/3和2/3处切两次。
3. 增强(Advice)
//也叫通知,定义了切入切点后增强的方式,包括前、后、环绕等。Spring AOP中把增强定义为拦截器
//切开蛋糕后要做什么,也就是加入奶油。
4. 切面(Aspect)
//也叫做方面。切面=切点+增强
//找到蛋糕坯子中要加奶油的地方并加入奶油,为蛋糕做奶油夹层操作。

单例的Bean如何注入prototype的Bean

Spring创建的Bean默认是单例的,但当Bean遇到继承的时候,可能会忽略这一点。

一个由单例引起内存泄漏的案例

1. 定义了一个SayService抽象类,其中维护了一个类型ArrayList的字段data,用于保存方法处理的中间数据。
2. 每次调用say方法都会往data加入新数据,可以认为SayService是有状态,如果SayService是单例的话必然会OOM。
@Slf4j
public abstract class SayService {
    List 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());
    }
}
3. 在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");
    }
}
4. @Service注解,能通过@Autowired注解让Spring自动注入对象,比如可以直接使用注入的List获取到SayHello和SayBye,而没有想过类的生命周期:
@Autowired
List sayServiceList;

@GetMapping("test")
public void test() {
    log.info("====================");
    sayServiceList.forEach(SayService::say);
}
5. 这一点非常容易忽略。这样设置后,有状态的基类就可能产生内存泄漏或线程安全问题。
6. 正确方式是,在类标记上@Service注解把类型交由容器管理前,首先评估一下类是否有状态,然后为Bean设置合适的Scope。
7. 修改后,为SayHello和SayBye两个类都标记@Scope注解,设置了PROTOTYPE的生命周期,也就是多例:
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
8. 但上线后还是出现了内存泄漏,证明修改无效。
9. 从日志中可以看到第二次调用后List元素个数变为了2,说明父类SayService维护的List在不断增长,不断调用必然出现OOM。
10. 这就引出了单例的Bean如何注入Prototype的Bean问题。由源码可以知道@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 {}
11. Bean默认是单例的,所以单例的Controller注入的Service也是一次性创建的,即使Service本身标识了prototype的范围也没用。
12. 修复方式,让Service以代理方式注入。这样Controller本身是单例的,但每次都能从代理获取Service。
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
13. 调试发现,注入的Service都是Spring生产的代理类,如果不希望走代理,可以直接从ApplicationContext中获取Bean:
@Autowired
private ApplicationContext applicationContext;
@GetMapping("test2")
public void test2() {
applicationContext.getBeansOfType(SayService.class).values().forEach(SayService::say);
}
//另一个潜在问题,这里Spring注入的SayService的List,注入一个List Bean时,需要进一步考虑Bean的顺序或者说优先级。
//大多数情况顺序不那么重要,但对于AOP,顺序可能引发致命问题

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

实现横切关注点,是AOP常见的一个应用。
通过AOP实现了一个整合日志记录、异常处理和方法耗时打点为一体的统一切面。
使用AOP切面后,这个应用的声明式事务处理失效的案例
1. 自定义注解Metrics,打上该注解的方法可以实现各种监控功能:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Metrics {
    /**
     * 在方法成功执行后打点,记录方法的执行时间发送到指标系统,默认开启
     *
     * @return
     */
    boolean recordSuccessMetrics() default true;

    /**
     * 在方法成功失败后打点,记录方法的执行时间发送到指标系统,默认开启
     *
     * @return
     */
    boolean recordFailMetrics() default true;

    /**
     * 通过日志记录请求参数,默认开启
     *
     * @return
     */
    boolean logParameters() default true;

    /**
     * 通过日志记录方法返回值,默认开启
     *
     * @return
     */
    boolean logReturn() default true;

    /**
     * 出现异常后通过日志记录异常信息,默认开启
     *
     * @return
     */
    boolean logException() default true;

    /**
     * 出现异常后忽略异常返回默认值,默认关闭
     *
     * @return
     */
    boolean ignoreException() default false;
}
2. 实现一个切面完成Metrics注解提供的功能。这个切面可以实现标记了@RestController注解的Web控制器的自动切入。
//如果还需要对更多Bean进行切入的话,再自行标记@Metrics注解。
@Aspect
@Component
@Slf4j
public class MetricsAspect {
    //让Spring帮我们注入ObjectMapper,以方便通过JSON序列化来记录方法入参和出参
    
    @Autowired
    private ObjectMapper objectMapper;

    //实现一个返回Java基本类型默认值的工具。其实,你也可以逐一写很多if-else判断类型,然后手动设置其默认值。这里为了减少代码量用了一个小技巧,即通过初始化一个具有1个元素的数组,然后通过获取这个数组的值来获取基本类型默认值
    private static final Map, Object> DEFAULT_VALUES = Stream
            .of(boolean.class, byte.class, char.class, double.class, float.class, int.class, long.class, short.class)
            .collect(toMap(clazz -> (Class<?>) clazz, clazz -> Array.get(Array.newInstance(clazz, 1), 0)));
    public static  T getDefaultValue(Class clazz) {
        return (T) DEFAULT_VALUES.get(clazz);
    }

    //@annotation指示器实现对标记了Metrics注解的方法进行匹配
   @Pointcut("within(@org.geekbang.time.commonmistakes.springpart1.aopmetrics.Metrics *)")
    public void withMetricsAnnotation() {
    }

    //within指示器实现了匹配那些类型上标记了@RestController注解的方法
    @Pointcut("within(@org.springframework.web.bind.annotation.RestController *)")
    public void controllerBean() {
    }

    @Around("controllerBean() || withMetricsAnnotation())")
    public Object metrics(ProceedingJoinPoint pjp) throws Throwable {
        //通过连接点获取方法签名和方法上Metrics注解,并根据方法签名生成日志中要输出的方法定义描述
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Metrics metrics = signature.getMethod().getAnnotation(Metrics.class);
 
        String name = String.format("【%s】【%s】", signature.getDeclaringType().toString(), signature.toLongString());
        //因为需要默认对所有@RestController标记的Web控制器实现@Metrics注解的功能,在这种情况下方法上必然是没有@Metrics注解的,我们需要获取一个默认注解。虽然可以手动实例化一个@Metrics注解的实例出来,但为了节省代码行数,我们通过在一个内部类上定义@Metrics注解方式,然后通过反射获取注解的小技巧,来获得一个默认的@Metrics注解的实例
        if (metrics == null) {
            @Metrics
            final class c {}
            metrics = c.class.getAnnotation(Metrics.class);
        }
        //尝试从请求上下文(如果有的话)获得请求URL,以方便定位问题
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes != null) {
            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
            if (request != null)
                name += String.format("【%s】", request.getRequestURL().toString());
        }
        //实现的是入参的日志输出
        if (metrics.logParameters())
            log.info(String.format("【入参日志】调用 %s 的参数是:【%s】", name, objectMapper.writeValueAsString(pjp.getArgs())));
        //实现连接点方法的执行,以及成功失败的打点,出现异常的时候还会记录日志
        Object returnValue;
        Instant start = Instant.now();
        try {
            returnValue = pjp.proceed();
            if (metrics.recordSuccessMetrics())
                //在生产级代码中,我们应考虑使用类似Micrometer的指标框架,把打点信息记录到时间序列数据库中,实现通过图表来查看方法的调用次数和执行时间,在设计篇我们会重点介绍
                log.info(String.format("【成功打点】调用 %s 成功,耗时:%d ms", name, Duration.between(start, Instant.now()).toMillis()));
        } catch (Exception ex) {
            if (metrics.recordFailMetrics())
                log.info(String.format("【失败打点】调用 %s 失败,耗时:%d ms", name, Duration.between(start, Instant.now()).toMillis()));
            if (metrics.logException())
                log.error(String.format("【异常日志】调用 %s 出现异常!", name), ex);

            //忽略异常的时候,使用一开始定义的getDefaultValue方法,来获取基本类型的默认值
            if (metrics.ignoreException())
                returnValue = getDefaultValue(signature.getReturnType());
            else
                throw ex;
        }
        //实现了返回值的日志输出
        if (metrics.logReturn())
            log.info(String.format("【出参日志】调用 %s 的返回是:【%s】", name, returnValue));
        return returnValue;
    }
}
3. 分别定义最简单的Controller、Service和Repository测试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 {
    List findByName(String name);
}
4. 使用用户名“test”测试注册功能,日志中打出了整个调用的出入参、方法耗时
5. 对@Metrics配置进行的一次调整
//对于@Controller的自动打点,不要自动记录入参和出参日志,否则日志量太大
//对于Service中的方法,最好可以自动捕获异常。
@Metrics(logParameters = false, logReturn = false) //改动点1
public class MetricsController {

@Service
@Slf4j
public class UserService {
    @Transactional
    @Metrics(ignoreException = true) //改动点2
    public void createUser(UserEntity entity) {
    ...
6. 上线后发现日志量并没有减少,并且事务回滚失效
7. 分析Spring通过TransactionAspectSupport类实现事务。
//在invokeWithinTransaction方法中设置断点可以发现,在执行Service的createUser方法时,
//TransactionAspectSupport 并没有捕获到异常,所以自然无法回滚事务。
//原因就是,异常被 MetricsAspect 吃掉了
8. 切面本身是一个Bean,Spring对不同切面增强得执行顺序是由Bean优先级决定得,具体规则:
 8.1 入操作(Around(连接点执行前)、Before),切面优先级越高,越先执行。一个切面得入操作执行完,才轮到下一个切面,所有切面入操作执行完,才开始执行连接点(方法)。
 8.2 出操作(Around(连接点执行后)、After、AfterReturning、AfterThrowing),切面优先级越低,越先执行。一个切面得出操作执行完,才轮到下一切面,直接返回到调用点。
 8.3 同一切面的Around比After、Before先执行
9. 对于Bean可以通过@Order注解来设置优先级,查看@Order注解和Ordered接口源码可以发现,默认情况下Bean的优先级为最低优先级,其值是Integer最大值。值越大优先级越低。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface Order {

   int value() default Ordered.LOWEST_PRECEDENCE;

}
public interface Ordered {
   int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
   int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
   int getOrder();
}
10. 通过例子理解增强的执行顺序
@Aspect
@Component
@Order(10)
@Slf4j
public class TestAspectWithOrder10 {
    @Before("execution(* org.geekbang.time.commonmistakes.springpart1.aopmetrics.TestController.*(..))")
    public void before(JoinPoint joinPoint) throws Throwable {
        log.info("TestAspectWithOrder10 @Before");//10-② 20-④
    }
    @After("execution(* org.geekbang.time.commonmistakes.springpart1.aopmetrics.TestController.*(..))")
    public void after(JoinPoint joinPoint) throws Throwable {
        log.info("TestAspectWithOrder10 @After");//10-⑧ 20-⑥
    }
    @Around("execution(* org.geekbang.time.commonmistakes.springpart1.aopmetrics.TestController.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        log.info("TestAspectWithOrder10 @Around before");//10-① 20-③
        Object o = pjp.proceed();
        log.info("TestAspectWithOrder10 @Around after");//10-⑦ 20-⑤
        return o;
    }
}

@Aspect
@Component
@Order(20)
@Slf4j
public class TestAspectWithOrder20 {
  ...
}
11. 因为Spring的事务管理也是基于AOP的,默认情况下优先级最低会先执行出操作,但自定义切面MetricsAspect也同样是最低优先级,因此会出现问题。
//问题:如果出操作先执行捕获异常,那么Spring的事务处理就会因为无法捕获到异常导致无法回滚事务。
//解决:明确MetricsAspect的优先级,可以设置为最高优先级,也就是最先执行入操作最后执行出操作
//将MetricsAspect这个Bean的优先级设置为最高
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MetricsAspect {
    ...
}
//另外,要知道切入的连接点是方法,注解定义的类上是无法直接从方法上获取到注解的,修复方式是,改为优先从方法获取,如果获取不到再从类获取,获取不到再使用默认的注解
Metrics metrics = signature.getMethod().getAnnotation(Metrics.class);
if (metrics == null) {
    metrics = signature.getMethod().getDeclaringClass().getAnnotation(Metrics.class);
}
12. 总结:利用反射+注解+Spring AOP实现统一的横切日志关注点时,遇到的Spring事务失效问题,是由自定义切面执行顺序导致,因此当使用Ioc和AOP时,一定要考虑是否会影响其他内部组件,
因为Spring内部大量利用IOC和AOP实现了各种组件。

 @Autowired、@Inject、@Resource来注入Bean的区别

@Autowired
1、@Autowired是spring自带的注解,通过‘AutowiredAnnotationBeanPostProcessor’ 类实现的依赖注入;
2、@Autowired是根据类型进行自动装配的,如果需要按名称进行装配,则需要配合@Qualifier;
3、@Autowired有个属性为required,可以配置为false,如果配置为false之后,当没有找到相应bean的时候,系统不会抛错;
4、@Autowired可以作用在变量、setter方法、构造函数上。

@Inject
1、@Inject是JSR330 (Dependency Injection for Java)中的规范,需要导入javax.inject.Inject;实现注入。
2、@Inject是根据类型进行自动装配的,如果需要按名称进行装配,则需要配合@Named;
3、@Inject可以作用在变量、setter方法、构造函数上。

@Resource
1、@Resource是JSR250规范的实现,需要导入javax.annotation实现注入。
2、@Resource是根据名称进行自动装配的,一般会指定一个name属性
3、@Resource可以作用在变量、setter方法上。

总结:
1、@Autowired是spring自带的,@Inject是JSR330规范实现的,@Resource是JSR250规范实现的,需要导入不同的包
2、@Autowired、@Inject用法基本一样,不同的是@Autowired有一个request属性
3、@Autowired、@Inject是默认按照类型匹配的,@Resource是按照名称匹配的
4、@Autowired如果需要按照名称匹配需要和@Qualifier一起使用,@Inject和@Name一起使用

当Bean产生循环依赖时的解决方式

直观解决方法时通过set方法去处理,背后的原理其实是缓存。
主要解决方式:使用三级缓存
singletonObjects: 一级缓存, Cache of singleton objects: bean name --> bean instance
earlySingletonObjects: 二级缓存, Cache of early singleton objects: bean name --> bean instance 提前曝光的BEAN缓存
singletonFactories: 三级缓存, Cache of singleton factories: bean name --> ObjectFactory

相关