故天将降大任于斯人也,必先苦其心志,劳其筋骨,饿其体肤,空乏其身,行拂乱其所为,所以动心忍性,曾益其所不能。

SpringBoot 记录集锦

前言

好记性不如烂笔头,开始学习SpringBoot了,有些东西就是要靠记录的,本篇文章记录了学习过程中遇到的Bug和需要记录的代码

Bug

1 在使用Mybatis做数据库条件查询操作时,不可避免的要增加新的参数,MyBatis在处理多参数的方法时,无法正确地映射这些参数。MyBatis在处理多参数的方法时,会默认将参数映射为param1param2,或者arg0arg1,而不是在注解中直接使用的namegender等参数名。

例如我们接下来要进行条件查询

1
2
3
4
5
6
select *
from emp
where name like '%张%'
and gender = 1
and entrydate between '2010-01-01' and '2020-01-01'
order by update_time desc;
1
2
3
4
5
6
7
8
9
10
@Test
public void list(){
List<Emp> empList = empMapper.list("张", (short) 1, LocalDate.of(2010, 1, 1), LocalDate.of(2020, 1, 1));
System.out.println(empList);
}

// 条件查询 姓名 张(模糊查询) 性别:男 入职时间2010-01-01 -- 2020-01-01
@Select("select * from emp where name like concat('%', #{name}, '%') and gender = #{gender} and " +
"entrydate between #{begin} and #{end} order by update_time desc ")
public List<Emp> list(String name, Short gender, LocalDate begin , LocalDate end);

会出现参数不匹配的错误

Caused by: org.apache.ibatis.binding.BindingException: Parameter ‘name’ not found. Available parameters are [arg3, arg2, arg1, arg0, param3, param4, param1, param2]

此时我们可以使用@Param注解手动映射即可

1
public List<Emp> list(@Param("name") String name, @Param("gender") Short gender, @Param("begin") LocalDate begin, @Param("end") LocalDate end);

Notes

1 Mybatis-Xml文件映射

开发规范:

  1. XML映射文件的名称与Mapper接口名称一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名)

  2. XML映射文件的namespace属性为Mapper接口全限定名一致

  3. XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致

    1
    2
    3
    4
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

    image-20240818154326925

推荐安装插件:MybatisX

2 application.properties 文件常用配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 配置Spring Boot应用的端口号为8080
server.port=8080

