# spring-study
**Repository Path**: zhu-guoming/spring-study
## Basic Information
- **Project Name**: spring-study
- **Description**: Spring案例和笔记
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2023-10-13
- **Last Updated**: 2023-11-16
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Spring核心框架
## 1、spring容器以及Bean的装配
创建maven项目,在pom.xml依赖
```xml
org.springframework
spring-context
5.3.23
```
依赖 `spring-context`(上下文) 会间接依赖 `aop`(面向切面编程)、`beans`(容器管理)、`core`(核心)、`expression`(表达式)
注意: 依赖spring-context的版本号,间接依赖的版本也是一样的
### 1.1、如何让实现类抛给`Spring`管理
让`Spring`管理实现类方式有两种方法 `xml` 和 `注解`
注解是`jdk1.5`提出的,但是 `spring` 是 `jdk1.4` 创建的,当时只能通过 `xml` 获取类
`beans`代表了被`Spring`容器管理的对象,在`Spring`中,对象称为`Bean`,通过`beans.xml`配置文件或注解的方式将对象定义为`Bean`。
为什么`Bean`管理的都是实现类呢?因为接口、抽象类不能实例化也就是不属于`Spring`管理内的对象,不能实例化的组件是不允许创建为 `Bean` (例如:接口、抽象类)
创建:在`resources`文件下创建`spring`核心配置文件
右键`resources`文件 `new` => `XML Configuration File` => `Spring Config` 命名 `beans.xml`
然后可以进行把实现类纳入`Spring` 的管理对象中,让`Spring`把你的实现类管理起来。
```xml
```
如何获取被`Spring`管理Bean对象呢?
```java
public static void main(String[] args) {
/* 创建容器工厂
(在spring框架中存在多种不同的容器工厂
每种容器工厂都有自身的特别结合功能,,列如
当我们需要通过解析xml配置文件初始化一个容器工厂时,
可以使用ClassPathXmlApplicationContext
这个容器工厂,而这些工厂最终的都是
ApplicationContext)
*/
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 从容器中根据Bean的id获取Bean的实例
UserService service1 = (UserService) context.getBean("userService");
// getBean方法第二个参数执行泛型,因此不需要强转
// UserService service2 = context.getBean("userService",UserService.class);
// 如果接口的实行类只有一个,可以不需要指定id,只需要泛型类型即可
// UserService service3 = context.getBean(UserService.class);
service1.say();
}
```
### 1.2、如何自定义工厂抛给`Spring`管理
然后可以进行把写好的自定义工厂纳入`Spring` 的管理对象中,让`Spring`把你的自定义工厂管理起来。
自定义工厂,也可以使用`spring`提供的 FactoryBean接口,实现此接口来创建自定义工厂(推荐)。
列如 StudentServiceFactoryBean 实现了 FactoryBean接口 ,当你从`spring`调用了StudentServiceFactoryBean
会有一个回调的方法getObject()返回的创建好的对象
```xml
```
```java
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 使用自定义工厂创建实例
UserService peopleService = (UserService) context.getBean("peopleService");
peopleService.say();
// 实现FactoryBean接口创建Bean对象
UserService studentService =(UserService) context.getBean("studentService");
studentService.say();
}
```
## 2、Bean的作用域
装配UserService ,scope属性指定Bean的创建方式,
`scope`支持的作用域范围:
` singleton`:单例,默认容器会为每一个Bean创建唯一的一个实例在容器管理,直到容器的销毁,Bean才会跟着销毁, 也是scope的默认值
`prototype`:原型,容器一开始并不会创建实例,而是当调用了getBean方法时才会根据class创建一个新的实例, 这个实例并不会纳入容器中,使用完之后直接丢弃, 因此,每次调用getBean方法时都会新建一个实例
`request`:请求作用域(需要集成在web环境中),与servlet中 的请求作用域保持一致,bean会在一次请求响应后就销毁
`session`:回话作用域(需要集成在web环境中),与servlet中 的请求作用域保持一致 ,当客户端关闭浏览器后, 销毁了会话id,服务端后台就会自动销毁这个Bean
除了以上常用的作用域,Spring还提供了其他作用域,如application(应用程序)作用域、websocket作用域等。
```xml
```
## 3、Bean的id以及name属性
id:属性表示Bean在容器的唯一标识,是不可重复的, 但同时还有另外一个name属性,用于指定bean在容器中的别名这个笔名是可以有多个的(别名之间可以使用逗号或者空格隔开,所以在获取bean的时候可以根据id也可以根据别名来获取。 注意:在指定name以后,可以不需要知道id,但是name的第一个名字就会自动作为作为id使用,其他的仍然是别名)
```xml
```
## 4、Bean生命周期
### 1.1、对象初始化的流程:
-> 父类的静态变量
-> 父类的静态代码块
-> 子类的静态变量
-> 子类的静态代码块
-> 父类的实例变量
-> 父类的实例代码块
-> 子类的实例变量
-> 子类的实例代码块
-> 父类的构造方法
-> 子类的构造方法
```java
public class User extends People{
/**
* 实例变量 (跟实例关联的,会随着对象的创建而创建,对象的销毁而销毁)
*/
String name = "user";
/**
* 静态变量 (也叫类变量,是跟类关联的,是在类加载时初始化)
*/
static String driver = "aaa";
/**
* 在创建对象的时候会执行一次(跟实例相关)
*/
{
System.out.println(name);
System.out.println("实例代码块");
}
/**
* 静态代码块,在类加载时会执行一次的代码段
*/
static {
System.out.println(driver);
System.out.println("静态代码块");
}
public User(){
System.out.println("执行构造方法");
}
public static void main(String[] args) {
User user = new User();
}
}
```
### 1.2、Bean的初始化流程
bean的初始化方法有两种方式(可以二选一):
1. 实现InitializingBean接口,接口包含一个afterPropertiesSet方法
2. 自定义初始化方法,并通过init-method属性来指定自定义的方法名即可。
`注意`:如果两种初始化方法同事存在,自定义方法是最后被执行
bean的销毁方法也有两种实现方式(可以二选一):
1. 实现DisposableBean接口,接口包含一个destroy方法
2. 自定义销毁方法,并通过destroy-method属性指定方法名即可
`重点`:spring管理bean的生命周期是针对单例的bean,通过原型创建的bean不会纳入spring容器中,因此不会执行声明周期的方法
```xml
```
```java
@Slf4j
public class UserService implements InitializingBean, DisposableBean {
public UserService() {
log.info("执行构造方法");
}
/**
* Bean的初始化方法,在构造方法后执行
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
log.info("Bean的初始化,执行afterPropertiesSet方法");
}
/**
* Bean初始化方法2,在构造方法后执行
*/
public void init(){
log.info("Bean初始化,执行自定义的初始化方法");
}
@Override
public void destroy() throws Exception {
log.info("Bean销毁前执行的方法,来自DisposableBean接口");
}
public void myDestroy(){
log.info("自定义Bean销毁前执行的方法");
}
}
```
### 1.3、Bean的后置处理器
Bean 的后置处理器,后置处理器的方法时时再调用初始化方法之前以及初始化方法执行完之后执行
`注意`:后置处理器的方法是针对所有的Bean对象,而不是单独的某一个Bean
```java
@Slf4j
public class MyBeanPostProcessor implements BeanPostProcessor {
/**
* 在Bean的初始化方法之前执行
* @param bean
* @param beanName
* @return
* @throws BeansException
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
log.info("执行postProcessBeforeInitialization方法");
return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
}
/**
* 在Bean的初始化方法之后执行
* @param bean
* @param beanName
* @return
* @throws BeansException
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("执行postProcessAfterInitialization方法");
return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
}
}
```
装配后置处理器,它会在所有Bean的初始化方法前后执行
`注意`:装配后置处理器用class绑定完整类名即可
```xml
```
总结:bean的生命周期流程
-> 执行对象的构造方法
-> 后置处理器的postProcessBeforeInitialization方法
-> InitializationBean接口afterPropertiesSet方法
-> 自定义的init-method方法
-> 后置处理器的postProcessAfterInitialization方法
-> DisposableBean接口destroy方法
-> 自定义的destroy-method方法
## 5、依赖注入的几种方式
什么是ioc(控制反转)?
就是让容器自动将需要的Bean注入到相关的类中完成装配的过程。
为什么需要控制反转,new 是强依赖,new 是写死了
导致`UserService` 依赖于 `UserDao` 这个底层模块,违反了依赖倒置原则,同时如果更换实现类就违反了开闭原则
```java
class UserService{
public void addUser(){
UserDao userDao = new UserDaoImpl();
}
}
```
依赖的顺序
```xml
```
### 实现IoC的两种方式
#### set方法
这样容器就会自动将bean通过set方法注入到UserService中
```xml
```
#### 构造方法
这样容器就会自动将bean通过构造方法注入到UserService中
```xml
```
## 6、依赖注入示例
### 值注入
IoC容器可以注入Bean,那也可以注入值
如下是使用 set 方法注入,也可以使用构造方法
set方式:
```xml
广州
珠海
12345678999
12345678911
```
构造方式:
```xml
list1
list2
set1
set2
```
## 7、注解结合xml配置
如果注解和xml结合,要在xml里配置扫描包:
```xml
```
### Dao层
可以使用`@Component("userDao")`但是在`spring`3.2不推荐是用此注解。
持久(Dao):`@Component`此注解由`@Repository`取代,但`@Component`也可以使用。
注解用于标识当前类为一个Bean,这样就会被spring容器扫描到,可以通过value属性来指定Bean的id,如果不指定value,默认的id就是当前类名并将首字母改为小写 列如:userDaoImpl
```java
// @Component("userDao")
@Repository("userDao")
@Slf4j
public class UserDaoImpl implements UserDao {
@Override
public void save() {
log.info("insert into user_info...");
}
}
```
### Service层
业务层(Service):`@Component`此注解由`@Service`取代,但`@Component`也可以使用。
```java
//@Component("userService")
@Service("userService")
@Slf4j
public class UserServiceImpl implements UserService {
private UserDao userDao;
/**
* 通过构造方法注入
* @param userDao
* 使用@Autowired注解进行注入
*/
@Autowired
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void addUser() {
log.info("业务逻辑...");
userDao.save();
}
}
```
### Controller层
控制层(Controller):`@Component`此注解由`@Controller取代,但`@Component`也可以使用。
```java
//@Component
@Controller
public class UserController {
private UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
public void add(){
userService.addUser();
}
}
```
## 8、Spring的@Autowired注解
`@Autowired`注解是`Spring`官方提供的注入注解,它可以声明在构造方法上,set方法上以及字段上。
字段注入:`spring`官方不推荐注解使用在字段上,优先推荐构造方法,然后到set方法。
注入规则:@Autowired注解默认是根据类型注入,(只有一个实现类)如果存在多个实现类的时候, 这是根据参数名称进行注入.如果存在多个实现类并且参数名称不匹配,则会引发异常,此时应该结合@Qualifier注解来指定要注入的bean,但是这个注解只可以用在普通的方法(set)上。也可以使用@Primary注解声明在某个实现类上,这样Spring就会优先注入这个实现类。
从Spring4.2版本开始,如果使用的是构造方法注入,可以不需要任何的注入注解,默认就按照类型注入
```java
@Controller
@Primary
public class UserController {
/**
* 字段注入(Spring不推荐从字段注入)
*/
// @Autowired
private UserService userService;
/**
* 构造方法注入实现类
* @param userService
*/
@Autowired
@Qualifier("userService")
public UserController(UserService userService) {
this.userService = userService;
}
/**
* set方法注入
* @param userService
*/
@Autowired
@Qualifier("userService")
public void setUserService(UserService aa) {
this.userService = aa;
}
public void add(){
userService.add();
}
}
```
## 9、JSR250注入注解
使用JSR250 (Java规范提案),它设计了@Resource注解来支持依赖注入Spring对这个注解也实现了支持,
需要注意的是这个注解只能用用在字段或者普通的set方法上,并不支持构造方法注入。默认也是按照类型注入
```java
@Controller
public class UserController {
private UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
/**
* name 属性指定需要注入的Bean的id
* @param userService
*/
@Resource(name = "userService")
public void setUserService(UserService userService) {
this.userService = userService;
}
public void addUser(){
userService.add();
}
}
```
## 10、JSR330注入注解
使用JSR330注解需要引用依赖
```xml
javax.inject
javax.inject
1
```
使用JSR330标准提供的@Inject注解实现依赖注入,但是这个注解并不存在JDK中,需要额外添加依赖。
这个注解同样支持字段,构造方法,set方法注入,并且默认也是按照类型注入,用法与@Autowried基本一致
当有多个实现类,并且方法参数与id不一致时,可以结合 @Named注解来指定bean的id又或者
可以使用@Primary注解设置注入的优先级
```java
@Controller
public class UserController {
private UserService userService;
/**
* @param aa
*/
@Inject
@Named("userService")
public UserController(UserService aa) {
this.userService = aa;
}
public void setUserService(UserService userService) {
this.userService = userService;
}
public void addUser(){
userService.add();
}
}
```
结合lombok了spring 4.2的新特性实现了更加简洁的注入 方式使用 @RequiredArgsConstructor注解,
lombok会自动添加一个带参的构造方法实现构造器的注入,注意:此时的字段必须是final修饰
```java
@Controller
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
public void addUser(){
userService.add();
}
}
```
## 11、策略模式注入所有的实现类
策略模式(多选一)使用场景类似,用户在买单的时候选择那种支付方式,列如:微信支付,支付宝支付,银行卡支付
支付接口,意味着有多种不同的实现
```java
public interface Payment {
/**
* 支付方法
* @param money
*/
void pay(BigDecimal money);
}
```
支付宝支付:实现Payment接口
```java
@Slf4j
@Service("Alibaba")
public class AliPayment implements Payment {
@Override
public void pay(BigDecimal money) {
log.info("支付宝支付金额" + money.doubleValue());
}
}
```
微信支付:实现Payment接口
```java
@Slf4j
@Service("WeChat")
public class WechatPayment implements Payment {
@Override
public void pay(BigDecimal money) {
log.info("微信支付金额" + money.doubleValue());
}
}
```
支付策略上下文:这里通过spring注入所有的策略实现类并完成具体的策略调用并且这个策略上下文也交给容器管理,便于将上下文注入到其他类中。(列如Controller)
```java
@Service
/**
* 利用lombok生成一个带参数的构造方法,
* 这样即可以通过构造方法直接注入
*/
@RequiredArgsConstructor
public class PaymentContext {
/**
* 注入一个map集合,spring会将Payment接口的所有实现类
* 一并保存到map中
* key为支付类型(bean的id),value是具体的支付策略实现
*/
private final Map paymentMap;
/**
* 根据支付类型选择具体的策略来完成支付
* @param paymentType
* @money 支付金额
*/
public void pay(String paymentType, BigDecimal money) {
Payment payment = paymentMap.get(paymentType);
payment.pay(money);
}
}
```
支付控制器
```java
@Controller
@RequiredArgsConstructor
public class PaymentController {
/**
* 注入策略上下文
*/
private final PaymentContext context;
public void pay(String type, BigDecimal money){
context.pay(type, money);
}
}
```
最后测试
```java
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
PaymentController controller = context.getBean(PaymentController.class);
controller.pay("WeChat", new BigDecimal("100.00"));
}
}
```
## 12、作用域代理
@Scope注解用于设置Bean的作用域,等效于xml中的scope属性当不指定value属性时,默认是单例,如果要使用原型就必须指定为prototype
注意!! 作用域prototype失效的情况:
当一个单例的Bean,注入一个原型的Bean的时候,原型会失效,因为在容器在初始化单例的Bean的为了正确注入实例,
会将需要注入的对象预先创建出来并注入到当前的单例Bean中,因此只要单例的Bean不销毁,被注入的这个对象也一并存在
```java
@Scope("prototype")
```
解决原型失效的问题:
当一个单例的Bean注入原型Bean的时候,如果想让原型生效那么可以设置proxyMode属性,这个属性就是代理作用域,
其原理是当初始化单例Bean的时候,并不会立即完成注入,而是将一个代理对象(暂时可理解为替身)设置到单例中,
当调用注入对象的方法时,此时替身会自动从原型容器中创建一个实例并返回,保证原型的有效
```java
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
```
## 13、spring配置类
### @Configuration
@Configuration注解用于标识一个类为合法的spring配置类
### @ComponentScan
@ComponentScan注解用于扫描指定的包,装配相关的Bean,等价于xml中的扫描
```java
@ComponentScan(basePackages = "指定扫描包的路径")
```
### @Bean
用于标识 Bean,而其中方法名代表 Bean 的 Id,返回的是对应的实现类
对应 xml配置文件 中的``
还可以通过name属性指定bean的别名,比如`@Bean(name = {"aa","bb"})`
案例:
```java
@Configuration
@ComponentScan(basePackages = "指定扫描包的路径")
public class AppConfig {
/**
* 除了使用@Component、@Service、@Controller
* @Respository注解来装配Bean以外,还可以使用
* @Bean注解在配置类中装配Bean,这种方式很类似
* 在xml中配置一个个的,
* 用了@Bean注解后默认标注的方法名就是Bean的id,
* 还可以通过name属性指定bean的别名
*
* @return
*/
//@Bean(name = {"aa","bb"})
@Bean
public UserService userService() {
return new UserServiceImpl();
}
/**
* 当需要注入其他Bean的时候,有两种方式
* 方式一:通过参数实现注入
* 方式二:通过调用方bean方法实现注入
*
* @Scope注解还可以声明在Bean方法上来设置Bean的作用域
* @return
*/
@Bean
@Scope("prototype")
public UserController userController() {
return new UserController(userService());
}
}
```
## 14、配置类的Lite模式和Full模式
配置类Lite模式(非代理模式)和Full模式(代理模式):
当配置类上标注了@Configuration注解时,并且proxyBeanMethods属性设置为 true,此时就是Full模式。Full模式就是spring会为当前配置类创建一个代理对象,从而代理配置类中所有@Bean注解的方法,这样每当调用配置类中的bean方法时,会从容器中进行检查Bean实例,并返回容器中存在的对象。
例如:当配置类上标注了@Configuration注解时,在userController()方法里注入userService()方法,而不是在配置类中调用,而是在spring容器中检查是否有@Bean注解,有的话就返回Bean实例。
```java
@Configuration
public class AppConfig {
/**
* 装配UserService
* @return
*/
@Bean
public UserService userService(){
return new UserServiceImpl();
}
/**
* 装配UserController并注入UserService
* @return
*/
@Bean
public UserController userController(){
// 得到需要注入的Bean
UserService userService = userService();
// 将Bean通过构造方法注入
return new UserController(userService);
}
}
```
反之就是Lite模式,在Lite模式配置类并不会被代理,每次调用Bean方法只是纯粹的调用,并不会经过代理。
例如:在AppConfig类中没有标注@Configuration注解时,也就是没有spring代理的支持(Lite模式),当你多次的调用Bean,会发现Bean的实例都会不一样
```java
public class AppConfig {
/**
* 装配UserService
* @return
*/
@Bean
public UserService userService(){
return new UserServiceImpl();
}
/**
* 装配UserController并注入UserService
* @return
*/
@Bean
public UserController userController(){
// 得到需要注入的Bean
UserService userService = userService();
// 将Bean通过构造方法注入
return new UserController(userService);
}
}
```
## 15、@import注解使用
@Import注解的三种用法:
1.使用@Import注解导入(装配)普通的Bean (不常用) 例如:@Import(UserController.class)
2.使用@Import注解导入其他的配置类(常用)例如在项目中可以模块化配置类,包括mvc的配置类、mybatis配置类、Redis配置类、RabbitMQ配置类等等,那么可以在一个总配置类中导入其他这些配置类进行合并,这样维护扩展性更强 例如: @Import({MvcConfig.class,MybatisConfig.class})
3.使用@Import注解实现选择性导入(即按照指定的逻辑来导入相关的类)(常用),这种方式需要自定义一个导入选择器交给spring执行例如:@Import({AnnoImportSelector.class})
```java
@Configuration
//@Import({UserController.class}) // 方式一
//@Import({MvcConfig.class,MybatisConfig.class}) // 方式二
@Import({AnnoImportSelector.class})
public class AppConfig {
}
```
使用@import导入自定义的选择器
自定义导入选择器,如果类上标注了@MyAnno的注解,就将纳入spring容器中管理
```java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnno {
}
```
AnnoImportSelector类实现了ImportSelector就会被spring认为把这个类当做自定义选择器
```java
public class AnnoImportSelector implements ImportSelector {
/**
* 自定义导入逻辑
* @param importingClassMetadata
* @return 返回值就是所有需要导入的类的完整类名
*/
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 创建一个集合保存带有注解的类的完整类名
List classNameList = new ArrayList<>();
// 解析类上是否存在@MyAnno注解
if (UserController.class.isAnnotationPresent(MyAnno.class)){
classNameList.add(UserController.class.getName());
}
return StringUtils.toStringArray(classNameList);
}
}
```
## 16、@PropertySource注解使用
将`properties`文件的属性注入到配置类的字段中由于`properties`文件时放在`resources`目录下,编译 之后会保存在`classpath`目录下,因此需要从`classpath:`路径中查找资源文件。
`@PropertySource`注解可以结合@Value注解来获取`properties`文件的key。
创建`properties`文件,在`resources`目录下`new` -> `Resource Bundle`
```properties
driver = com.mysql.cj.jdbc.Driver
url = jdbc:mysql://localhost:3306/mybatis_test
user = "root"
password = 123456
```
### @Value字段注入
使用`@Value`注解结合`spel`表达式进行值注入,也就是将`properties`文件中的属性值注入到当前的字段中,`spel`表达式中对应的是`properties`文件中的`key`。
案例:
```java
@Configuration
@PropertySource("classpath:properties文件名称")
@Slf4j
public class AppConfig {
@Value("${driver}")
private String driver;
@Value("${url}")
private String url;
@Value("${user}")
private String userName;
@Value("${password}")
private String password;
@Bean
public Connection connection() {
log.info(driver);
log.info(url);
log.info(userName);
log.info(password);
try {
return DriverManager.getConnection(url,userName,password);
} catch (SQLException e) {
throw new RuntimeException("Get connection error", e);
}
}
}
```
## 17、JDK和CGLIB动态代理
代理模式:
静态代理 --> 代理对象在编译期就确定了,需要编写代理类
动态代理 --> 在运行时动态创建代理对象,不需要编写代理类
### 概念
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。

使用代理后:

### 生活中的代理
- 广告商找大明星拍广告需要经过经纪人
- 合作伙伴找大老板谈合作要约见面时间需要经过秘书
- 房产中介是买卖双方的代理
### 相关术语
- 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
- 目标:被代理“套用”了非核心逻辑代码的类、对象、方法。
### 静态代理
创建静态代理类:
````java
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("[日志] add 方法开始了,参数是:" + i + "," + j);
// 通过目标对象来实现核心业务逻辑
int addResult = target.add(i, j);
System.out.println("[日志] add 方法结束了,结果是:" + addResult);
return addResult;
}
}
````
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。
就拿日志功能来 说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。
提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理 类来实现。这就需要使用动态代理技术了。
### 动态代理:
#### JDK动态代理
目标对象
```java
@Slf4j
public class UserServiceImpl implements UserService {
@Override
public void add() {
log.info("添加用户信息.....");
}
}
```
动态代理执行的回调处理器,回调处理器必须实现InvocationHandler接口:
````java
@Slf4j
public class UserServiceInvocationHandler implements InvocationHandler {
/**
* 目标对象(被代理的对象)
*/
private Object target;
/**
* 通过构造方法传入目标对象
* @param target
*/
public UserServiceInvocationHandler(Object target) {
this.target = target;
}
/**
* 核心的回调方法,目的是负责调用目标对象的行为,
* 这样可以在调用目标方法前后额外执行一些增强的逻辑
* @param proxy 由jdk动态创建出来的代理对象
* @param method 目标对象的具体方法
* @param args 目标对象方法所需要的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 调用前增强
before();
// 反射调用目标对象的方法
// target就是目标对象的具体实例
// args是方法需要的参数
// returnVal是目标 方法的返回值,如果没有返回值则是null
Object returnVal = method.invoke(target, args);
// 调用后增强
after();
return returnVal;
}
private void before() {
log.info("目标方法调用前...");
}
private void after() {
log.info("目标方法调用后...");
}
}
````
测试:
JDK动态代理需要实现一个InvocationHandler接口 ,并且结合proxy代理生成工具来动态创建代理对象。
注意:JDK的动态代理要求目标对象一定要有实现接口,否则无法创建代理对象,因为JDK动态代理是基于接口来生成代理对象
````java
public static void main(String[] args) {
//创建目标对象(被代理的对象)
UserService service = new UserServiceImpl();
// 创建回调处理器
InvocationHandler handler = new UserServiceInvocationHandler(service);
// 可以通过当前类获取一个类加载器
ClassLoader loader = Main.class.getClassLoader();
// 获取目标对象实现的所有接口的Class
Class[] interfaces = UserServiceImpl.class.getInterfaces();
// 动态创建代理对象,通过jdk提供的proxy类来动态创建代理对象
// newProxyInstance方法需要提供三个参数
// 参数1: 需要提供一个类加载去加载动态创建出来的代理字节码
// 从而实例化一个代理对象
// 参数2: 目标对象实现的所有接口的Class,因为JDK动态代理
// 是一定根据接口来创建一个代理对象,这个代理对象
// 会自动实现这些接口
// 参数3: 自定义的回调处理器
// 返回的就是已经创建好的对象
UserService proxy =(UserService) Proxy.newProxyInstance(loader,interfaces,handler);
// 调用代理对象的任何方法,都会去调用回调处理器的invoke方法
// 来完成代理的调用
proxy.add();
}
````
#### Cglib动态代理
目标对象
````java
public class UserService {
public void add(){
log.info("添加用户");
}
}
````
方法拦截器用于调用目标对象的方法
````java
/***
* cglib要求编写一个方法拦截器用于调用目标对象的方法,
* 本质上和jdk的InvocationHandler的作用是一样的。
* 需要实现MethodInterceptor接口
*/
public class UserServiceMethodInterceptor implements MethodInterceptor {
/**
* 回调处理方法(等同于InvocationHandler的invoke方法)
* @param proxy 运行时创建的代理对象(其实就是目标对象的子类)
* @param method 目标对象的方法
* @param args 目标对象方法所需要的参数
* @param methodProxy 代理对象的方法
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
log.info("调用目标对象方法之前");
//注意:这里应该调用的是子类的代理方法
//method则是父类中被代理的方法
Object returnVal = methodProxy.invokeSuper(proxy, args);
log.info("调用目标对象方法之后");
return returnVal;
}
}
````
测试
````java
public class Main {
public static void main(String[] args) {
//创建代理生成器
Enhancer enhancer = new Enhancer();
//要告诉代理生成器需要代理的父类
enhancer.setSuperclass(UserService.class);
//设置方法拦截器(回调处理器)
enhancer.setCallback(new UserServiceMethodInterceptor());
//创建代理对象(运行时动态创建的子类对象就是代理对象
UserService userService = (UserService) enhancer.create();
System.out.println("userService = " + userService);
}
}
````
JDK动态代理和Cglib动态代理的区别:
实现JDK动态代理时,目标对象必须要实现接口,代理对象是基于目标对象实现的接口而生成的代理对象。
而Cglib动态代理的目标对象不需要实现接口,它是基于继承来实现代理对象,是在目标对象下生成一个子类,通过目标对象代理出来的子类对父类进行功能的增强。
#### 使用JDK动态代理写连接池
连接池对象:
```java
@Setter
public class ConnectionPool {
/**
* 连接池(存放连接的集合)
*
*/
private LinkedList pool = new LinkedList<>();
/**
* 连接属性
*/
private String url;
private String user;
private String password;
/**
* 连接池的大小
*/
private Integer poolSize;
/**
* 在构造方法中初始化连接池的大小
*/
public void init() {
for (int i = 0; i < poolSize; i++) {
try {
//1.从数据库获取连接对象
Connection conn = DriverManager.getConnection(url, user, password);
//2.对连接对象创建代理
conn = createProxy(conn);
//3.将代理过的连接对象保存到池中
pool.add(conn);
} catch (SQLException e) {
throw new RuntimeException("Init connection pool error.", e);
}
}
}
/**
* 为连接对象创建代理
* @param target
* @return
*/
private Connection createProxy(Connection target) {
// 创建回调处理器
InvocationHandler handler = new ConnectionInvocationHandler(target, pool);
// 获取类加载器
ClassLoader loader = ConnectionPool.class.getClassLoader();
// 获取连接对象的所有接口, Connection本身是个接口
Class>[] interfaces = new Class[]{Connection.class};
// 返回创建代理
return (Connection) Proxy.newProxyInstance(loader, interfaces, handler);
}
/**
* 从池里面获取代理连接
* @return
*/
public Connection getConnection() {
return pool.removeFirst();
}
/**
* 查看连接池的大小
* @return
*/
public int size() {
return pool.size();
}
}
```
动态代理执行的回调处理器:
```
public class ConnectionInvocationHandler implements InvocationHandler {
/**
* 目标对象(被代理的连接对象)
*/
private Connection target;
private LinkedList pool;
/**
* 通过构造方法传入目标对象
* @param target
*/
public ConnectionInvocationHandler(Connection target, LinkedList pool) {
this.target = target;
this.pool = pool;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 如果当前调用的是Connection的close方法则将它放回到池中
if ("close".equals(method.getName())) {
// 从池的尾部放回去
// 注意:这里放回池中的必须是代理对象,而不是目标Connection
pool.addLast((Connection) proxy);
return null;
} else {
// 除close以外的其他方法则正常调用目标的行为
return method.invoke(target, args);
}
}
}
```
properties文件:
```properties
url = jdbc:mysql://localhost:3306/mybatis_test
user = root
password = 123456
poolSize = 5
```
配置类:
```java
@Configuration
@PropertySource("classpath:db.properties")
@Slf4j
public class AppConfig {
@Value("${url}")
private String url;
@Value("${user}")
private String user;
@Value("${password}")
private String password;
@Value("${poolSize}")
private Integer poolSize;
/**
* 装配连接池
* @return
*/
@Bean
public ConnectionPool connectionPool() {
log.info("user:" + user);
log.info("url:" + url);
log.info("pass:" + password);
ConnectionPool pool = new ConnectionPool();
pool.setUrl(url);
pool.setPassword(password);
pool.setPoolSize(poolSize);
pool.setUser(user);
// 初始化连接池
pool.init();
return pool;
}
}
```
测试:
```java
@Slf4j
public class Main {
public static void main(String[] args) throws SQLException {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
ConnectionPool pool = context.getBean(ConnectionPool.class);
log.info("连接数:" + pool.size());
Connection conn1 = pool.getConnection();
Connection conn2 = pool.getConnection();
log.info("连接数:" + pool.size());
conn1.close();
log.info("连接数:" + pool.size());
}
}
```
## 18.Spring AOP xml配置示例一
定义一个切面类,将需要增强的逻辑方法编写在这个类中。
这些增强的逻辑方法专业术语叫做增强或者是通知。
增强的类型一共有五种(前置通知,后置通知,环绕通知,异常通知,最终通知)
前置通知:需要实现MethodBeforeAdvice接口
后置通知:需要实现AfterReturningAdvice接口
环绕通知:需要实现AfterReturningAdvice接口
异常通知:需要实现ThrowsAdvice接口
```java
@Slf4j
public class ServiceAspect implements MethodBeforeAdvice,
AfterReturningAdvice,MethodInterceptor, ThrowsAdvice {
/**
* 前置通知
* @param method 准备调用的目标对象方法
* @param args 目标对象方法所需要的参数
* @param target 目标对象(被代理的对象)
* @throws Throwable
*/
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
log.info("前置通知...");
}
/**
* 后置通知
* @param returnValue 目标方法的返回值
* @param method 目标对象的方法
* @param args 目标方法所需要的参数
* @param target 目标对象
* @throws Throwable
*/
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
log.info("后置通知...");
}
/**
* 环绕通知
* @param invocation 回调处理器,用于调用目标对象的方法
* @return
* @throws Throwable
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("环绕通知前...");
// 调用目标对象方法
Object returnVal = invocation.proceed();
log.info("环绕通知后...");
return returnVal;
}
/**
* 异常通知,根据官方文档说明
* 该方法名必须叫做afterThrowing,
* 并且必须包含一个Exception参数
* @param e
*/
public void afterThrowing(Exception e) {
log.info("异常通知:" + e);
}
}
```
代理对象:
```java
@Slf4j
public class UserServiceImpl implements UserService {
@Override
public void add() {
log.info("添加用户");
throw new RuntimeException("自定义异常");
}
@Override
public void delete() {
log.info("删除用户");
}
}
```
```java
@Slf4j
public class StuService {
public void add() {
log.info("添加学生信息");
}
}
```
最后xml配置:
```xml
edu.nf.ch18.service.UserService
serviceAspect
serviceAspect
```
测试:
```java
@Slf4j
public class Main {
public static void main(String[] args) {
// 创建容器工厂
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
// 从容器中获取代理对象
// UserService service = context.getBean("userServiceProxy", UserService.class);
// service.add();
// log.info("-----------------------");
// service.delete();
StuService stuService = context.getBean("stuServiceProxy", StuService.class);
stuService.add();
}
}
```
## 19.Spring AOP xml配置示例二
使用Aspect依赖
```xml
org.aspectj
aspectjweaver
1.9.8
```
定义切面类同上
代理对象同上!!
xml配置:
```java
```
最后测试同 `18.Spring AOP xml配置示例一`
## 20.Spring AOP xml配置示例三
使用Aspect依赖同上!!
定义切面类同上然后再加一个最终方法:
```java
/**
* 最终通知
* 不管有没有异常产生最终通知都会被执行
* @param jp 连接点
*/
public void after(JoinPoint jp) {
log.info("最终通知");
}
```
代理对象同上!!
xml配置:
```xml
```
最后测试同 `18.Spring AOP xml配置示例一`
## 21.Spring AOP 注解结合配置类示例
定义一个切面类 `18.Spring AOP xml配置示例一`
定义切面类同上然后再加一个最终方法:
```java
/**
* 最终通知
* 不管有没有异常产生最终通知都会被执行
* @param jp 连接点
*/
public void after(JoinPoint jp) {
log.info("最终通知");
}
```
代理对象同上!!
配置类:
```java
@Configuration
@ComponentScan(basePackages = "edu.nf.ch21")
// 启用AspectJ注解处理器
// proxyTargetClass指定为true表示强制使用CGLIB生成代理
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
}
```
最后测试
## 22.Aware接口
Aware接口(感知型接口),当spring容器发现某个Bean实现了Aware接口后,name就会为这个Bean注入一些容器核心对象。比如某些业务场景中需要的到Bean的名字或者id是,可以通过此接口来获取。
```java
@Service
@Slf4j
public class UserService implements BeanNameAware {
/**
* 注入bean的名字
*/
private String beanName;
/**
* 容器会通过这个set方法将bean的名称注入进来
* @param beanName
*/
@Override
public void setBeanName(String beanName) {
this.beanName = beanName;
}
public void add() {
log.info("添加用户,使用beanName:" + beanName);
}
}
```
配置类:
```java
@Configuration
@ComponentScan(basePackages = "edu.nf.ch22")
public class AppConfig {
}
```
获取ApplicationContext实现ApplicationContextAware接口,这样spring容器将感知到当前的Bean需要注入容器对象
```java
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
/**
* 声明容器
*/
private static ApplicationContext applicationContext;
/**
* 通过set方法将容器本身给注入进来
* @param appContext
* @throws BeansException
*/
@Override
public void setApplicationContext(ApplicationContext appContext) throws BeansException {
applicationContext = appContext;
}
public static ApplicationContext getApplicationContext(){
return applicationContext;
}
/**
* 封装getBean方法
* @param id
* @param clazz
* @return
* @param
*/
public static T getBean(String id, Class clazz) {
return applicationContext.getBean(id, clazz);
}
public static T getBean(Class clazz) {
return applicationContext.getBean(clazz);
}
}
```
调用ApplicationContextHolder.getBean(指定需要感知的类)
```java
@Slf4j
@Service
public class StuService {
public void add() {
// 直接使用
UserService service = ApplicationContextHolder.getBean(UserService.class);
service.add();
}
}
```
测试:
```java
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
StuService service = context.getBean(StuService.class);
service.add();
}
```
## 23.容器事件
自定义事件对象,这个对象用于发布给spring容器,容器就会自动处理这个事件。
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyEvent {
/**
* 事件消息
*/
private String message;
}
```
自定义事件监听器,用于监听用户发布事件并进行处理,监听器需要纳入容器管理。
```java
@Component
@Slf4j
public class MyEventListener {
/**
* 自定义事件监听方法,容器会将用户发布的
* 事件对象传入这个方法中进行时间处理
*
* @EventListener用于标识当前方法为监听方法
* @param event
*/
@EventListener
public void handlerEvent(MyEvent event) {
log.info("处理事件:" + event.getMessage());
}
}
```
配置类:
```java
@Configuration
@ComponentScan(basePackages = "edu.nf.ch23")
public class AppConfig {
}
```
测试:
```java
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 创建事件对象
MyEvent event = new MyEvent("hello world");
// 向容器发布事件
context.publishEvent(event);
}
```
## 24.定时任务
配置类:
```java
@Configuration
@ComponentScan(basePackages = "edu.nf.ch24")
//启用定时任务注解处理器
@EnableScheduling
public class AppConfig implements SchedulingConfigurer {
/**
* 注册自定义的定时任务线程池
* @param taskRegistrar
*/
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
//注册定时任务线程池
taskRegistrar.setTaskScheduler(taskScheduler());
}
/**
* 装配一个自定义的定时任务线程池
* @return
*/
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
//创建定时任务线程池
ThreadPoolTaskScheduler poolTaskScheduler = new ThreadPoolTaskScheduler();
//设置池的线程大小
poolTaskScheduler.setPoolSize(10);
//设置线程名称的前缀
poolTaskScheduler.setThreadNamePrefix("任务线程-");
return poolTaskScheduler;
}
}
```
处理订单业务定时任务:
```java
@Service
@Slf4j
public class OrderService {
public void backup() {
log.info("备份订单");
}
}
```
订单定时器,定时触发订单的备份逻辑
```java
@Component
@RequiredArgsConstructor
public class OrderTask {
private final OrderService service;
// 使用@Scheduled注解标注当前方法为一个定时任务方法
// 并且使用cron表达式来设定执行的时间每5秒
@Scheduled(cron = "0/5 * * * * ?")
public void executeBackup() {
service.backup();
}
}
```
测试:
```java
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
while(true){}
}
```