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) 日志问题解决
步骤
-
引入 aop 相关坐标 - 代码片段1
-
编写切面类 - 代码片段2
-
写切面
-
写通知
-
调目标
-
定切点
-
代码片段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: [ 西安未央区,西安雁塔区,西安碑林区 ]
注意事项
-
冒号之后如果有值,那么冒号与值之间必须加空格(好在 idea 能语法高亮提示)
-
以空格的缩进表示层次关系,左对齐的数据是同一层级的
-
不能以 tab 的缩进表示层次关系(好在 idea 能将 tab 自动转空格)
-
注释与 properties 一样,使用 #
-
一些特殊字符如
&
,*
等有特殊含义,要用单引号或双引号引起来 -
数字不要前置 0
-
@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 的初始化阶段完成的
持续更新中,关注少年柯铭不迷路....