Spring 总结:从 IOC 到分层解耦
获取参数
你将无法同时获取 @PathVariable 和 @RequestParam
如果最后一个是 @PathVariable
@RequestParam 适用于 URL 查询参数 (? stockQuantity=100) 或表单数据 (form-urlencoded)。
而不适用于put
put 需要 pojo 传递
参数案例
- 数组参数
数组: 请求参数名=形参中数组变量名,直接可以封装
集合: 请求参数名=集合变量名,通过 @RequestParam 绑定参数关系
- 日期参数
使用 @DateTimeFormat(pattern= "") 指定接受参数格式
- Json 参数
使用 @RequestBody
Json 数据 键名 与形参对象 属性名 相同
- 路径参数
localhost:8080/path/1localhost:8080/path/100请求参数 (1,100)成为了请求路径的一部分,这种参数就称为路径参数
@RequestMapping("/path/{id}")
使用 @pathVariable 获取路径中的参数绑定到形参
@DeleteMapping("/{ids}")
public Result delete(@PathVariable List<Integer> ids){
分层解耦
文件上传
接受文件类: MultipartFile
常用方法
String getOriginalFilename(); //获取原始文件名
void transferTo(File dest); //将接收的文件转存到磁盘文件中
long getSize(); //获取文件的大小,单位:字节
byte[] getBytes(); //获取文件内容的字节数组
InputStream getInputStream(); //获取接收到的文件内容的输入流
文件存储
springboot文件默认上传大小为 1M
#配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB
#配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB
本地储存方式
@PostMapping("/upload")
Result upload(String username , Integer age , MultipartFile image) throws IOException {
log.info("文件上传: {}, {}, {}", username, age, image);
String originalFilename = image.getOriginalFilename();
int index = originalFilename.lastIndexOf(".");//获取最后一个.的位置
String extname = originalFilename.substring(index);//获取文件拓展名
String newFileName = UUID.randomUUID().toString()+extname;
log.info("新的文件名: {}", newFileName);
image.transferTo(new File("D:\\Users\\XCJY\\Desktop\\"+newFileName));
return Result.success();
}
阿里云 OSS
依赖 (可选)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
- 阿里云
aliyun.oss.endpoint = https://oss-cn-beijing.aliyuncs.com
aliyun.oss.bucketName = web-tlias1122233
web-tlias1122233
AccessKey ID:LTAI5t5YvwjEkM88f6m8ExCS
AccessKey Secret:wO895xzzup9fBtouTWFZzs7z2Memym
Ssky:
alioss:
access-key-id: LTAI5t5YvwjEkM88f6m8ExCS
access-key-secret: wO895xzzup9fBtouTWFZzs7z2Memym
bucket-name: web-tlias1122233
endpoint: https://oss-cn-beijing.aliyuncs.com
或设置环境变量
OSS_ACCESS_KEY_ID=LTAI5t5YvwjEkM88f6m8ExCS
OSS_ACCESS_KEY_SECRET=wO895xzzup9fBtouTWFZzs7z2Memym
OSS_STS_ROLE_ARN=acs:ram::1547130400260530:role/xxx666
如何使用OSSJavaSDK完成常见操作_对象存储 OSS-阿里云帮助中心 (aliyun.com)
阿里云存储方式
public class UploadController {
@Autowired
AliOSSUtils aliOSSUtils;
@PostMapping("/upload")
Result upload(MultipartFile image) throws IOException {
log.info("文件上传, 文件名: {}", image.getOriginalFilename());
String url = aliOSSUtils.upload(image);
log.info("文件上传完成,文件访问的url", url);
return Result.success(url);
}
}
/**
* 阿里云 OSS 工具类
*/
@Component
public class AliOSSUtils {
@Autowired
private AliOSSProperties aliOSSProperties;
EnvironmentVariableCredentialsProvider credentialsProvider;
{
try {
credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
} catch (ClientException e) {
e.printStackTrace();
}
}
/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile file) throws IOException {
String endpoint = aliOSSProperties.getEndpoint();
String bucketName = aliOSSProperties.getBucketName();
// 获取上传的文件的输入流
InputStream inputStream = file.getInputStream();
// 避免文件覆盖
String originalFilename = file.getOriginalFilename();
String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
//上传文件到 OSS
//OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
ossClient.putObject(bucketName, fileName, inputStream);
//文件访问路径
String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
}
}
登陆校验技术
由于 http 和 https 都是是无状态的,每次访问都是独立的,造成了在服务端没有办法仅仅通过 http 协议来区分两次请求是否来自同一浏览器,同一会话,此时就需要会话跟踪技术
会话技术
数据共享: 非常重要,如登陆后,其他页面依然获取了登陆信息; 验证码: 获取自己填写的验证码和上一次请求当中所生成的验证码的值 (在一次会话当中的两次请求共享数据)
但 http 支持 cookie 的携带,可以进行传送数据
登陆一次后就建立了一个会话,在一次会话中可以发送多个请求
Cookie会话跟踪技术, 存储在客户端, 客户端存储了整个cookie数据,容易被伪造Session会话跟踪技术,存储在服务端,客户端的cookie只储存key值,去服务端查找cookie数据- 令牌技术会话跟踪技术: 客户端请求不再使用
cookie, 而是使用token, 储存在客户端,服务端只负责验证
JWT 令牌
就是为了保证传输的内容是可靠的,没有被第三方篡改的,可以使用这个没有被篡改的 token 登录,否则即使信息泄露,也会拒绝登录
注意 : JWT 令牌本身并不加密,只是通过签名来确保其完整性, 避免伪造,第三部分就是为了避免伪造, 服务端拿到 JWT 令牌后根据第一部分的算法配合自己密钥来得出数字签名,如果和传递过来的不一样就是伪造
第三部分采用第一部分的如 HMAC-SHA256 算法, 传入自己的密钥, 如 itheima,将第二部分的数据进行签名计算, 生成数字签名,而黑客不知道密钥,就无法制造出合法的数字签名,传出去的数字签名也只有自己能进行验证,解决了集群认证问题,使用相同密钥就能解开
用于在通信双方以 json 数据格式安全的传输信息,由于数字签名的存在,这些信息是可靠的
加密 (Encryption): 核心目标是保密性。将原始数据(称为明文)转换为不可读的格式(称为密文),只有拥有密钥的人才能将密文还原回明文。
签名 (Signature): 核心目标是完整性和身份验证。它不隐藏信息,而是通过生成一个附加到数据的数字签名来证明数据的来源和是否被篡改。JWT 并不加密第二部分的信息
jwt 本身无状态,后端无需记录信息,只需要验证第三部分,签名部分可靠就相当于信息可靠
- 第一部分:
Header(头): 记录平台类型 (如:JWT),签名算法 (任意 HS 256),如{"alg": "HS 256","type": "JWT"} - 第二部分:
payload(有效荷载): 携带一些自定义信息、默认信息等。例如:{"id": "1","username": "Tom"} - 第三部分:数值签名,
Signature(签名),防止 Token 被篡改、确保安全性。将header、payload,并加入指定秘钥,通过第一部分的签名算法计算而来
第三部分:.signWith(signatureAlgorithm,secretKey.getBytes(StandardCharsets.UTF_8))
原始数据是 json 格式的数据,通过 base 64 编码 (非加密)转换成下面的格式
Base 64:是一种基于 64 个可打印字符(A-Z a-z 0-9 + /)来表示二进制数据的编码方式。
eyJhbGciOiJIUzI1NiJ9 -> base 64 解码 -> { "alg": "HS 256"}
eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY5MzY3NDQyN30 -> base64解码 -> { "name": "tom".....}
使用 jwts
加密解密都是为了确保 claims 内容没有被篡改
加密采用 builder
- 内容
- 过期时间
- 加密算法+密钥
builder.compact();
解密 从 http 获取 token
- 加密算法+密钥
.parseClaimsJws(token).getBody();获取其中的claims
JWt
public class JwtUtil {
/**
* 生成jwt
* 使用Hs256算法, 私匙使用固定秘钥
*
* @param secretKey jwt秘钥
* @param ttlMillis jwt过期时间(毫秒)
* @param claims 设置的信息
* @return
*/
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);
// 设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp);
return builder.compact();
}
/**
* Token解密
*
* @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
* @param token 加密后的token
* @return
*/
public static Claims parseJWT(String secretKey, String token) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
}
setSigningKey 为了验证 JWT 的完整性和真实性,虽然 claims 能直接获取,但不知真伪
eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY5MzY3NDQyN30.Z0Eac2WZQzHGkBIHDIZF8hjNDlvMOg9ijdSvlAj_u9g
一个小时后的令牌->第一部分和第二部分是相同的,但是第三部分不同,报错已过期 通过第三部分来验证用户和是否有效
eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY5MzcxMjQ4N30.h3f-C46NdJMnlwnzv0uUk9zqW-K-0z7JsGGmv56BNT8
知道密钥的情况下,并不能用密钥来直接解密JWT的签名部分。相反,可以使用密钥验证JWT的签名,以确定JWT是否受信任和完整。
最后一个数字签名加密解不了
统一拦截
过滤器 Filter
概念:Filter 过滤器,是 JavaWeb 三大组件 (Servlet、Filter、Listener)之一
过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能比如:登录校验、统一编码处理、敏感字符处理等
filter 先执行放行前逻辑, 放行后 filterChain.doFilter(servletRequest,servletResponse); 可以访问对应资源,之后回到 filter 中,执行放行后的逻辑操作