# >>> MyBatis配置
# 指定MyBatis的Mapper文件的路径
mybatis.mapper-locations=classpath:mappers/*xml
# 指定MyBatis的实体类所在的包路径
mybatis.type-aliases-package=wiki.zhr.mybatisdemo.mybatis.entity
# 开启mybatis的驼峰命名自动映射开关 a_column ------> aCloumn
mybatis.configuration.map-underscore-to-camel-case=true
#配置mybatis的日志, 指定输出到控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl


# >>> 数据库MySql配置
# 配置数据源的驱动类
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 配置数据源的URL,指定连接的数据库为mybatis,位于本地的MySQL服务器
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
# 配置数据库的用户名
spring.datasource.username=root
# 配置数据库的密码
spring.datasource.password=Zhr202012345

# 配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB

# 配置单个请求最大(总)上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB

3 新建SpringBoot Web项目常见依赖

  • Spring Web (Web)
  • Lombok (Developer Tools)
  • MySQL Driver (SQL)
  • MyBatis Framework (SQL)

4 前后端交互统一响应结果 pojo/Result.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Integer code;//响应码,1 代表成功; 0 代表失败
private String msg; //响应信息 描述字符串
private Object data; //返回的数据

//增删改 成功响应
public static Result success(){
return new Result(1,"success",null);
}
//查询 成功响应
public static Result success(Object data){
return new Result(1,"success",data);
}
//失败响应
public static Result error(String msg){
return new Result(0,msg,null);
}
}

5 在controller层使用log进行日志输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// example

@Slf4j // 该注解等效于定义log 源于log依赖
@RestController
public class DeptController {

// private static Logger log = LoggerFactory.getLogger(DeptController.class);
@RequestMapping("/depts")
public Result list() {
log.info("查询全部部门数据"); // 替代sout
return Result.success();
}
}

6 在Controller中向前端请求参数时用到的各个注解

  • @PathVariable 请求URL参数

    1
    2
    3
    4
    5
    6
    @GetMapping("/{id}")
    public Result queryById(@PathVariable Integer id){
    log.info("根据id查询部门:{}", id);
    Dept dept = deptService.queryById(id);
    return Result.success(dept);
    }
  • @RequestBody 请求已经封装好的类参数

    1
    2
    3
    4
    5
    6
    @PutMapping
    public Result updateById(@RequestBody Dept dept){
    log.info("根据id修改部门:{}", dept.getId());
    deptService.updateById(dept);
    return Result.success();
    }
  • @RequestParam 请求未封装的参数(如果前端没有指定值,可以设置默认值)

    1
    2
    3
    4
    5
    6
    @GetMapping
    public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pageSize){
    log.info("分页查询,参数:{}, {}",page, pageSize);
    PageBean pageBean = empService.list(page, pageSize);
    return Result.success(pageBean);
    }

7 后端分页神器,依赖插件:PageHelper

1
2
3
4
5
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>

用法:在ServiceImpl中:

1
2
3
PageHelper.startPage(page, pageSize);
List<Emp> empList = empMapper.list(); // EmpMapper里面直接正常查询所有的信息就好
Page<Emp> p = (Page<Emp>) empList; // 强制转换

eg.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public PageBean list(Integer page, Integer pageSize) {
// long total = empMapper.total();
// Integer start = (page - 1) * pageSize;
// List<Emp> empList = empMapper.list(start, pageSize);

// 1. 设置分页参数
PageHelper.startPage(page, pageSize);
// 2. 查询数据
List<Emp> empList = empMapper.list();
Page<Emp> p = (Page<Emp>) empList;
// 3. 封装PageBean对象
PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
return pageBean;
}

8 服务端处理前端上传的文件 & 本地存储 & 阿里云对象服务云存储

前端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<form action="/upload" method="post" enctype="multipart/form-data">
<!-- 表单开始,action属性指定提交地址,method指定提交方法,enctype用于支持文件上传 -->
姓名: <input type="text" name="username"><br>
<!-- 用户输入姓名的文本框 -->

年龄: <input type="text" name="age"><br>
<!-- 用户输入年龄的文本框 -->

头像: <input type="file" name="image"><br>
<!-- 用户上传头像的文件选择框 -->

<input type="submit" value="提交">
<!-- 提交按钮,点击后表单数据将发送到服务器 -->
</form>

后端:

本地存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
@RestController
@RequestMapping("/upload")
public class uploadController {
@PostMapping
public Result upload(String username, Integer age, MultipartFile image) throws IOException {
log.info("文件上传:{}, {}, {}", username, age, image);

// 获取原始文件名
String originalFilename = image.getOriginalFilename();

// 构造唯一的文件名 -- uuid(通用唯一识别码)
int index = originalFilename.lastIndexOf(".");
String extname = originalFilename.substring(index);
String newFileName = UUID.randomUUID().toString() + extname;

// 将文件存储在本地 /Users/hrz/Pictures/server-img
image.transferTo(new File("/Users/hrz/Pictures/server-img/" + newFileName));
return Result.success();
}
}

云存储 官方文档

安装阿里云oss存储依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
<!-- 如果使用的是Java 9及以上的版本,则需要添加JAXB相关依赖 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.3</version>
</dependency>

注意要在Intellij里配置环境变量 OSS_ACCESS_KEY_IDOSS_ACCESS_KEY_SECRET

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package wiki.zhr.tliaswebmanagement;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.common.auth.*;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.FileInputStream;
import java.io.InputStream;

public class Demo {

public static void main(String[] args) throws Exception {

// System.out.println("OSS_ACCESS_KEY_ID: " + System.getenv("OSS_ACCESS_KEY_ID"));
// System.out.println("OSS_ACCESS_KEY_SECRET: " + System.getenv("OSS_ACCESS_KEY_SECRET"));



// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-beijing.aliyuncs.com";
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// 填写Bucket名称,例如examplebucket。
String bucketName = "zouhr-web-tlias";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "user-images/1.jpg";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath= "/Users/hrz/Pictures/壁纸/jjy.png";

// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);

try {
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
// 创建PutObject请求。
PutObjectResult result = ossClient.putObject(putObjectRequest);
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
}

在日后开发时,可以考虑添加工具 utils/AliOSSUtils.java,注意注解@Component和@Value的使用,前者可以在使用该工具类时直接注入,后者可以直接从配置文件中获取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package wiki.zhr.utils;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.UUID;

/**
* 阿里云 OSS 工具类
*/
@Component
public class AliOSSUtils {

@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;
@Value("${aliyun.oss.accessKeySecret}")
private String accessKeySecret;
@Value("${aliyun.oss.bucketName}")
private String bucketName;

/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile file) throws IOException {
// 获取上传的文件的输入流
InputStream inputStream = file.getInputStream();

// 避免文件覆盖
String originalFilename = file.getOriginalFilename();
String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));

