spring全集--第二章AOP

1. AOP

作用:让重复代码集中起来

1.1 AOP 入门

1) 日志问题

要对业务方法的执行时间进行日志记录

public interface PersonService {
    void save(Person person);

    void update(Person person);

    void delete(int id);

    List<Person> findAll();

    List<Person> findByPage(int page, int size);

    Person findById(int id);
}
@Service
public class PersonServiceImpl implements PersonService {
    private static final Logger log = LoggerFactory.getLogger(PersonServiceImpl.class);

    @Override
    public void save(Person person) {
        long start = System.nanoTime();
        log.info("save({})", person);
        long end = System.nanoTime();
        log.info("cost time: {} ns", (end - start));
    }

    @Override
    public void update(Person person) {
        long start = System.nanoTime();
        log.info("update({})", person);
        long end = System.nanoTime();
        log.info("cost time: {} ns", (end - start));
    }
	
	// ...
}

发现有很多重复代码,如果需求有变更,改动很大

) 解决思路

AOP 即 aspect oriented programming(面向切面编程),它的核心思想是将重复的逻辑剥离出来(称为通知),以达到增强扩展性的目的

目标

@Service
public class PersonServiceImpl implements PersonService {
    private static final Logger log = LoggerFactory.getLogger(PersonServiceImpl.class);

    @Override
    public void save(Person person) {
        log.info("save({})", person);
    }

    @Override
    public void update(Person person) {
        log.info("update({})", person);
    }
	
	// ...
}

通知-前增强

long start = System.nanoTime();

通知-后增强

long end = System.nanoTime();
log.info("cost time: {} ns", (end - start));

​重要概念

其中

  • 目标 - 英文 target,需要被增强的对象

  • 通知 - 英文 advice,那些重复的逻辑,可以理解为增强

  • 代理 - 英文 proxy,光有目标和通知还不够,需要代理来结合二者

  • 连接点 - 英文 joinpoint,在 Spring 中特指方法执行(暗含方法执行时的相关信息)

  • 切点 - 英文 pointcut,匹配连接点的条件,通知仅会在匹配切点的方法执行时被应用

  • 切面 - 英文 aspect,通知和切点合在一起,称为切面

代理类伪代码

public class PersonServiceProxy implements PersonService {
    @Override
    public void save(Person person) {
        // 执行通知方法及目标方法
    }

    @Override
    public void update(Person person) {
        // 执行通知方法及目标方法
    }
}

4) 日志问题解决

步骤

  1. 引入 aop 相关坐标 - 代码片段1

  2. 编写切面类 - 代码片段2

    1. 写切面

    2. 写通知

    3. 调目标

    4. 定切点

代码片段1

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

代码片段2

@Component // 写切面. 切面类也需要配合组件扫描被 Spring 管理起来
@Aspect // 写切面. 标注此类是一个切面类
public class Aspect1 {
    private static final Logger log = LoggerFactory.getLogger(Aspect1.class);
	// 写通知. 标注 @Around 注解的方法称为通知方法,内含增强逻辑
	// 定切点. 最后在 @Around 内书写切点表达式,确定哪些目标方法需要增强
    @Around("execution(* com.itheima.demo1.service.impl.PersonServiceImpl.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
		
		// 调目标. pjp.proceed() 即表示调用目标方法
        Object result = pjp.proceed();
		
        long end = System.nanoTime();
        log.info("cost time: {} ns", (end - start));
        return result;
    }
}

测试

@SpringBootTest
class Aop1Tests {

    @Autowired
    private PersonService personService;

    @Test
    public void save() {
        personService.save(new Person(3, "李四"));
    }

    @Test
    public void update() {
        personService.update(new Person(3, "李小四"));
    }

    @Test
    public void delete() {
        personService.delete(1);
    }

    @Test
    public void findById() {
        Person person = personService.findById(1);
    }