当再次访问相同/不同资源,依旧执行相同步骤
- 过滤器链 (使用多个过滤器)
优先级指定有两种方式, 当不指定优先级时,根据名字来
- 如
@Order (2)默认为字母顺序 - 使用
FilterRegistrationBean来设置过滤器的优先级
定义一个Spring的Bean FilterRegistrationBean 来包装我们的自 定义Filter,从而让Spring容器来管理我们的过滤器
同样的在Servlet中,也有类似的包装bean:ServletRegis registrationBean
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<FirstFilter> firstFilterRegistration() {
FilterRegistrationBean<FirstFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new FirstFilter());
registrationBean.setOrder(1); // 设置优先级为1
return registrationBean;
}
@Bean
public FilterRegistrationBean<SecondFilter> secondFilterRegistration() {
FilterRegistrationBean<SecondFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new SecondFilter());
registrationBean.setOrder(2); // 设置优先级为2
return registrationBean;
}
}
| 属性名 | 类型 | 描述 |
|---|---|---|
| filterName | String | 指定过滤器的 name 属性,等价于 |
| value | String[] | 该属性等价于 urlPatterns 属性。但是两者不应该同时使用。 |
| urlPatterns | String[] | 指定一组过滤器的 URL 匹配模式。等价于 标签。 |
| servletNames | String[] | 指定过滤器将应用于哪些 Servlet。取值是 @WebServlet 中的 name 属性的取值,或者是 web.xml 中 的取值。 |
| dispatcherTypes | DispatcherType | 指定过滤器的转发模式。具体取值包括:ASYNC、ERROR、FORWARD、INCLUDE、REQUEST。 |
| initParams | WebInitParam[] | 指定一组过滤器初始化参数,等价于 标签。 |
| asyncSupported | boolean | 声明过滤器是否支持异步操作模式,等价于 标签。 |
| description | String | 该过滤器的描述信息,等价于 标签。 |
| displayName | String | 该过滤器的显示名,通常配合工具使用,等价于 标签。 |
使用
Filter过滤器添加注解@WebFilter(urlPatterns = { "/AddServlet","/qiantai/order.jsp","/RemoveServlet", "/UserOrderingServlet" })使用步骤
springboot启动类添加注解@ServletComponentScan(因为WebListener/WebFilter注解不是spring的规范,所以为了识别它,需要在启动类上添加注解 @servletcomponentscanSpring Boot 会自动扫描指定包中的Servlet、Filter和Listener的实现类,并将它们注册到 Spring 的上下文中。这意味着你不需要在配置文件中手动注册这些组件。)(也可以不加,手动将相关的配置类使用@component作为普通的 bean 返回)- 创建实现
Filter接口, 添加@WebFilter(urlPatterns = "/*")注解 - 重写方法
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) - 从
servletRequest中获取数据,进行操作/判断,filterChain.doFilter(servletRequest, servletResponse)进行放行
FilterDemo
@WebFilter(urlPatterns = "/*")
public class DemoFileter implements Filter {
@Override//初始化方法,只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init 初始化方法执行了");
}
//另外两个方法不常用
@Override//拦截后调用,调用多次
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Demo 拦截到了请求...放行前逻辑");
//放行
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("Demo 拦截到了请求...放行后逻辑");
}
@Override
public void destroy() {
System.out.println("destroy 销毁方法执行了");
}
}
标题
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;//请求头数据
HttpServletResponse resp = (HttpServletResponse) servletResponse;
//1. 获取请求url
String url = req.getRequestURI().toString();
log.info("请求的url:{}", url);
if(url.contains("login")){
log.info("登陆放行");
filterChain.doFilter(servletRequest, servletResponse);//login直接放行
return;
}
String jwt = req.getHeader("token");
if (!StringUtils.hasLength(jwt)){
log.info("未登陆");
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
try {//如果出现异常说明验证没有通过,
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败, 返回未登录错误信息");
Result error = Result.error("解析令牌失败");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
//6.放行。
log.info("令牌合法, 放行");
filterChain.doFilter(servletRequest, servletResponse);//放行
}
}
拦截器 Interceptor
DispacherServlet -> HandlerMapping -> Interceptor -> HandlerAadapter ->调用前准备 handleMethodArgumentResolver(参数解析器) 和 handleMethodReturnValueHandler(返回值处理器)
是一种动态拦截方法调用的机制,类似于过滤器。Spring 框架中提供的,用来动态拦截控制器方法的执行
作用:拦截 spring 环境中请求,在指定的方法调用前后,根据业务需要执行预先设定的代码
- 定义拦截器:实现
HandlerInterceptor接口,并交给 IOC 容器管理 (加@Component),方法有pre/post/afterHandle方法,都是返回 true 放行 - 配置拦截器:创建实现
WebMvcConfigurer的配置类,注入刚才定义的拦截器使之生效,ctrl+o重写addInterceptors方法,添加registry.addInterceptor(loginCheckInterceptor).addPathPatterns("").excludePathPatterns();//配置拦截资源
简介
WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns();//配置拦截资源
}
}
- 多个拦截器的执行顺序
- 若每个拦截器的
preHandle ()都返回 true
此时多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关:
preHandle()会按照配置的顺序执行,而postHandle()和afterComplation()会按照配置的反序执行
- 若某个拦截器的
preHandle ()返回了 false
preHandle() 返回 false 和它之前的拦截器的 preHandle() 都会执行,postHandle() 都不执行,返回 false 的拦截器之前的那一个拦截器的 afterComplation() 会执行 (索引)
全局异常处理器
@RestControllerAdvice
是 @ControllerAdvice 和 @ResponseBody 的组合注解
- 使用方法 ^x4m5fo
- 创建一个类, 添加注解
@RestControllerAdvice - 编写方法,方法注解
@ExceptionHandler(异常类型)
全局异常处理器
@RestControllerAdvice//代表定义了全局异常处理器
public class GlobeExceptionHandler {
@ExceptionHandler(Exception.class)//捕获异常类型:Exception.class所有异常
public Result ex(Exception ex){
ex.printStackTrace();
return Result.error("对不起,操作失败,请联系管理员");
}
}
全局异常处理器拦截自定义异常的抛出
BaseException为自定义异常类的父类,自定义异常类继承BaseException即可被拦截
@ExceptionHandler
public Result exceptionHandler(BaseException ex){
return Result.error(ex.getMessage());
}
事物管理
Spring 事务管理器
为什么不用 AOP 实现事物,因为开发数据库使用的工具是多种多样的 (jdbc, mysql, hibernate),每种事物代码不同
Spring声明式事务对应依赖spring-tx: 包含声明式事务实现的基本规范(事务管理器规范接口和事务增强等等)spring-jdbc: 包含DataSource方式事务管理器实现类DataSourceTransactionManagerspring-orm:包含其他持久层框架的事务管理器实现类例如:Hibernate/Jpa等
我们现在要使用的事务管理器是
org.springframework.jdbc.datasource.DataSourceTransactionManager,将来整合 JDBC 方式、JdbcTemplate 方式、Mybatis 方式的事务实现!
DataSourceTransactionManager 类中的主要方法:
doBegin():开启事务
doSuspend():挂起事务
doResume():恢复挂起的事务
doCommit():提交事务
doRollback():回滚事务
概述
事物管理底层也是 AOP
springboot 启动文件上开启事物管理,添加注解
@EnableTransactionManagement
- 使用注解
@Transactional- 位置: service 层的
方法,类,接口上@Transactional标识在方法上,则只会影响该方法@Transactional标识的类上,则会影响类中所有的方法
- 作用: 将当前方法交给 spring 进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务
rollback;(默认运行时异常才会回滚,通过@Transactional(rollbackFor = Exception.class)可以指定什么异常进行回滚,Exception.class所有异常都会回滚)
- 位置: service 层的
对数据进行 修改 且 执行多次的代码 的需要进行事物管理,如果只执行一步,或者不需要进行修改,也不需要进行事物管理
声明式: 基于 AOP 方式实现,代码更简洁、使用更方便,理解更容易 编程式: 非常灵活,完全由用户自己控制,可以最小粒度的控制事务范围, 和锁类似,按照最小范围使用原则, 尤其编程式事务
@Transactional
@Override
public void delete(int id) {
deptMapper.delete(id);
empMapper.deleteByDeptId(id);
}
- 开启事物管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
属性
| @Transactional属性名 | 说明 |
|---|---|
| name | 当在配置文件中有多个TransactionManager,可以用该属性指定选择哪个事务管理器。 |
| propagation | 事务的传播行为,默认值为REQUIRED。 |
| isolation | 事务的隔离度,默认值采用DEFAULT。 |
| timeout= | 事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。 超时异常 org.springframework.transaction.**TransactionTimedOutException**: Transaction timed out: deadline was Fri Jun 04 16:25:39 CST 2022 |
| readonly=false默认 | 对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化,对增删改操作设置只读会抛出下面异常:Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed |
| rollbackfor | 用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之 间可以通过逗号分隔。 |
| no-rollback-for | 抛出no-rollback-for指定的异常类型,不回滚事务。 |
- 回滚策略 rollbackfor
声明式事务默认只针对运行时异常 RuntimeException及其子类进行回滚,编译时异常或Exception不回滚。