// 将文件上传至 user-images 目录下
// String objectName = "user-images/" + fileName;

//上传文件到 OSS
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.putObject(bucketName, fileName, inputStream);

//文件访问路径
String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
}

}

别忘了在applicaation.properties中配置OSS相关信息

1
2
3
4
aliyun.oss.endpoint=https://oss-cn-beijing.aliyuncs.com
aliyun.oss.accessKeyId=LTAI5tN3Lq4PRnQvFQLXzXtc
aliyun.oss.accessKeySecret=KU1rOO0ErixkMBVrDk7M6jP07sUrhO
aliyun.oss.bucketName=zouhr-web-tlias

9 JWT令牌

引入jjwt依赖:

1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

JWT工具类 JwtUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class JwtUtils {

private static String signKey = "itheima";//签名密钥
private static Long expire = 43200000L; //有效时间

/**
* 生成JWT令牌
* @param claims JWT第二部分负载 payload 中存储的内容
* @return
*/
public static String generateJwt(Map<String, Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)//自定义信息(有效载荷)
.signWith(SignatureAlgorithm.HS256, signKey)//签名算法(头部)
.setExpiration(new Date(System.currentTimeMillis() + expire))//过期时间
.compact();
return jwt;
}

/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分负载 payload 中存储的内容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)//指定签名密钥
.parseClaimsJws(jwt)//指定令牌Token
.getBody();
return claims;
}
}

10 Filter过滤器

定义:实现Filter接口即可