    @Test
    public void findAll() {
        List<Person> all = personService.findAll();
    }

    @Test
    public void findByPage() {
        List<Person> all = personService.findByPage(1, 5);
    }

}

部分运行结果

...
15:55:40  INFO PersonServiceImpl : findByPage(1, 5)
15:55:40  INFO Aspect1           : cost time: 322800 ns
15:55:40  INFO PersonServiceImpl : save(Person{id=3, name='李四'})
15:55:40  INFO Aspect1           : cost time: 370600 ns

1.2 AOP 进阶

1) 通知顺序

当有多个切面的切点都匹配目标时,多个通知方法都会被执行。之前介绍的 pjp.proceed() 在有多个通知方法匹配时,更准确的描述应该是这样的:

  • 如果还有下一个通知,则调用下一个通知

  • 如果没有下一个通知,则调用目标

那么它们的执行顺序是怎样的呢?

  • 默认按照 bean 的名称字母排序

  • @Order(数字) 加在切面类上来控制顺序

    • 目标前的通知方法:数字小先执行

    • 目标后的通知方法:数字小后执行

下面是三个切面类,都使用了环绕通知 切面类1

@Aspect
@Component
@Order(3)
public class Aspect21 {
    private static final Logger log = LoggerFactory.getLogger(Aspect21.class);

    @Around("execution(* com.itheima.demo1.service.Service2.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        log.info("before...");
        Object result = pjp.proceed();
        log.info("after...");
        return result;
    }
}

切面类2

@Aspect
@Component
@Order(2)
public class Aspect22 {
    private static final Logger log = LoggerFactory.getLogger(Aspect22.class);

    @Around("execution(* com.itheima.demo1.service.Service2.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        log.info("before...");
        Object result = pjp.proceed();
        log.info("after...");
        return result;
    }
}

切面类3

@Aspect
@Component
@Order(1)
public class Aspect23 {
    private static final Logger log = LoggerFactory.getLogger(Aspect23.class);

    @Around("execution(* com.itheima.demo1.service.Service2.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        log.info("before...");
        Object result = pjp.proceed();
        log.info("after...");
        return result;
    }
}

目标类

@Service
public class Service2Impl implements Service2 {
    private static final Logger log = LoggerFactory.getLogger(Service2Impl.class);

    @Override
    public void save(Person person) {
        log.info("save({})", person);
    }
    // ...
}

测试

@SpringBootTest
class Aop2Tests {

    @Autowired
    private Service2 service2;

    @Test
    public void save() {
        service2.save(new Person(3, "李四"));
    }

}

运行结果

16:02:35  INFO Aspect23          : before...
16:02:35  INFO Aspect22          : before...
16:02:35  INFO Aspect21          : before...
16:02:35  INFO Service2Impl      : save(Person{id=3, name='李四'})
16:02:35  INFO Aspect21          : after...
16:02:35  INFO Aspect22          : after...
16:02:35  INFO Aspect23          : after...

2) 通知类型

之前介绍的是环绕通知,它是功能最为强大的通知,但除此以外还有四种通知类型

  • @Before - 此注解标注的通知方法在目标方法前被执行

  • @After - 此注解标注的通知方法在目标方法后被执行,无论是否有异常

  • @AfterReturning - 此注解标注的通知方法在目标方法后被执行,有异常不会执行

  • @AfterThrowing - 此注解标注的通知方法发生异常后执行

它们与 @Around 有一个区别是,它们不用考虑目标方法的执行,而 @Around 需要自己调用 ProceedingJoinPoint.proceed() 来让目标方法执行

@Before 切面类

@Aspect
@Component
public class Aspect31 {
    private static final Logger log = LoggerFactory.getLogger(Aspect31.class);

    @Before("execution(* com.itheima.demo1.service.Service3.*(..))")
    public void before() {
        log.info("before...");
    }
}

@After 切面类