rollbackFor属性:需要设置一个Class类型的对象, 认是RuntimeException and Error异常方可回滚rollbackForClassName属性:需要设置一个字符串类型的全类名noRollbackFor属性:需要设置一个 Class 类型的对象, 指定哪些异常不会回滚, 默认没有指定, 如果指定, 应该在rollbackFor的范围内rollbackFor属性:需要设置一个字符串类型的全类名
- 隔离级别
Mysql/Mysql高级 > SQL 中的四种隔离级别
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化
- 传播行为
propagation
事物 a 方法,事物 b 方法,此时 b 在运行时究竟时加入 a 方法的事物还是不加入新建一个事物?涉及到事物的传播行为
假设有方法 A 和方法 B, 以下是方法 B 加入 propagation 后的效果
A 调用 B 方法时
required(默认): B 需要事物 A 有事物,则 B 加入 A, A 没有事物 , B 则自己创建一个事物required_newB 需要新事物 A 有事物则挂起,B 新事物会优先保证自己运行supportsB 有无事物无所谓 A 有事物则加入,无事物就无所谓not_supportsA 调用无事物B, A 有事物 B 则挂起neverA 调用 B, A 也必须无事物,否则异常mandatoryA 调用 B, A 必须有事物,否则异常nestedA 调用新事物 B, A 有事物作为嵌套事物存在,A 没事物以新的事物执行
传播行为
REQUIRED :大部分情况下都是用该传播行为即可。
REQUIRES_NEW :当我们不希望事务之间相互影响时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。
什么时候使用?
- 当一个基础的业务单元 (如对外提供的http接口,service方法),存在>=2的数据变更时,就需要考虑,部分更行成功,部分更新失败是否会出现脏数据,是否会有影响
若是:则可以考虑引入事务 若否:不建议加入事务
- 只有一个数据变更,但是后续有强依赖这个数据的逻辑,也需要加入事务
比如接到支付成功消息,我要做一个校对的后台任务 我的业务逻辑是:先调用第三方校验是否属实,然后更新本地状态为成功,最后MQ方式通知订单状态变更 此时我们需要将第二步数据变更与第三步消息通知放在事务中,只要通知失败,我们还是不更新本地状态,避免出现我们校验成功了,但是订单还是未支付
- 使用建议避免大事务 只在需要的地方加事务 注意分布式事务场景 所有的操作,走索引,避免出现锁表 减少范围、大批量的数据处理场景 如果业务支撑,可以降低隔离级别,一是减少隔离级别带来的附加成本,二则是可以避免某些场景下的死锁
失效情况
../SpringBoot/Spring总结#代理失效情况
上面这种场景不生效很好理解,子线程的异常不会被外部的线程捕获, testMultThread 这个方法的调用不抛异常,因此不会触发事务回滚
AOP:Aspect Oriented Programming
AOP 本质上就是对代理模式的简化
代理模式
目标 : 放核心代码,减少了目标的调用和打扰 代理 : 放目标方法中的非核心功能
代理有两种: 静态代理 : 为每个类创建一个自己的代理 (太麻烦) 动态代理 : 不单独为类创建单独的代理,而是自动提供一个代理对象
代理解决附加代码干扰核心代码和不方便统一维护的问题
静态代理
Calculator 接口 CalculatorPureImpl 目标类/房东 CalculatorStaticProxy 代理类/中介
public class CalculatorStaticProxy implements Calculator {
// 将被代理的目标对象声明为成员变量
private Calculator target;
// 调用目标方法
public CalculatorStaticProxy(Calculator target) {
this.target = target;
}
@Override
public int add(int i, int j) {
// 附加功能由代理类中的代理方法来实现
System.out.println("参数是:" + i + "," + j);
// 通过目标对象来实现核心业务逻辑
int addResult = target.add(i, j);
System.out.println("方法内部 result = " + result);
return addResult;
}
动态代理
分类 : JDK 动态代理 (接口) 和 cglib 动态代理 (继承)
JDK原生的实现方式 :有接口时 , 目标类实现了接口 , JDK 创建的 代理类 也会自动实现接口,且代理类和目标类之间没有继承关系,无法代理没有实现接口的类
cglib(spring中):没有接口时, 目标类不需要实现 接口,cglib 创建的 代理类 会继承于目标类来实现代理
JDK 动态代理相对于 CGLIB 动态代理来说,生成代理对象的性能更高,而 CGLIB 动态代理需要通过继承和重写目标类的方式进行代理, 在生成代理对象时需要生成字节码和类加载, 相对较慢
这是因为JDK动态代理使用Java标准库提供的反射机制,而CGLIB动态代理需要通过继承和重写目标类的方式进行代理,在生成代理对象时需要生成字节码和类加载,相对较慢。
JDK动态代理工厂
public class JdkProxy {
private Object target;
public JdkProxy(Object target) {
this.target = target;
}
public Object getProxy(){
/**
* newProxyInstance():创建一个代理实例
* 其中有三个参数:
* 1、classLoader:加载动态生成的代理类的类加载器
* 2、interfaces:目标对象实现的所有接口的class对象所组成的数组
* 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重
写接口中的抽象方法 */
ClassLoader classLoader = target.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
/**
* proxy:代理对象
* method:代理对象需要实现的方法,即其中需要重写的方法
* args:method所对应方法的参数
*/
Object result = null;
try {
result = method.invoke(target, args);
System.out.println(method.getName()+",结果:"+ result);
} catch (Exception e) {
e.printStackTrace();
System.out.println(method.getName()+",异常:"+e.getMessage());
} finally {
System.out.println(method.getName()+",方法执行完毕");
}
return result;
}
};
/**
* jdk生成代理对象
* 参数一:类加载器
* 参数二:目标类的接口
* 参数上:具体要进行的代理动作[非核心动作,调用目标方法]
*/ return Proxy.newProxyInstance(classLoader, interfaces,
invocationHandler);
}
}
JdkProxy factory=new JdkProxy(目标类);
目标类的接口 proxy1= (目标类的接口)factory.getProxy(); //生成代理类
proxy1.add(1,1);//JdkProxy会进行拦截,先执行invoke方法,然后执行add方法
proxy1就是代理类,可以正常调方法
AOP 的动态代理选择
使用 AOP 的目标类有接口时,会自动选择 JDK原生的实现方式 实现代理,没有接口会自动选择 cglib 来实现代理
使用
使用方法
定义注解类(@Component + @Aspect),并在方法上 (形参 ProceedingJoinPoint JoinPoint)注解 @Around("execution( )")
//这是一个切面类
@Slf4j
@Component
@Aspect
public class TimeAspect {
//切入点表达式 : 控制切入点
@Around("execution(* com.ithiema.service.*.*(..))")//:指定公用逻辑作用在哪些方法上:service包下所有方法都会运行封装的方法
//通知
public Object recordTime(ProceedingJoinPoint JoinPoint) throws Throwable {//aspect提供共的ProceedingJoinPoint类api
long begin = System.currentTimeMillis();
Object result = JoinPoint.proceed();
long end = System.currentTimeMillis();
log.info(JoinPoint.getSignature()+"方法执行耗时{}ms",end-begin);//JoinPoint.getSignature()得到方法签名
return result;
}
}
///整个被称为切面
//生效的地方被称为切入点
Spring 中 aop 底层基于动态代理 (CGLIB 代理技术)来实现的,程序运行时基于程序动态代理技术,为目标对象生成一个代理对象,对原始方法进行功能的增强,注入时就不再注入目标对象,而是注入代理对象,调用的是代理对象中的方法
通知类型
内部可以填 表达式 或者 注解,填什么,什么所在的地方成为 切入点
JAVA微服务核心/spring/Spring > 4 2 1 相关术语
@Around:环绕通知,此注解标注的通知方法在目标方法前、后根据设置位置都可以被执行,异常代码后的代码不会执行- 需要
手动调用proceedingJoinPoint.proceed();来执行原方法,且返回值必须指定为Object,来接受方法返回值并返回
- 需要
@Before: 前置通知,此注解标注的通知方法在目标方法前被执行@After: 后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行@AfterReturning: 返回后通知, 此注解标注的通知方法在目标方法后被执行, 有异常不会执行returning="result"@AfterThrowing: 异常后通知,此注标注的知方法生常后行throwing = " "
对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
对于,获取连接点信息只能使用 JoinPoint ,它是 ProceedingJoinPoint 的父类
通知顺序
- 不同切面类中,默认按照切面类的类名字母排序: 目标方法前的通知方法:字母排名靠前的先执行 目标方法后的通知方法:字母排名靠前的后执行
- 用
@Order(数字)加在切面类上来控制顺序
目标方法前的通知方法:数字小的先执行
目标方法后的通知方法:数字小的后执行
切入点表达式
@execution
execution([访问修饰符] 返回值 [包名.类名].方法名(方法形参) [throws 异常] ) [] 中的可省略
省略包名. 类名后, 如果有同名方法,都会运行,所以不建议省略