配置:@WebFilter(urlPatterns = “/*”) @ServletComponentScan (加在启动类上,开启对Servlet组件支持)

filter.DemoFilter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )
public class DemoFilter implements Filter {
@Override //初始化方法, 只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init 初始化方法执行了");
}

@Override //拦截到请求之后调用, 调用多次
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Demo 拦截到了请求...放行前逻辑");
//放行
chain.doFilter(request,response);
}

@Override //销毁方法, 只调用一次
public void destroy() {
System.out.println("destroy 销毁方法执行了");
}
}

下面给出一个只对login放行的filter过滤器:LoginChectFilter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 1. 获取请求url
String url = req.getRequestURL().toString();
log.info("请求的url:{}", url);

// 2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行
if(url.contains("login")) {
log.info("登录操作,放行...");
chain.doFilter(request, response);
return ;
}

// 3. 获取请求头中的令牌(token)
String jwt = req.getHeader("token");

// 4. 判断令牌是否存在,若不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(jwt)) {
log.info("请求头token为空,返回未登录信息");
Result error = Result.error("NOT_LOGIN");
// 手动转换 对象 -- json -----> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return ;
}

// 5. 解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) { // jwt解析失败
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
// 手动转换 对象 -- json -----> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return ;
}

// 6. 放行
log.info("令牌合法,放行!");
chain.doFilter(request, response);

}
}

11 Interceptor拦截器 (推荐,因为是Spring体系下的

定义:实现HandlerInterceptor接口,并重写其所有方法

interceptor.LoginCheckInterceptor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override // 目标资源方法运行前运行,返回true:放行,返回false:不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取请求url
String url = request.getRequestURL().toString();
log.info("请求的url:{}", url);

// 2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行
if(url.contains("login")) {
log.info("登录操作,放行...");
return true;
}

// 3. 获取请求头中的令牌(token)
String jwt = request.getHeader("token");

// 4. 判断令牌是否存在,若不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(jwt)) {
log.info("请求头token为空,返回未登录信息");
Result error = Result.error("NOT_LOGIN");
// 手动转换 对象 -- json -----> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return false;
}

// 5. 解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) { // jwt解析失败
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
// 手动转换 对象 -- json -----> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return false;
}

// 6. 放行
log.info("令牌合法,放行!");
return true;
}

@Override // 目标资源方法运行后运行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle ...");
}

@Override // 试图渲染完毕后运行,最后运行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion ...");
}
}

须配置config.Webconfig.java

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class Webconfig implements WebMvcConfigurer {

@Autowired
private LoginCheckInterceptor loginCheckInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}
}

12 全局异常处理器

controller,service,mapper都不抛出的异常交给全局异常处理器,例如,新增数据违背了该字段是唯一性,此时可以交给全局异常处理器讲提示消息反馈给前端

exception.GlobalExceptionHandler

1
2
3
4
5
6
7
8
9
10
@RestControllerAdvice  // 表示当前类为全局异常处理器
public class GlobalExceptionHandler {

@ExceptionHandler(Exception.class) // 指定可以捕获哪种类型的异常进行处理
public Result ex(Exception e){
e.printStackTrace(); // 打印堆栈中的异常信息
// 捕获到异常之后,响应一个标准的Result
return Result.error("对不起,操作失败,请联系管理员");
}
}

13 Spring事务管理

事务:一组操作的集合,它是一个不可分割的工作单位

使用注解@Transaction,三种使用位置

  • 写在方法上
  • 写在类上
  • 写在接口上

有需要的话可以在配置文件中加入其日志

1
logging.level.org.springframework.jdbc.support.JdbcTransactionManager=debug

如果想要事务里的所有异常(不仅包括运行时异常)都能正常回滚

1
@Transactional(rollbackFor=Exception.class)

14 Spring AOP面向切面编程

定义:面向切面编程就是面向特定方法编程,在程序运行期间在不修改源代码的基础上对已有方法进行增强

其实就是定义一个模板方法,里面包含我们需要增强的方法

典型的应用场景:

  • 记录操作日志
  • 权限控制
  • 事务管理

优势:

  1. 代码无侵入
  2. 减少重复代码
  3. 提高开发效率
  4. 维护方便

添加依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

示例:统计各个业务层方法执行耗时 aop.TimeAspect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j
@Component
@Aspect // 表明为一个AOP类
public class TimeAspect {

@Around("execution(* wiki.zhr.service.*.*(..))") // 切入点表达式
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//记录方法执行开始时间
long begin = System.currentTimeMillis();

//执行原始方法
Object result = pjp.proceed();

//记录方法执行结束时间
long end = System.currentTimeMillis();

//计算方法执行耗时
log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin);

return result; // 没有返回值的话原始方法就拿不到返回值
}
}

核心概念:

  1. 连接点(JoinPoint):可以被AOP控制的方法
  2. 通知(Advice):指那些重复的逻辑,也就是共性功能(最终体现为一个方法)
  3. 切入点(PointCut):匹配连接点的条件,通知仅会在切入点方法执行时被应用
  4. 切面(Aspect):描述通知与切入点的对应关系(通知➕切入点)
  5. 目标对象(Target):通知所应用的对象