@Aspect
@Component
public class Aspect32 {
    private static final Logger log = LoggerFactory.getLogger(Aspect32.class);

    @After("execution(* com.itheima.demo1.service.Service3.*(..))")
    public void after() {
        log.info("after...");
    }
}

@AfterReturning 切面类

@Aspect
@Component
public class Aspect33 {
    private static final Logger log = LoggerFactory.getLogger(Aspect33.class);

    @AfterReturning("execution(* com.itheima.demo1.service.Service3.*(..))")
    public void afterReturning() {
        log.info("after returning...");
    }
}

@AfterThrowing 切面类

@Aspect
@Component
public class Aspect34 {
    private static final Logger log = LoggerFactory.getLogger(Aspect34.class);

    @AfterThrowing("execution(* com.itheima.demo1.service.Service3.*(..))")
    public void afterThrowing() {
        log.info("after throwing...");
    }
}

目标方法执行没有异常时,以下三个通知生效

@Before、@After、@AfterReturning

16:12:57  INFO Aspect31          : before...
16:12:57  INFO Service3Impl      : save(Person{id=3, name='李四'})
16:12:57  INFO Aspect33          : after returning...
16:12:57  INFO Aspect32          : after...

目标方法执行出现异常时,以下三个通知生效 @Before、@After、@AfterThrowing

16:15:37  INFO Aspect31          : before...
16:15:37  INFO Service3Impl      : save(Person{id=3, name='李四'})
16:15:37  INFO Aspect34          : after throwing...
16:15:37  INFO Aspect32          : after...

java.lang.ArithmeticException: / by zero
...

3) 连接点

连接点概念的原始定义不是特别好理解

A point during the execution of a program, such as the execution of a method or the handling of an exception

简单理解就是 目标方法,在Spring 中用 JoinPoint 抽象了连接点,用它可以获得方法执行时的相关信息,如方法名、方法参数类型、方法实际参数等等

  • 对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint

  • 对于其他四种通知,获取连接点信息只能使用 JoinPoint,它是 ProceedingJoinPoint 的父类型

那么如何获取这些信息呢?参考下面的代码

private void printMethodInfo(JoinPoint pjp) {
	MethodSignature signature = (MethodSignature) pjp.getSignature();
    Method method = signature.getMethod();
    log.info("连接点方法:{}", method);
    log.info("参数类型:{}", Arrays.toString(signature.getParameterTypes()));
    log.info("参数名:{}",
             Arrays.stream(method.getParameters())
             .map(Parameter::getName)
             .collect(Collectors.toList())
            );
    log.info("参数值:{}", Arrays.toString(pjp.getArgs()));
}

 

4) 切点表达式

切点表达式用来匹配【哪些】目标方法需要应用通知,常见的切点表达式如下

  • execution(返回值类型 包名.类名.方法名(参数类型))

    • * 可以通配任意返回值类型、包名、类名、方法名、或任意类型的一个参数

    • .. 可以通配任意层级的包、或任意类型、任意个数的参数

  • @annotation() 根据注解匹配

  • args() 根据方法参数匹配

测试代码均一样,仅粘贴一次

@SpringBootTest
class Aop5Tests {

    @Autowired
    private Service5 service5;

    @Test
    public void save() {
        service5.save(new Person(3, "李四"));
    }

    @Test
    public void update() {
        service5.update(new Person(3, "李小四"));
    }

    @Test
    public void delete() {
        service5.delete(1);
    }

    @Test
    public void findById() {
        Person person = service5.findById(1);
    }

    @Test
    public void findAll() {
        List<Person> all = service5.findAll();
    }

    @Test
    public void findByPage() {
        List<Person> all = service5.findByPage(1, 5);
    }

}

execution()

它主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为

execution(@注解? 访问修饰符? 返回值 包名.类名?.方法名(方法参数) throws 异常?)

 

