该文章为ai润色,原文笔记在最后。该文章仅为个人记录所写。
1. 一次创建草稿的请求怎么走
后端启动后,日志里能看到:
Tomcat started on port 8080 (http) with context path '/'
所以接口地址是http://localhost:8080/api/item。客户端发送 POST 请求,请求体里放标题、内容和用户 ID:
{ "title": "第一条练习草稿", "content": "练习", "ownerId": 1 }按我现在的理解,这个过程可以先这样记:
客户端发出 HTTP 请求 ↓ Tomcat 接收请求,经过过滤器链 ↓ Spring MVC 的 DispatcherServlet 找到对应的处理方法 ↓ 已配置的拦截器进行前置处理 ↓ 读取 JSON 请求体,转换成 CreateItemRequest ↓ DemoController.createItem() ↓ ItemServiceImpl.createItem() ↓ 检查数据,创建 ServiceItem,保存到 Map ↓ 返回 ServiceItem,转换为 JSON ↓ 客户端收到 HTTP 响应
刚开始我写成“JSON 先进入 Controller,然后由@RequestBody转换”。更准确一点,是 Spring MVC 根据@RequestBody读取请求体,通过消息转换器完成转换,再把CreateItemRequest传给 Controller 方法。
Controller 接着调用itemService.createItem(request)。这个itemService是 Spring 注入的,不需要在 Controller 里自己new ItemServiceImpl()。
Service 里的流程比较容易看懂:检查标题和内容是否为空,创建ServiceItem,放入标题、内容、ownerId,设置DRAFT状态和创建时间,再生成 ID 并保存。
我把保存数据的工具类命名为simpleDatabase,但它里面其实是一个静态HashMap,没有连接数据库。新增和查询分别是:
items.put(item.getId(), item); items.get(id);
ID 也是在 Java 里通过++id生成的。以后换成真实数据库,主键可能用数据库自增、序列或 UUID,不能笼统地说“主键都是数据库自动自增”。
查询则从DemoController.selectById()进入ItemServiceImpl.selectById(),最后调用simpleDatabase.select(id),从 Map 里取出数据。
2. 顺便弄清 Spring Boot 的几个基础概念
启动类上的注解
启动类上常见的@SpringBootApplication是一个组合注解,主要包含这三个职责:
| 注解 | 用来做什么 |
|---|---|
@SpringBootConfiguration | 表明这是一个 Spring Boot 配置类 |
@EnableAutoConfiguration | 根据依赖、配置和已有 Bean 等条件进行自动配置 |
@ComponentScan | 默认扫描启动类所在包及其子包,寻找组件 |
我先记住它们各自负责什么。尤其是“自动配置”有条件,并不是加一个注解就把所有功能都开启了。
配置文件和默认值
application.properties使用key=value,YAML 使用缩进表达层级。例如下面两段配置表达的是同一件事:
spring.application.name=${APP_NAME:unnamed}spring: application: name: ${APP_NAME:unnamed}${APP_NAME:unnamed}表示读取APP_NAME,没有时用unnamed。这里读取的是 Spring 环境中的配置值,来源可以包括系统环境变量和命令行参数。
读教程时,我还遇到了“缺省配置”这个词。其实“缺省”就是默认:某个配置项有默认值时,省略配置就按默认值来。主动设置后通常会覆盖默认值,具体还要看配置优先级。如果必要的配置没有默认值,省略后仍然可能报错。
Logback 是什么
Logback 是日志实现,用来记录启动信息、业务过程和异常等内容。我们在代码里通过 SLF4J 的接口写日志,Logback 可以负责实际输出。
Spring Boot 常用 starter 默认带有 Logback,所以没有自己写日志配置文件,控制台也能看到日志。如果要输出到文件、修改格式或调整日志级别,再进行配置。教程里的defaults.xml则提供了一些预设日志配置。
Maven 会不会自动挑一个能用的版本
我最初以为,额外依赖可以让 Maven 自动选版本;如果某个版本不可用,它还会换一个可用的。后来发现这个理解不对。
我的项目通过pom.xml声明依赖。Boot 父项目或 BOM 已经管理版本的依赖,通常可以省略单独的版本号;没有被管理的依赖,需要确定版本。dependencyManagement是管理规则,并不等于已经引入这个库。
多个依赖间接引入同一个库的不同版本时,Maven 通常先看依赖路径的距离:离当前项目更近的优先,距离相同才看声明顺序,依赖管理也会影响结果。如果选中的版本无法解析,构建通常会报错,不会自动换成任意其他版本。
3. AOP 为什么能把日志单独拿出来
假如创建、查询、修改这些业务都要打印日志,最直接的写法就是在每个方法里加日志。以后改日志格式,就需要挨个改。
AOP,也就是面向切面,可以把这些横跨多个方法的处理集中起来。先用切点选出要处理的方法,再用通知决定什么时候做什么。日志、权限检查、事务都是常见的例子。
学到这里,我又顺着问了“耦合”和“内聚”。
耦合可以理解成代码模块之间依赖的程度。依赖本身很正常,但如果一个模块知道另一个模块太多内部细节,一点变化就影响很多地方,修改就会比较困难。
内聚则是一个模块内部的内容是否围绕同一个职责。业务方法集中处理业务,日志模块集中处理日志,都可以有比较好的内聚。
所以,把合适的日志处理提取成切面,可以让业务方法更集中,也方便统一修改日志。不过,重复的业务代码不一定都适合做成 AOP,还是要看它承担的职责。
4. 我对动态代理的误解
这是今天绕得最久的地方。
我原来以为:
每个类都先生成一个动态代理 ↓ 需要 AOP 时,代理再进入 AOP 接受增强 ↓ 使用时先调用 AOP,再调用代理对象
现在我这样理解 Spring AOP:
Spring 判断某个 Bean 需要代理 ↓ 为它创建代理,组织目标对象和通知 ↓ 调用者拿到代理引用 ↓ 调用方法时执行匹配的通知,再调用目标方法
这里的增强不是把日志代码复制进原类源码。代理负责把通知与目标调用连接起来,通知在方法被调用时执行。
代理也是针对对象创建的。一个 Bean 已经有代理,调用它另一个不匹配切点的方法,仍然可能经过同一个代理,只是不执行这组通知。动态代理还可以用于 AOP 以外的场景。
JDK 动态代理和 CGLIB
| 方式 | 与目标类的关系 | 容易记错的地方 |
|---|---|---|
| JDK 动态代理 | 生成实现指定接口的代理类,不继承目标实现类 | 不会复制目标类的所有方法和字段,也不会复制私有方法 |
| CGLIB | 生成目标类的子类,通过可重写的方法拦截调用 | final类不能继承,final、private方法不能按这种方式增强 |
在日志里,我看到了这两个类名:
目标对象:com.example.demo.service.serviceImpl.ItemServiceImpl 代理对象:com.example.demo.service.serviceImpl.ItemServiceImpl$$SpringCGLIB$$0
Spring Boot 4.0 的 AOP 自动配置默认使用 CGLIB,但不能把它说成所有 Spring 环境都统一默认 CGLIB。Spring AOP 基于代理,目标对象内部的this.方法()调用通常会绕过代理,这也是以后排查切面为什么没执行时要注意的地方。
5. 给创建草稿加一个日志切面
我先写了一个LogAspect,在创建草稿前打印日志:
@Slf4j @Aspect @Component public class LogAspect { @Before("execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.createItem(..))") public void beforeCreate() { log.info("准备创建草稿"); } }@Aspect声明切面,@Component让它在组件扫描时成为 Spring Bean,@Slf4j提供日志字段。项目还需要相应的 AOP 依赖和支持。
发送创建请求后,控制台出现了“准备创建草稿”,接口也照常返回。
切点表达式怎么读
第一次看到这一长串,我基本看不懂:
execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.createItem(..))
拆开就清楚一些了:
execution:匹配方法执行。最前面的
*:任意返回类型。com.example.demo.service.serviceImpl:包名。ItemServiceImpl:类名。createItem:方法名。(..):零个或多个任意类型参数。
调整匹配范围时,可以这样写:
# 匹配 ItemServiceImpl 的方法 execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.*(..)) # 匹配 service 包及其子包中的方法 execution(* com.example.demo.service..*.*(..))
参数位置里,()表示无参数,(*)表示恰好一个任意类型参数,(..)表示零个或多个参数。这些表达式选出的范围,还要受 Spring AOP 代理能力的限制。
几个术语放回代码里理解
| 术语 | 我的简单理解 | 对应例子 |
|---|---|---|
| Aspect,切面 | 集中放一类额外处理 | LogAspect |
| Join Point,连接点 | 可以被增强的执行位置,Spring AOP 中是方法执行 | 一次createItem()执行 |
| Pointcut,切点 | 筛选这些执行位置的规则 | execution(...) |
| Advice,通知或增强 | 选中之后具体做什么 | 创建前打印日志 |
| Target Object,目标对象 | 真正处理原业务的对象 | ItemServiceImpl |
| AOP Proxy,代理对象 | 接收调用、执行通知并调用目标的对象 | 日志里带SpringCGLIB的对象 |
| Weaving,织入 | 把切面和原调用联系起来的过程 | Spring AOP 通过代理组织调用 |
| Interceptor,拦截器 | 在调用前后加入处理的一种实现 | 代理里的拦截调用链,与 MVC 请求拦截器不是同一个概念 |
| Introduction,引介 | 让代理额外实现某个接口 | 原类源码不用增加该接口 |
6.@Around让我踩了一个返回值的坑
常见通知有这几种:
| 通知 | 执行时机 |
|---|---|
@Before | 方法执行前 |
@AfterReturning | 方法正常返回后 |
@AfterThrowing | 方法抛出异常后 |
@After | 方法结束时,正常返回或异常都会触发 |
@Around | 围绕调用,可以决定是否继续、最终返回什么 |
我开始用@Around时,预期很简单:开始时打印一条日志,执行原来的创建方法,结束时再打印一条日志。
结果日志从around-Start一直打印到了around-End,POST 的响应体却是空的。状态码为 200,响应头里写着:
Content-Length: 0
当时我还把它描述成“没有返回 HTTP 连接”。仔细看才发现,HTTP 响应已经回来了,只是没有响应体。GET 也还能查到已有草稿,于是我一直在找为什么 POST 没有把对象返回。
问题出在环绕通知:我调用了proceed(),却没有把它的结果返回,而且通知方法写成了void。
改成下面的处理后,响应恢复正常:
@Around("execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.createItem(..))") public Object aroundCreate(ProceedingJoinPoint joinPoint) throws Throwable { log.info("around-Start"); Object result = joinPoint.proceed(); log.info("around-End"); return result; }所以,继续调用和返回调用结果是两件事。proceed()让调用继续,return result才把结果交回调用者。
为什么 Before 和 After 不用这样返回
@Before、@After是框架在调用前后安排的附加动作,它们的返回值不负责传递目标方法的结果。
@Around则包住了这次调用,代理调用最终拿到的返回值来自环绕通知。想保留原来的行为,就要把继续调用得到的结果返回。常用Object是为了适配不同返回类型;原方法为void时,得到的结果可以是null。
如果不调用proceed(),目标方法就不会继续执行,但不代表请求一定卡住。环绕通知也可以直接返回一个结果,只是它改变了原来的调用行为。
另外,我原来在@After里打印“创建草稿完成”。这个通知异常时也会执行,所以这句日志容易误导。要表达成功,应该考虑@AfterReturning;@After更适合表达“执行结束”。
7. JoinPoint 到底是不是原方法
我最开始把JoinPoint理解成原方法本身,后来又不明白:既然是原方法,怎么还能拿到代理对象的信息?
它其实是描述当前方法执行的上下文对象,可以从中取得不同的信息:
| 调用 | 可以得到什么 |
|---|---|
getSignature() | 方法签名信息,例如名称 |
getArgs() | 本次调用的参数 |
getTarget() | 目标对象 |
getThis() | Spring AOP 代理对象 |
ProceedingJoinPoint扩展了JoinPoint,用于环绕通知,额外提供proceed()。它会继续剩余通知链,最终调用目标方法。
对应到我的例子,getTarget().getClass().getName()打印原来的ItemServiceImpl,getThis().getClass().getName()打印带SpringCGLIB的代理类名。
8. 自定义注解:写一个标记,再让切面识别它
前面是按照包名、类名和方法名匹配。另一种常见方式,是在方法上加一个注解,再让切面匹配带有这个注解的方法。
这让我想到了事务:方法上写@Transactional,框架就会根据它做相应处理。自定义注解也可以提供这种标记方式,但注解本身不会自动执行处理逻辑。
我用的注解叫MetricTime:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MetricTime { String value(); }它可以放在方法上,例如:
@MetricTime("select") public ServiceItem selectById(Long id) { return simpleDatabase.select(id); }注解是什么
我现在把注解理解成贴在类、方法等位置上的结构化说明,也叫元数据。它可以携带一些额外信息,但要由编译器、工具或框架读取并处理,才会产生相应作用。
例如@MetricTime("select"),表示这个方法带有MetricTime标记,其中value是select。切面可以根据这个标记选中方法,也可以读取value,用它作为日志里的名称。
@Target:可以写在哪里
我原来把它理解成“作用域”,现在觉得说“允许标记的位置”更直观。
| 值 | 允许的位置 |
|---|---|
TYPE | 类、接口、枚举、record、注解类型等类型声明 |
METHOD | 方法声明 |
FIELD | 字段,包括枚举常量 |
PARAMETER | 方法或构造器的参数 |
CONSTRUCTOR | 构造器声明 |
LOCAL_VARIABLE | 局部变量声明 |
ANNOTATION_TYPE | 注解类型声明 |
PACKAGE | 包声明,通常写在package-info.java |
TYPE_PARAMETER | 泛型类型参数声明 |
TYPE_USE | 使用类型的位置,例如泛型实参 |
MODULE | 模块声明 |
RECORD_COMPONENT | record 的组件 |
允许多个位置时,可以写@Target({ElementType.METHOD, ElementType.TYPE})。
@Retention:保留到什么时候
它不负责决定“什么时候执行”,而是决定注解信息保留到哪个阶段。
| 值 | 含义 |
|---|---|
SOURCE | 只保留在源码,编译时丢弃,例如@Override |
CLASS | 写入 class 文件,运行时不能通过普通反射读取 |
RUNTIME | 写入 class 文件,运行时仍能通过反射读取 |
没有声明@Retention时,默认是CLASS。如果希望切面在运行时读取注解信息,这里就需要保留到RUNTIME。
value()是不是可选参数
不是。是否可选,要看有没有default:
public @interface MetricTime { String value(); String unit() default "ms"; }这个示例里的value没有默认值,使用时必填;unit有默认值,可以省略。
@MetricTime("select")是@MetricTime(value = "select")的简写。名字叫value只是让写法可以更简短,不会让它自动变成可选属性。
9.@annotation的两种写法
这里又卡了一下,因为表达式里看起来都是注解,却有两种写法。
按注解类型匹配
@Around("@annotation(com.example.demo.aspect.MetricTime)")它匹配带有@MetricTime的方法,通知方法不必额外声明MetricTime参数。如果想读取注解属性,可以从方法信息中获取注解:
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); MetricTime metricTime = method.getAnnotation(MetricTime.class); String name = metricTime.value();
我之前只写了MetricTime metricTime;,接着就调用metricTime.value(),IDE 提示变量可能尚未初始化。现在看原因很直接:声明变量不会让它自动变成方法上的注解实例,需要先取得这个实例。
这里的MethodSignature来自org.aspectj.lang.reflect,Method来自java.lang.reflect。还要注意,使用接口代理时得到的Method可能来自接口;注解写在实现方法上时,需要定位到目标方法,不能直接假设getAnnotation()一定能读到。
匹配并绑定注解
@Around("@annotation(metricTime)") public Object time(ProceedingJoinPoint joinPoint, MetricTime metricTime) throws Throwable { String name = metricTime.value(); return joinPoint.proceed(); }这个片段展示的是参数绑定:表达式中的metricTime对应通知方法的同名参数,Spring 会把匹配到的注解传进来,方法里就能直接读取它的属性。
两种写法都使用切点表达式。区别是,一种按类型匹配,另一种同时把注解绑定到参数。Spring 通知参数绑定
如果要做耗时统计,思路是在proceed()前记录开始时间,在finally中计算耗时,这样正常返回和异常结束都可以记录。计时之外,也要注意保留返回值和异常的行为。
原文内容如下(原文内容可能有错误,仅供参考):
AOP
面向切面,可以将在多个不同业务方法中但是又相同的部分(与业务逻辑本身关联不大但是又不可或缺的部分)单独提取出来,制作成切面,通过切点选出要处理的方法,再通过通知决定在方法执行前、执行后或执行前后做什么。这样业务方法可以更集中地表达自己的业务逻辑,共用处理也比较方便统一修改。例如安全检查、日志、事务等代码。
如图假如一个业务逻辑是从上到下的,但是一些与业务逻辑不直接相关的代码降低了内聚,但是有了切面,被切面覆盖的代码可以从业务逻辑中切出去,业务逻辑只需要关心具体业务,提高内聚,对于业务之外但是又不得不用的代码可以通过切面来配装,也可以通过修改切面代码来一键修改所有日志等实现,降低直接耦合
AOP原理
Spring AOP 可以通过代理对象,在符合切入点规则的方法调用前后加入额外处理。切入点负责选方法,代理负责在调用时把通知和原方法联系起来。Spring AOP默认使用CGLIB动态代理,也可以手动切换。
JDK动态代理:生成的是实现指定接口的代理类,不继承原来的实现类CGLIB动态代理:生成的是原类的子类,通过重写可重写的方法来进行代理,因此final、private等不能被重写的方法不能用这种方式增强
我的误解和改正:我之前一直以为每个类都会先默认创建一个动态代理。某个方法需要 AOP 时,代理对象再“进入 AOP”接受增强;使用时先调用 AOP,再由 AOP 调用代理对象。
但是实际上应该是:Spring AOP 不是一个存放代理对象的地方。Spring 判断某个 Bean 需要 AOP 处理后,才会为它准备代理对象;调用者实际调用的是代理对象。代理会在符合规则的方法被调用时执行相应的通知,再调用原对象的方法。这里的“增强”不是把通知代码复制进原类,也不是修改原类源码。代理对象承载了这套调用流程,通知是在方法被调用时才真正执行的。不是每个类都会因为 Spring AOP 而自动生成代理;动态代理也可以用于 AOP 以外的场景
装配AOP
Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点;
Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行;
Pointcut:切入点,即一组连接点的集合;
Advice:增强,指特定连接点上执行的动作;
Introduction:引介,指为一个已有的Java对象动态地增加新的接口;
Weaving:织入,指将切面整合到程序的执行流程中;
Interceptor:拦截器,是一种实现增强的方式;
Target Object:目标对象,即真正执行业务的核心逻辑对象;
AOP Proxy:AOP代理,是客户端持有的增强后的对象引用。
先在SpringBoot中引入AOP依赖
然后定义一个切面类LogAspect
@Slf4j @Aspect @Component public class LogAspect { // 方法执行前通知 @Before(("execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.createItem(..))")) public void beforeCreate(){ log.info("准备创建草稿"); } // 方法执行后通知 @After(("execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.createItem(..))")) public void afterCreate(){ log.info("创建草稿完成"); } }其中先通过@Aspect注解声明这是一个AOP切面,然后通过@Component将其注册为Bean
在AOP中,有这几个通知注解
| 通知 | 何时执行 |
|---|---|
@Before | 方法执行前 |
@AfterReturning | 方法正常返回后 |
@AfterThrowing | 方法抛出异常后 |
@After | 方法结束时,无论正常还是异常 |
@Around | 围绕整个调用 |
切点表达式
而注解中需要添加切点表达式来规定AOP的范围,最常用的切点表达式如下:
execution(返回类型 包名.类名.方法名(参数))
execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.createItem(..))从右往左读依次为
creatItem方法
(..):允许任意个、任意类型的参数ItemServiceImpl类
com.example.demo.service.serviceImpl包
最前面的
*:允许返回任意类型
范围的变化:
| 表达式 | 匹配范围 |
|---|---|
execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.createItem(..)) | 这个类的createItem方法 |
execution(* com.example.demo.service.serviceImpl.ItemServiceImpl.*(..)) | 这个类的所有方法 |
execution(* com.example.demo.service..*.*(..)) | service包及其子包中,各类的所有方法 |
()表示无参数,(*)表示恰好一个任意类型的参数,(..)表示零个或多个参数
AOP术语与我的代码例子结合的解释:
| 术语 | 简单理解 | 在你的例子里 |
|---|---|---|
| Aspect(切面) | 把一类额外处理集中放在一起 | LogAspect集中放日志规则 |
| Join Point(连接点) | 程序运行中一个可以被处理的位置;Spring AOP 中主要是方法执行 | 一次createItem()的执行 |
| Pointcut(切入点) | 筛选连接点的规则:选哪些方法 | execution(...ItemServiceImpl.createItem(..)) |
| Advice(通知/增强) | 选中方法后,具体做什么 | 执行前打印“准备创建草稿” |
| Target Object(目标对象) | 真正完成原业务的对象 | ItemServiceImpl对象 |
| AOP Proxy(代理对象) | 调用者接触的“中间人”,负责执行通知并继续调用目标对象 | 你看到的ItemServiceImpl$$SpringCGLIB$$0 |
| Weaving(织入) | 把切面与目标调用联系起来的过程 | Spring AOP 在运行时准备代理,使日志能参与方法调用 |
| Interceptor(拦截器) | 实现“调用前后做额外处理”的一种方式 | 可以把代理里的通知调用链理解为一层层拦截 |
| Introduction(引介) | 让代理对象额外表现为“实现了某个新接口” | 它不修改原类的源码 |
Joinpoint和ProceedingJoinPoint
| 名称 | 是什么 | 常见用途 |
|---|---|---|
JoinPoint | 描述当前被通知处理的方法调用 | 查看方法名getSignature()、参数getArgs()、目标对象getTarget()、代理对象getThis() |
ProceedingJoinPoint | JoinPoint的扩展,供@Around使用 | 除了查看调用信息,还能用proceed()继续调用 |
JoinPoint提供“这次调用的信息”;ProceedingJoinPoint允许你决定“是否继续这次调用”。在 Spring AOP 中,这里的调用点是方法执行
注解装配AOP
除了通过添加切点表达式来定义装配AOP,还可以通过使用注解的方式快速、灵活地定义装配AOP。
首先先定义一个注解
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MetricTime { String value(); }@Target()规定这个注解可以写在哪里
| 值 | 允许标记的位置 |
|---|---|
TYPE | 类、接口、枚举、record 的声明,包括注解类型 |
METHOD | 方法声明 |
FIELD | 成员字段,包括枚举常量 |
PARAMETER | 方法或构造器的参数 |
CONSTRUCTOR | 构造器声明 |
LOCAL_VARIABLE | 方法中的局部变量 |
ANNOTATION_TYPE | 注解类型的声明,例如你定义的MetricTime |
PACKAGE | 包声明,通常写在package-info.java |
TYPE_PARAMETER | 泛型参数声明,例如class Box<T>中的T |
TYPE_USE | 使用某个类型的位置,例如List<@某注解 String> |
MODULE | 模块声明 |
RECORD_COMPONENT | record 的组件,例如record User(String name)中的name |
可以允许多个位置:@Target({ElementType.METHOD, ElementType.TYPE})
@Retention()规定注解信息保留到哪个阶段
| 值 | 注解信息保留到哪里 |
|---|---|
SOURCE | 只留在源码中,编译时丢弃,例如@Override |
CLASS | 写入.class文件,运行时不能通过普通反射读取 |
RUNTIME | 写入.class文件,运行时仍能通过反射读取 |
注解中定义的内容叫属性,是否必填只看它有没有default:
public @interface MetricTime { String value(); // 没有默认值:必填 String unit() default "ms"; // 有默认值:可选 }在切面中,通过将注解信息传给通知来使用注解实现AOP切面
| 写法 | 作用 | 通知方法需要什么参数 |
|---|---|---|
@annotation(com.example.demo.aspect.MetricTime) | 选中带有@MetricTime的方法 | 不需要额外声明MetricTime参数 |
@annotation(metricTime) | 选中方法,同时把它的注解传给通知 | 需要声明MetricTime metricTime,名字必须对应 |
如果使用第一种方法,需要通过反射使用Joinpoint来获取原注解中的元数据,而使用第二种则直接可以获取到元数据
SpringBoot
一个请求到后端的简单流程
浏览器客户端发起HTTP请求->请求经过过滤器->请求进入SpringMVC找到对应方法->请求经过拦截器->请求进入Controller(自动格式转化为相应类型)->请求进入Service处理相应的业务(需要调用数据库时,一般由Service调用Mapper调用数据库)->业务处理完毕后返回Controller返回结果并生成HTTP响应->处理好的数据返回客户端
配置文件
application.properties
采用key=value的格式,因为必须写出完整的key,而配置中key又有很大一部分前缀是相同的,导致可读性困难
# application.properties spring.application.name=${APP_NAME:unnamed} spring.datasource.url=jdbc:hsqldb:file:testdb spring.datasource.username=sa spring.datasource.password= spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver spring.datasource.hikari.auto-commit=false spring.datasource.hikari.connection-timeout=3000 spring.datasource.hikari.validation-timeout=3000 spring.datasource.hikari.max-lifetime=60000 spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.minimum-idle=1 application.yml
采用层级结构,key的重复前缀可省略,可读性好
# application.yml spring: application: name: ${APP_NAME:unnamed} datasource: url: jdbc:hsqldb:file:testdb username: sa password: driver-class-name: org.hsqldb.jdbc.JDBCDriver hikari: auto-commit: false connection-timeout: 3000 validation-timeout: 3000 max-lifetime: 60000 maximum-pool-size: 20 minimum-idle: 1 环境变量
可以使用${}来使用环境变量,环境变量可以是系统变量,也可以是在启动时传入命令配置
启动时的必要注解
@SpringBootApplication:组合注解,里面包含了三个必要的注解
1、@SpringBootConfiguration:表明这是一个Spring配置类
2、@EnableAutoConfiguration:根据已有依赖,自动配置需要的功能
3、@ComponentScan:扫描启动包以及其子包,寻找要交给Spring管理的类
依赖管理pom.xml
SpringBoot项目需要从pom.xml中配置依赖,并交由Maven管理。项目指定了 Spring Boot 父项目的版本;对于它已经管理版本的依赖,添加依赖时通常可以省略单独的版本号。如果要加入未被管理的依赖,就需要确认并指定版本。
当不同依赖间接引入同一个库的不同版本时,Maven 会按依赖管理和依赖树规则确定最终版本:一般优先采用离当前项目更近的版本;距离相同才考虑声明顺序。如果选定版本无法解析,构建通常会报错,不会自动换成任意其他版本。