title: “Spring”
date: “2022-04-08T10:29:44+08:00”
tags:
- “java”
categories: - “java”
toc: true
bookComments: false
bookSearchExclude: false
IOC、DI
xml====》xml配置文件注册bean类
注解====》 注解开发-CSDN博客
IOC(Inversion of Control):反转控制。由主动的 new 资源变为被动的接受资源
即组件以一些预先定义好的方式(例如:setter 方法)接受来自于容器的资源注入。DI(Dependency Injection):依赖注入。
IOC 容器和 Bean 的配置
链接 2.1====》笔记 【金山文档】 尚硅谷 IOC和bean的配置
@Resource 与@Autowired 的 区别
- @Resource 默认是按照变量名称来装配注入的,只有当找不到与名称匹配的 bean 才会按照类型来装配注入;
- @Autowired 默认是按照类型装配注入的,如果想按照变量名称来转配注入,则需要结合@Qualifier 一起使用
todo 动态注册bean
泛型依赖注入
链接 2.2====》 视频57~59——尚硅谷
使用场景:todo
解释:
单继承,自然很容易确定子类类型。
Spring中如果有继承且有泛型,Spring会通过泛型来确定子类的类型。
两个子类之间的依赖关系不需要在子类中去声明,而是在父类中进行了声明,而依赖的纽带就是 泛型类型,必须是相同的父类泛型类型才具有依赖关系。
类似如图
举例:
我们创建三个Dao,BookDao和UserDao都继承BaseDao,并且二者都有一个save方法,各自的Service都调用各自的Dao的Save方法。
Dao:
Service:
Test:
这样必然是能执行成功的,但是很繁琐,有形式重复代码。
现在使用泛型依赖注入。
将BookService 和UserService都清空,都继承BaseService,代码都放在BaseService中。
这样就成功的进行了泛型依赖注入,
值得注意的是,BaseService不需要注入容器中;因为BookService 和UserService继承了BaseService,便可以时候用器save方法和@autowired ( 不懂就点击○演示 )
原理:
为什么BookService 能找到BaseDao——原理
Spring中如果有继承且有泛型,Spring会通过泛型来确定子类的类型
AOP
什么是AOP
面向切面编程:
日常中权限校验、日志记录、统计等,这些代码会散落穿插在各个业务逻辑中,非常冗余且不利于维护。而且每次新的业务都需要新添加日志记录等,太繁琐,造成了高耦合低内聚。
AOP将权限校验、日志记录等非业务代码完全提取出来,与业务代码分离,并寻找节点切入业务代码中:
AOP要做三类事:
- 在哪里切入,也就是权限校验等非业务操作在哪些业务代码中执行。
- 在什么时候切入,是业务代码执行前还是执行后。
- 切入后做什么事,比如做权限校验、日志记录等。
AOP的体系可以梳理为下图:
相关名称和概念
切点表达式:切入点就相当于数据库中的数据,切入点表达式就相当于与sql语句。
横切关注点:从每个方法中抽取出来的同一类非核心业务。
切面(Aspect):封装横切关注点信息的类,每个关注点体现为一个通知方法。
通知(Advice):切面的各个具体工作
目标(Target):被通知的对象
代理(Proxy):目标对象加了通知之后的代理对象
连接点(Joinpoint):横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。例如:类某个方法调用前、调用后、方法捕获到异常后等。在应用程序中可以使用横纵两个坐标来定位一个具体的连接点如下图
切入点(pointcut):真正使用通知方法的连接点。每个类的方法中都包含多个连接点。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
相关包依赖配置
手动
基础包:
面向切面编程包:
maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
在Springboot配置类中加@EnableAspectJAutoProxy
加不加都可。
todo 看看原码配置与否的区别
注解
通知注解顺序
@Pointcut
@Pointcut
注解,用来定义一个切点。其他通知注解,可以直接使用pointcut =“pointCut()”
@Aspect
@Component
public class LogAspectHandler {
/**
* 定义一个切面,拦截 com.ytte.controller 包和子包下的所有方法
*/
@Pointcut("execution(* com.ytte.controller..*.*(..))")
public void pointCut() {}
}
@Pointcut 注解指定一个切点,定义需要拦截的东西,这里介绍两个常用的表达式:
execution()
,annotation()
。
execution表达式:
- 切入点表达式的语法格式
execution([权限修饰符(可写可不写)] [返回值类型] [简单类名/全类名] [方法名] ([参数列表]))
- 举例说明
| 表达式 | execution( com.ytte.spring.ArithmeticCalculator.(..)) |
| —- | —- |
| 含义 | ArithmeticCalculator接口中声明的所有方法。
第一个 “ ”代表任意修饰符及任意返回值。
第二个 “ ”代表任意方法。
“..”匹配任意数量、任意类型的参数。
若目标类、接口与该切面类在同一个包中可以省略包名。 |
表达式 | execution(public ArithmeticCalculator.(..)) |
---|---|
含义 | ArithmeticCalculator接口的所有公有方法 |
表达式 | execution(public double ArithmeticCalculator.*(..)) |
---|---|
含义 | ArithmeticCalculator接口中返回double类型数值的方法 |
表达式 | execution(public double ArithmeticCalculator.(*double, ..)) |
---|---|
含义 | 第一个参数为double类型的方法。“..” 匹配任意数量、任意类型的参数。 |
表达式 | execution(public double ArithmeticCalculator.(double, *double)) |
---|---|
含义 | 参数类型为double,double类型的方法 |
- 在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。 | 表达式 | execution ( .add(int,..)) || execution( .sub(int,..)) | | —- | —- | | 含义 | 任意类中第一个参数为int类型的add方法或sub方法 |
annotation() 表达式:
annotation()
方式是针对某个注解来定义切点,比如我们对具有 @PostMapping 注解的方法做切面,可以如下定义切面:
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void annotationPointcut() {}
然后使用该切面的话,就会切入注解是 @PostMapping
的所有方法。这种方式很适合处理 @GetMapping、@PostMapping、@DeleteMapping
不同注解有各种特定处理逻辑的场景。
针对自定义注解来定义切面。
@Pointcut("@annotation(com.example.demo.PermissionsAnnotation)")
private void permissionCheck() {}
@Around
Around增强处理有以下特点:
@Around可以自由选择增强动作与目标方法的执行顺序,可以在增强动作前后,甚至过程中执行目标方法。
- 当定义一个Around增强处理方法时,该方法的第一个形参必须是 ProceedingJoinPoint 类型(至少一个形参)。(ProceedingJoinPoint是JoinPoint子类)(注意是大写的P)
- 在增强处理方法体内调用ProceedingJoinPoint参数的procedd()方法才会执行目标方法。
@Around可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值。
- 调用ProceedingJoinPoint的proceed方法时,还可以传入一个Object[ ]对象,该数组中的值将被传入目标方法作为实参。
如果传入的Object[ ]数组长度与目标方法所需要的参数个数不相等,或者Object[ ]数组元素与目标方法所需参数的类型不匹配,程序就会出现异常。
- 调用ProceedingJoinPoint的proceed方法时,还可以传入一个Object[ ]对象,该数组中的值将被传入目标方法作为实参。
@Around功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before、AfterReturning就能解决的问题,就没有必要使用Around。如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用Around。
使用场景:需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用Around增强处理了。
例子
- 自定义注解类:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PermissionAnnotation{
}
- 定义接口类: ```java package com.example.demo;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.springframework.web.bind.annotation.*;
@RestController @RequestMapping(value = “/permission”) public class TestController { @RequestMapping(value = “/check”, method = RequestMethod.POST) @PermissionsAnnotation() public JSONObject getGroupList(@RequestBody JSONObject request) { return JSON.parseObject(“{\”message\”:\”SUCCESS\”,\”code\”:200,\”data\”:” + request + “}”); } }
-
唯一切面类:
```java
package com.example.demo;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Order(1)
public class PermissionAdvice {
@Pointcut("@annotation(com.example.demo.PermissionsAnnotation)")
private void permissionCheck() {
}
@Around("permissionCheck()") //(注意是大写的P)
public Object permissionCheck(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("===================开始增强处理===================");
//获取请求参数,详见接口类
Object[] objects = joinPoint.getArgs();
Long id = ((JSONObject) objects[0]).getLong("id");
String name = ((JSONObject) objects[0]).getString("name");
System.out.println("id1->>>>>>>>>>>>>>>>>>>>>>" + id);
System.out.println("name1->>>>>>>>>>>>>>>>>>>>>>" + name);
// 修改入参
JSONObject object = new JSONObject();
object.put("id", 8);
object.put("name", "lisi");
objects[0] = object;
// 将修改后的参数传入
return joinPoint.proceed(objects);
}
}
- 结果:
传入参数:{"id":-8,"name":"--lisi "}
,响应结果表明:@Around
截取到了接口的入参,并使接口返回了切面类中的结果。可看出对返回值进行了更改,。
@Before
注解指定的方法在切面切入目标方法之前执行,可以做一些 Log
处理,也可以做一些信息的统计,比如获取用户的请求 URL
以及用户的 IP
地址等等。
JointPoint
可以用来获取一个签名,利用签名可以获取请求的包名、方法名,包括参数(通过joinPoint.getArgs()
获取)等。
例如下面代码:
pointCut()为1中定义的,这里是使用重复的切入点表达式@Pointcut
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Aspect
@Component
@Slf4j
public class LogAspectHandler {
/**
* 在上面定义的切面方法之前执行该方法
* @param joinPoint jointPoint
*/
@Before("pointCut()")
public void doBefore(JoinPoint joinPoint) {
log.info("====doBefore方法进入了====");
// 获取签名
Signature signature = joinPoint.getSignature();
// 获取切入的全限定类名
String declaringTypeName = signature.getDeclaringTypeName();
// 获取即将执行的方法名
String funcName = signature.getName();
log.info("即将执行方法为: {},属于{}包", funcName, declaringTypeName);
// 也可以用来记录一些信息,比如获取请求的 URL 和 IP
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 获取请求 URL
String url = request.getRequestURL().toString();
// 获取请求 IP
String ip = request.getRemoteAddr();
log.info("用户请求的url为:{},ip地址为:{}", url, ip);
}
}
@After
@After
注解和 @Before
注解相对应,指定的方法在切面切入目标方法之后执行,也可以做一些完成某方法之后的 Log 处理。
@Aspect
@Component
@Slf4j
public class LogAspectHandler {
/**
* 定义一个切面,拦截 com.mutest.controller 包下的所有方法
*/
@Pointcut("execution(* com.mutest.controller..*.*(..))")
public void pointCut() {}
/**
* 在上面定义的切面方法之后执行该方法
* @param joinPoint jointPoint
*/
@After("pointCut()")
public void doAfter(JoinPoint joinPoint) {
log.info("==== doAfter 方法进入了====");
Signature signature = joinPoint.getSignature();
String method = signature.getName();
log.info("方法{}已经执行完", method);
}
}
写个 Controller 测试一下执行结果,新建一个 AopController 如下:
@RestController
@RequestMapping("/aop")
public class AopController {
@GetMapping("/{name}")
public String testAop(@PathVariable String name) {
return "Hello " + name;
}
}
启动项目,在浏览器中访问 localhost:8080/aop/name,观察一下控制台的输出信息:
====doBefore 方法进入了====
即将执行方法为: testAop,属于com.itcodai.mutest.AopController包
用户请求的 url 为:http://localhost:8080/aop/name,ip地址为:0:0:0:0:0:0:0:1
==== doAfter 方法进入了====
方法 testAop 已经执行完
@AfterReturning
@AfterReturning
注解和 @After
有些类似,区别在于 @AfterReturning
注解可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理(AfterReturning切面类拿到的是原方法的return 而不是原方法的参数,不能影响更改原方法的返回值ProceedingJoinPoint is only supported for around advice),例如:
@Aspect
@Component
@Slf4j
public class LogAspectHandler {
/**
* @param joinPoint joinPoint
* @param result result
*/
@AfterReturning(pointcut = "pointCut()", returning = "result")
public void doAfterReturning(JoinPoint joinPoint, Object result) {
Signature signature = joinPoint.getSignature();
String classMethod = signature.getName();
log.info("方法{}执行完毕,返回参数为:{}", classMethod, result);
// 实际项目中可以根据业务做具体的返回值增强
log.info("对返回参数进行业务上的增强:{}", result + "增强版");
}
}
需要注意的是,在 @AfterReturning 注解 中, 属性 returning 的值必须要和参数保持一致,否则会检测不到。该方法中的第二个入参就是被切方法的返回值,在 doAfterReturning 方法中可以对返回值进行增强,可以根据业务需要做相应的封装。测试一下:
方法 testAop 执行完毕,返回参数为:Hello CSDN
对返回参数进行业务上的增强:Hello CSDN 增强版
@AfterThrowing
当方法抛出异常时,会进入 @AfterThrowing
注解的方法中执行。要注意的是 throwing
属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常。异常接受类型一定要Exception,写子类写小了AOP接受不到。
返回值也是如此,写Object
@Aspect
@Component
@Slf4j
public class LogAspectHandler {
/**
* 在上面定义的切面方法执行抛异常时,执行该方法
* @param joinPoint jointPoint
* @param ex ex
*/
@AfterThrowing(pointcut = "pointCut()", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
Signature signature = joinPoint.getSignature();
String method = signature.getName();
// 处理异常的逻辑
log.info("执行方法{}出错,异常为:{}", method, ex);
}
}
@Order
在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。
切面的优先级可以通过实现Ordered接口或利用@Order注解指定。
实现Ordered接口,getOrder()方法的返回值越小,优先级越高。
注意事项
AfterReturning与Around的区别
ProceedingJoinPoint的proceed方法,只能用在Around上,所以AfterReturning无法改变原方法的返回值
Around拿到的是原方法的参数,通过对参数的更改,来改变返回值;而AfterReturning拿到的是原方法的return
JoinPoint获取目标方法的信息 导包:(注意是大写的P)
视频 p72 ———— 尚硅谷
import org.aspectj.lang.JoinPoint;
Object[] objects = joinPoint.getArgs();
- 获取方法签名(通过签名可以你获得方法所有信息)
Signature signature = joinPoint.getSignature();
- 获取方法名
String funcName = signature.getName();
- 获取切入的全限定类名
String declaringTypeName = signature.getDeclaringTypeName();
Spring对通知方法的约束
补充:异常接受类型一定要Exception,写子类写小了AOP接受不到。返回值也是如此,写Object
动态代理:
aop 实质就是用了 jdk 的动态代理(需要对象实现了接口)
代理设计模式的原理:使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
作用:
动态代理主要用来做方法的增强,让你可以在不修改源码的情况下,增强一些方法,在方法执行前后做任何你想做的事情(甚至根本不去执行这个方法)。因为在InvocationHandler的invoke方法中,你可以直接获取正在调用方法对应的Method对象,具体应用的话,比如可以添加调用日志,做事务控制等。
还有一个有趣的作用是可以用作远程调用。比如现在有Java接口,这个接口的实现部署在其它服务器上,在编写客户端代码的时候,没办法直接调用接口方法,因为接口是不能直接生成对象的,这个时候就可以考虑代理模式(动态代理)了,通过Proxy.newProxyInstance代理一个该接口对应的InvocationHandler对象,然后在InvocationHandler的invoke方法内封装通讯细节就可以了。具体的应用,最经典的当然是Java标准库的RMI,其它比如hessian,各种webservice框架中的远程调用,大致都是这么实现的。todo 不了解
Proxy类的代码量被固定下来,不会因为业务的逐渐庞大而庞大。
可以实现AOP编程,实际上静态代理也可以实现,总的来说,AOP可以算作是代理模式的一个典型应用。
解耦,通过参数就可以判断真实类,不需要事先实例化,更加灵活多变。
两个很大的问题(日常不使用动态代理的原因):
代理对象和被代理对象的关联就在于实现了同一个接口,一旦被代理对象没有实现接口就不能进行动态代理(cglib 可以为没有接口的对象创建代理对象)
写着麻烦,每有一个被代理对象就需要写一个代理类
因此出现了 AOP
注意:
- 代理对象调用的方法也要是 public 修饰符,否则方法中获取不到注入的 bean,会报空指针错误。
- AopContext.currentProxy ()可以获取到本类的代理对象
事务控制
链接 ====》 视频94——尚硅谷
链接 ====》spring注解开发-事务管理
链接 ====》[【金山文档】 尚硅谷第08章 声明式事务
事务控制基于xml
链接 ====》视频115——尚硅谷
@Transactional
链接 ====》 @Transactional注解详解-CSDN博客
几种属性
@Transactional(timeout=?)
设置事务处理超时时间为?秒,超时则会报异常 TransactionTimedOutException
@Transactional(readOnly=true)
默认是 false,使用场景:方法内都是查询方法,使用 readOnly 可以优化事务,提高运行速度。 如果方法内不全是查询方法使用 readOnly 属性则会报错:TransientDataAccessResourceException。
@Transactional(isolation={READ_CPMMITTED})
设置隔离级别
@Transactional(noRollbackFor={ArithmeticException.class})
- noRollbackFor:设置哪些异常出现可以不回滚
- rollbackFor:设置哪些异常出现必须回滚
运行时异常会回滚
编译期异常事务不会回滚
隔离级别
链接 ====》 视频104——尚硅谷
概念:
脏读(不允许发生):(读取到了没有提交的数据)
后者读到前者修改后的值后,前者将值回滚了,后者读到了没有意义数据。不可重复读:
在一个事务中多次读取同一个数据时,结果出现不一致。
- 幻读(被他人插入了新数据):
在一个事务中使用相同的 SQL 两次读取,第二次读取到了其他事务新插入的行。
各个隔离级别解决并发问题的能力 和 各种数据库产品对事务隔离级别的支持程度见下表
脏读 | 不可重复读 | 幻读 | Oracle | MySQL | |
---|---|---|---|---|---|
READ UNCOMMITTED 读未提交 | 有 | 有 | 有 | × | √ |
READ COMMITTED 读已提交 | 无 | 有 | 有 | √(默认) | √ |
REPEATABLE READ 可重复读 | 无 | 无 | 有 | × | √(默认) |
SERIALIZABLE 串行化 | 无 | 无 | 无 | √ | √ |
事务的传播行为
视频====》视频110~112——尚硅谷
- 开启新事务或者在原事务中运行
- 开启新事务,如果有事务就挂起原来事务
- 必须在当前事务中运行,否则不执行
- 必须在当前事务中运行,否则抛异常
- 不能在事务中运行,否则挂起原事务
- 不能在事务中运行,否则抛异常
- 需要在嵌套事务内部中执行,否则启动新事务执行。
- 默认值是第一种 required 和require_new
例子加深印象====》视频112——尚硅谷
required:事务的属性继承于大事务
视频====》视频113——尚硅谷注意:在@Transactional 注解的方法中,再调用本类中的其他方法 method2 时,那么 method2 方法上的@Transactional 注解是不!会!生!效!的!想要其生效,需要使用代理对象去调用本类方法
原因:
Jdbc
链接 5.1====》【金山文档】 尚硅谷 第07章 JdbcTemplate
链接 5.2====》视频84——尚硅谷
todo
原码分析
链接 6.1====》 视频96——尚硅谷
todo
参考
搞定!
人生无常,大肠包小肠!
继续学习吧!