其中带 ? 的表示可以省略的部分

  • 注解可省略(没啥用)

  • 访问修饰符可省略(没啥用,仅能匹配 public、protected、包级,private 不能增强)

  • 包名.类名可省略

  • thorws 异常可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

例1 .. 出现在包部分表示任意层级的包

@Aspect
@Component
public class Aspect5 {

    private static final Logger log = LoggerFactory.getLogger(Aspect5.class);

    // 例1 .. 出现在包部分表示任意层级的包
    @Pointcut("execution(* com.itheima.demo1..Service5Impl.*(..))")
    public void pt1() {
    }

    @Before("pt1()")
    public void before() {
        log.info("before...");
    }
}

结果(所有方法都被增强了)

16:37:49  INFO Aspect5           : before...
16:37:49  INFO Service5Impl      : delete(1)
16:37:49  INFO Aspect5           : before...
16:37:49  INFO Service5Impl      : findAll()
16:37:49  INFO Aspect5           : before...
16:37:49  INFO Service5Impl      : update(Person{id=3, name='李小四'})
16:37:49  INFO Aspect5           : before...
16:37:49  INFO Service5Impl      : findById(1)
16:37:49  INFO Aspect5           : before...
16:37:49  INFO Service5Impl      : findByPage(1, 5)
16:37:49  INFO Aspect5           : before...
16:37:49  INFO Service5Impl      : save(Person{id=3, name='李四'})

例2 切点加在接口上, 会匹配此接口的实现类

@Aspect
@Component
public class Aspect5 {

    private static final Logger log = LoggerFactory.getLogger(Aspect5.class);

    // 例2 切点加在接口上, 会匹配此接口的实现类
    @Pointcut("execution(* com.itheima.demo1.service.Service5.*(..))")
    public void pt2() {
    }
	
    @Before("pt2()")
    public void before() {
        log.info("before...");
    }
}

测试结果同 例1(所有方法都被增强了)

例3 * 可以通配方法名的一部分

@Aspect
@Component
public class Aspect5 {

    private static final Logger log = LoggerFactory.getLogger(Aspect5.class);

    // 例3 * 可以通配方法名的一部分
    @Pointcut("execution(* com.itheima.demo1.service.Service5.find*(..))")
    public void pt3() {
    }
	
    @Before("pt3()")
    public void before() {
        log.info("before...");
    }
}

结果(所有 find 开头的方法都被增强了)

16:44:41  INFO Service5Impl      : delete(1)
16:44:41  INFO Aspect5           : before...
16:44:41  INFO Service5Impl      : findAll()
16:44:41  INFO Service5Impl      : update(Person{id=3, name='李小四'})
16:44:41  INFO Aspect5           : before...
16:44:41  INFO Service5Impl      : findById(1)
16:44:41  INFO Aspect5           : before...
16:44:41  INFO Service5Impl      : findByPage(1, 5)
16:44:42  INFO Service5Impl      : save(Person{id=3, name='李四'})

例4 * 通配参数,是匹配【任意类型的一个参数】

@Aspect
@Component
public class Aspect5 {

    private static final Logger log = LoggerFactory.getLogger(Aspect5.class);

    // 例4 * 通配参数, 是匹配【任意类型的一个参数】
    @Pointcut("execution(* com.itheima.demo1.service.Service5.*(*))")
    public void pt4() {
    }
	
    @Before("pt4()")
    public void before() {
        log.info("before...");
    }
}

结果(save、update、delete 方法都被增强了,因为它们均有一个参数)

16:47:56  INFO Aspect5           : before...
16:47:56  INFO Service5Impl      : delete(1)
16:47:56  INFO Service5Impl      : findAll()
16:47:56  INFO Aspect5           : before...
16:47:56  INFO Service5Impl      : update(Person{id=3, name='李小四'})
16:47:56  INFO Aspect5           : before...
16:47:56  INFO Service5Impl      : findById(1)
16:47:56  INFO Service5Impl      : findByPage(1, 5)
16:47:56  INFO Aspect5           : before...
16:47:56  INFO Service5Impl      : save(Person{id=3, name='李四'})