- 统配符号
*单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分..u 多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数,不能用在包开头
匹配 DeptService 下所有方法形参有 Integer 类型,返回值为 void 类型的方法
- 可以使用
且(&&)、或(||)、非(!)来组合比较复杂的切入点表达式。
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是 update 开头。
描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 ..,使用 * 匹配单个包
重用(提取)切点表达式 @Pointcut
某个方法添加切入点表达式的注解 @Pointcut,然后其他通知类型中引用该方法

自定义注解类
@annotation(注解类): 匹配方法上有注解的方法
@within(注解类): 匹配类上有注解的所有方法
自定义注解,在需要用的方法上添加
简化 execution 中复杂的内容
同样创建一个 Aspect 类只是切入点表达式不需要设置范围,只需要设置为注解类位置
@Retention (RetentionPolicy. RUNTIME):
表示这个注解的保留策略是RUNTIME,即运行时保留,可以通过反射读取注解信息
@Target(ElementType.TYPE):
表示这个注解可以应用在类、接口、枚举上 (TYPE 表示类类型) ^f3q55w
使用方法
- 创建一个名为
xxx的annotation注解类,添加注解@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)assets/JavaWeb/image-20231031132350583.png - 依旧定义
Aspect类,类中引用@Around("@annotation(注解类位置)")@Around("@annotation(com.ithiema.anno.Log)") - 在需要用的方法上添加
@xxx即可使用
而 execution 方法直接是 @Around("execution(位置)
声明一个类,添加两个注解@Detention@Target
案例
在 Aspect 中设置@Around("@annotation(com.ithiema.Log)")
@Retention (RetentionPolicy. RUNTIME)
@Target (ElementType. METHOD)
public @interface Log {
String value () default "default value"; // 定义一个元素,带有默认值
OperationType value ();
}
public enum OperationType {
UPDATE, INSERT
}
使用@AutoFill(OperationType.INSERT)
注解属性设置为枚举,方便AOP
拦截
MethodSignature signature = (MethodSignature) joinPoint.getSignature ();//方法签名对象
AutoFill autoFill = signature.getMethod (). getAnnotation (AutoFill. class);//获得方法上的注解对象
OperationType operationType = autoFill.value ();//获得数据库操作类型
连接点/获取通知相关信息
对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
对于其他四种通知,获取连接点信息只能使用 JoinPoint ,它是 ProceedingJoinPoint 的父类
`joinPoint.getSignature().getDeclaringType().getAnnotation(MdcDot.class);`
注意: joinPoint 是 aspectj 中的,不是 aopalliance 中的
- 获取目标方法的返回值
@AfterReturning(value = "execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
}
- 获取目标方法的异常 (出现异常时)
@AfterThrowing(value = "execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
}
操作日志
LogAspect
@Slf4j
@Component
@Aspect
public class LogAspect {
@Autowired
private OperateLogMapper operateLogMapper;
@Autowired
private HttpServletRequest request;
@Around("@annotation(com.ithiema.anno.Log)")//指定annotation位置
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
//由于不能随便定义变量
//获取操作人
String jwt = request.getHeader("token");
Claims claims = JwtUtils.parseJWT(jwt);
Integer operateUser = (Integer) claims.get("id");
//操作时间
LocalDateTime operateTime = LocalDateTime.now();
//操作类名
String className = joinPoint.getTarget().getClass().getName();
//操作方法名
String methodName = joinPoint.getSignature().getName();
//操作方法参数
Object[] args = joinPoint.getArgs();
String methodParams = Arrays.toString(args);
long begin = System.currentTimeMillis();
//调用原始方法
Object result = joinPoint.proceed();
//上面原始目标方法返回值不是json
String returnValue = JSONObject.toJSONString(result);
//记录运行时间
long end = System.currentTimeMillis();
long costTime= end - begin;
//记录操作日志
OperateLog operateLog = new OperateLog(null,operateUser,operateTime,className,methodName,methodParams,returnValue,costTime);
operateLogMapper.insert(operateLog);
log.info("AOP记录操作日志: {}" , operateLog);
return result;
}
}
日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长
注解类
@Retention (RetentionPolicy. RUNTIME):
表示这个注解的保留策略是 RUNTIME, 即运行时保留, 可以通过反射读取注解信息
@Target(ElementType.TYPE):
表示这个注解可以应用在类、接口、枚举上 (TYPE 表示类类型)
- 内部属性格式
类型 名称() default 值;
别的类 可以通过反射来获取其他类上注解的值
java
您提到的 @AliasFor 注解是Spring Framework中的一部分,确实在注解类中使用了自身的注解。这种情况是通过Spring框架的特殊处理机制实现的,而不是Java语言本身的特性。Java不允许在注解类中直接使用自身的注解。
@AliasFor 注解的 value 和 attribute 属性互为别名,这意味着这两个属性可以互相替代使用,设置其中一个属性的值会自动应用到另一个属性上。例如:
@MyAnnotation(value = "test"),attribute也会被设置为test
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface AliasFor {
@AliasFor("attribute")
String value() default "";
@AliasFor("value")
String attribute() default "";
Class<? extends Annotation> annotation() default Annotation.class;
}
代码规范
业务层 (Controller, Service)方法名: login 等
持久层 (dao, mapper),是用来操作数据库的,用 login 不合适,适合用数据库操作相关的方法名,如 getByUsernameAndPassword
方法放到对应 mapper 中,比如删除部门的同时员工也需要删除,删除部门的方法放到部门中,删除员工的方法放到员工中,不要将删除员工的代码放到部门中









