@annotation

自定义 @Log 注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Log {

    String value() default "console";
}

目标类上也挑几个方法加上 @Log 注解

@Service
public class Service5Impl implements com.itheima.demo1.service.Service5 {
    private static final Logger log = LoggerFactory.getLogger(Service5Impl.class);

    @Log
    public void save(Person person) {
        log.info("save({})", person);
    }

    @Log("file")
    public void update(Person person) {
        log.info("update({})", person);
    }

    @Log("db")
    public void delete(int id) {
        log.info("delete({})", id);
    }

    public List<Person> findAll() {
        log.info("findAll()");
        List<Person> people = Arrays.asList(new Person(1, "王五"), new Person(2, "赵六"));
        return people;
    }

    public List<Person> findByPage(int page, int size) {
        log.info("findByPage({}, {})", page, size);
        List<Person> people = Arrays.asList(new Person(3, "王五"), new Person(4, "赵六"));
        return people;
    }

    public Person findById(int id) {
        log.info("findById({})", id);
        Person person = new Person(id, "张三");
        return person;
    }
}

例5,匹配有没有 @Log 注解

切面类

@Aspect
@Component
public class Aspect5 {

    private static final Logger log = LoggerFactory.getLogger(Aspect5.class);

    // 例5 匹配【注解信息】
    @Pointcut("@annotation(com.itheima.demo1.service.Log)")
    public void pt5() {
    }
    
    @Before("pt5()")
    public void before() {
        log.info("before...");
    }
}

结果(加了 @Log 注解的方法 save、update、delete 均被增强)

16:50:49  INFO Aspect5           : before...
16:50:49  INFO Service5Impl      : delete(1)
16:50:49  INFO Service5Impl      : findAll()
16:50:49  INFO Aspect5           : before...
16:50:49  INFO Service5Impl      : update(Person{id=3, name='李小四'})
16:50:49  INFO Service5Impl      : findById(1)
16:50:49  INFO Service5Impl      : findByPage(1, 5)
16:50:49  INFO Aspect5           : before...
16:50:49  INFO Service5Impl      : save(Person{id=3, name='李四'})

例6,匹配并捕获 @Log 注解

切面类

@Aspect
@Component
public class Aspect5 {

    private static final Logger log = LoggerFactory.getLogger(Aspect5.class);

    // 例6 匹配并捕获【注解信息】
    @Before("@annotation(logObj)")
    public void before(Log logObj) {
        log.info("before...{}", logObj.value());
    }
}

结果(首先,加了 @Log 注解的方法才会被增强;其次,在通知方法参数上获得了具体的 @Log 注解对象,可以获取它配置的值

16:55:29  INFO Aspect5           : before...db
16:55:29  INFO Service5Impl      : delete(1)
16:55:29  INFO Service5Impl      : findAll()
16:55:29  INFO Aspect5           : before...file
16:55:29  INFO Service5Impl      : update(Person{id=3, name='李小四'})
16:55:29  INFO Service5Impl      : findById(1)
16:55:29  INFO Service5Impl      : findByPage(1, 5)
16:55:29  INFO Aspect5           : before...console
16:55:29  INFO Service5Impl      : save(Person{id=3, name='李四'})

例7,匹配并捕获【注解信息】及【目标方法参数】

切面类

@Aspect
@Component
public class Aspect5 {

    private static final Logger log = LoggerFactory.getLogger(Aspect5.class);

    @Before("@annotation(logObj) && args(person)")
    public void before(Log logObj, Person person) {
        log.info("before...{}, {}", logObj, person);
    }
}

结果(类似的、在例2 的基础上又做了参数类型的匹配和捕获,只增强了带 Person 参数的 save 和 update 方法,delete 方法并未增强)

16:57:25  INFO Service5Impl      : delete(1)
16:57:25  INFO Service5Impl      : findAll()
16:57:25  INFO Aspect5           : before...@com.itheima.demo1.service.Log(value=file), Person{id=3, name='李小四'}
16:57:25  INFO Service5Impl      : update(Person{id=3, name='李小四'})
16:57:25  INFO Service5Impl      : findById(1)
16:57:25  INFO Service5Impl      : findByPage(1, 5)
16:57:25  INFO Aspect5           : before...@com.itheima.demo1.service.Log(value=console), Person{id=3, name='李四'}
16:57:25  INFO Service5Impl      : save(Person{id=3, name='李四'})

​5) 代理方式

Spring 支持两种代理方式

  • jdk 动态代理

    • 仅支持接口方式的代理

  • cglib 代理

    • 支持接口方式的代理,以及子类方式的代理

使用哪一种?

  • springboot 默认配置 spring.aop.proxy-target-class=true

    • 此时无论目标是否实现接口,都是采用【cglib 技术】,生成的都是子类代理

  • 如果设置了 spring.aop.proxy-target-class=false,那么又分两种情况

    • 如果【目标】实现了接口,Spring 会【jdk 动态代理技术】生成代理

    • 如果【目标】没有实现接口,Spring 会采用【cglib 技术】生成代理

切面类 Aspect1

@Component
@Aspect
public class Aspect6 {
    private static final Logger log = LoggerFactory.getLogger(Aspect6.class);

    @Before("execution(* foo(..))")
    public void before() {
        log.info("before");
    }
}

配置

spring.aop.proxy-target-class=false

 

例1,目标实现了接口,会采用 jdk 动态代理

接口 Service6

public interface Service6 {
    void foo();
}

接口 Service6 的实现类 Service6Impl  

@Service
public class Service6Impl implements Service6 {
    private static final Logger log = LoggerFactory.getLogger(Service6Impl.class);

    @Override
    public void foo() {
        log.info("foo()");
    }
}

测试

@SpringBootTest
class Aop6Tests {

    @Autowired
    private Service6 service6;

    @Test
    public void save() {
        service6.foo();
        System.out.println(service6.getClass());
        System.out.println(service6.getClass().getSuperclass());
    }
}

结果

18:19:49  INFO Aspect6           : before
18:19:49  INFO Service6Impl      : foo()
class com.sun.proxy.$Proxy55
class java.lang.reflect.Proxy

例2,目标实现接口,采用 cglib 代理的情况

配置修改为(或注释掉也可以),其它不变

spring.aop.proxy-target-class=true

 结果

18:23:39  INFO Aspect6           : before
18:23:39  INFO Service6Impl      : foo()
class com.itheima.demo1.service.impl.Service6Impl$$EnhancerBySpringCGLIB$$fe669311
class com.itheima.demo1.service.impl.Service6Impl

请大家记住代理类的名字特征,以后可以简单地从类名上就能判断出是哪种代理方式

如果目标没有实现接口,则无论如何配置,都采用 cglib 生成子类代理,这个请大家自行验证

6) 通知失效

例1,同一类内部,方法1 调用方法2,导致方法2 通知失效

@Service
public class Service7 {
    private static final Logger log = LoggerFactory.getLogger(Service7.class);

    public void m1() {
        log.info("m1");
        // 1. 调用本类方法, 通知失效!
        m2();
    }

    public void m2() {
        log.info("m2");
    }
}

切面类

@Component
@Aspect
public class Aspect7 {
    private static final Logger log = LoggerFactory.getLogger(Aspect7.class);

    @Before("execution(* com.itheima.demo1.service.Service7.*(..))")
    public void before() {
        log.info("before");
    }
}

测试

@SpringBootTest
class Aop7Tests {

    @Autowired
    private Service7 service7;

    @Test
    public void m1() {
        // 通过 m1 间接调用 m2, m1 的通知生效, 而 m2 的通知失效
        service7.m1();
    }

    @Test
    public void m2() {
        // 单独调用 m2 没有问题, 通知会生效
        service7.m2();
    }
}

m2 结果

18:30:46  INFO Aspect7           : before
18:30:46  INFO Service7          : m2

 m1 结果

18:33:24  INFO Aspect7           : before
18:33:24  INFO Service7          : m1
18:33:24  INFO Service7          : m2

从运行结果可以看出

  • 当我们直接调用 m2 方法时,m2 通知生效

  • 但是当我们通过 m1 方法间接调用 m2 方法时,m1 通知生效,但是 m2 通知失效

失效的原因是,通过 m1 调用 m2 时,并没有走代理对象调用 m2,没有机会进行通知增强

解决方法1

为引导类添加如下注解,开启 expose-proxy="true"

@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
public class Demo1Application {

    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class, args);
    }

}

 Service7

@Service
public class Service7 {
    private static final Logger log = LoggerFactory.getLogger(Service7.class);

    public void m1() {
        log.info("m1");
        ((Service7) AopContext.currentProxy()).m2();
    }

    public void m2() {
        log.info("m2");
    }
}

重新运行 m1 测试,结果为

18:37:35  INFO Aspect7           : before
18:37:35  INFO Service7          : m1
18:37:35  INFO Aspect7           : before
18:37:35  INFO Service7          : m2

解决方法2

注入代理对象来调用,不需要加注解 @EnableAspectJAutoProxy 和开启 expose-proxy="true"

Service7

@Service
public class Service7 {
    private static final Logger log = LoggerFactory.getLogger(Service7.class);

    @Autowired
    private Service7 proxy;

    public void m1() {
        log.info("m1");
        proxy.m2();
    }

    public void m2() {
        log.info("m2");
    }
}

重新运行 m1 测试,结果为

18:41:48  INFO Aspect7           : before
18:41:48  INFO Service7          : m1
18:41:48  INFO Aspect7           : before
18:41:48  INFO Service7          : m2

2. 配置进阶

2.1 Yaml

读音 [ˈjæməl]

Spring 除了支持 properties 的配置文件格式以外,还支持 yaml 格式的配置文件,文件名不变仍然固定为 application,后缀变成 yaml 或 yml

yaml 应用还是非常广泛的:

  • 第一种用途就像 properties,xml 可以作为程序的配置文件

  • 第二种用途跟 json 更像,可以作为数据的序列化方式

下面就来学习 yaml 的语法格式

1) 普通键值

name: zhangsan
age: 18

2) 对象

user:
  name: zhangsan
  age: 18 

 等价方式

user: { name: zhangsan, age: 18 }

3) 数组

user:
  name: zhangsan
  age: 18
  address:
    - 西安未央区
    - 西安雁塔区
    - 西安碑林区 

等价方式  

user:
  name: zhangsan
  age: 18
  address: [ 西安未央区,西安雁塔区,西安碑林区 ]

注意事项

  1. 冒号之后如果有值,那么冒号与值之间必须加空格(好在 idea 能语法高亮提示)

  2. 以空格的缩进表示层次关系,左对齐的数据是同一层级的

  3. 不能以 tab 的缩进表示层次关系(好在 idea 能将 tab 自动转空格)

  4. 注释与 properties 一样,使用 #

  5. 一些特殊字符如 &* 等有特殊含义,要用单引号或双引号引起来

  6. 数字不要前置 0

  7. @Value 对 properties 和 yaml 中的数组格式支持不好,可以给它用逗号分隔的单个值

5) properties 与 yaml 的对应关系

properties 中的 . 视为 yaml 中的对象的层级关系即可,例如:

spring.application.name=spring_case_boot_01

对应下面的 yaml 文档

spring:
  application:
    name: spring_case_boot_01 

它们都支持文档内的变量引用,例如有如下配置  

server.port=8080
app.host=localhost
app.url=http://${app.host}:${server.port}

app:
  host: localhost
  url: http://${app.host}:${server.port}
server:
  port: 8080

用下面的 java 代码,都可以输出 url 的实际值为 http://localhost:8080

@SpringBootApplication
@MapperScan
public class SpringCaseBoot01Application {

    // ...

    @Bean
    public CommandLineRunner runner4(@Value("${app.url}") String url) {
        return (args) ->{
            System.out.println(url);
        };
    }

}

本章扩展

AOP 底层实现

  • Spring 中的 AOP 底层就是用了课堂上讲到的代理方式,结合通知和目标,提供增强功能

  • 除此以外,还有一种技术叫做 aspectj,它的思路与代理方式有所不同:

    • 代理是运行时生成新的字节码

    • 而 aspectj 是在编译 class 类文件时,就把通知的增强功能,织入到目标类的字节码中

    • aspectj 还提供了在加载目标类时,修改目标类的字节码,织入增强功能

简单比较的话:

  • aspectj 在编译和加载时,修改目标字节码,性能较高

  • aspectj 因为不用代理,能突破一些技术上的限制,例如对构造、对静态方法也能增强

  • 但 aspectj 侵入性较强,且需要学习新的 aspectj 特有语法,因此实际使用的人很少

两种代理方式

jdk 动态代理

示例代码

public class JdkProxyDemo {

    interface Foo {
        void foo();
    }

    static class Target implements Foo {
        public void foo() {
            System.out.println("target foo");
        }
    }

    public static void main(String[] param) {
        // 目标对象
        Target target = new Target();
        // 代理对象
        Foo proxy = (Foo) Proxy.newProxyInstance(
                Target.class.getClassLoader(), new Class[]{Foo.class},
                (p, method, args) -> {
                    System.out.println("proxy before...");
                    Object result = method.invoke(target, args);
                    System.out.println("proxy after...");
                    return result;
                });
        // 调用代理
        proxy.foo();
    }
}

运行结果

proxy before...
target foo
proxy after...

 

Cglib 代理

public class CglibProxyDemo {

    static class Target {
        public void foo() {
            System.out.println("target foo");
        }
    }

    public static void main(String[] param) {
        // 目标对象
        Target target = new Target();
        // 代理对象
        Target proxy = (Target) Enhancer.create(Target.class, 
                (MethodInterceptor) (p, method, args, methodProxy) -> {
            System.out.println("proxy before...");
            Object result = methodProxy.invoke(target, args);
            // 另一种调用方法,不需要目标对象实例
//            Object result = methodProxy.invokeSuper(p, args);
            System.out.println("proxy after...");
            return result;
        });
        // 调用代理
        proxy.foo();
    }
}

运行结果与 jdk 动态代理相同

  • 因为要考虑通知、切点等,AOP 的实际实现要远远比上述代码复杂,但可以从上述代码看出 AOP 的基本思想

  • 关于这两种代理内部字节码如何生成等更底层的知识,敬请期待我后续的视频

AOP 自动配置

Spring Boot 是利用了自动配置类来简化了 aop 相关配置

  • AOP 自动配置类为 org.springframework.boot.autoconfigure.aop.AopAutoConfiguration

  • 可以通过 spring.aop.auto=false 禁用 aop 自动配置

  • AOP 自动配置的本质是通过 @EnableAspectJAutoProxy 来开启了自动代理,如果在引导类上自己添加了 @EnableAspectJAutoProxy 那么以自己添加的为准

  • @EnableAspectJAutoProxy 的本质是向容器中添加了 AnnotationAwareAspectJAutoProxyCreator 这个 bean 后处理器,它能够找到容器中所有切面,并为匹配切点的目标类创建代理,创建代理的工作一般是在 bean 的初始化阶段完成的

持续更新中,关注少年柯铭不迷路....

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值