Spring框架
一、spring的介绍
1、Spring概述
Spring是一个生态体系(常被称为 Spring 全家桶),是一系列用于 Java 应用开发的框架和库的集合。它包含了Spring Framework、Spring MVC、Spring Boot、Spring Data、Spring Cloud(Spring Integration、Spring Batch、Spring Security等)。
Spring Framework 是 Spring 生态系统的基础和核心部分,我们常提到的IOC、AOP等概念实际上来源于Spring Framework。
而 Spring Boot、Spring Cloud 等则是围绕 Spring Framework 构建的,用于提升开发效率和方便管理的工具和框架。
2、Spring Framework
Spring基础框架,基本上任何其他Spring项目都是以Spring Framework为基础的。
2.1 特性: 非侵入式、控制反转、面向切面编程、容器、组件化、声明式、一站式
2.2 五大功能模块
功能模块 | 功能介绍 |
---|---|
Core Container | 核心容器,管理对象的创建和依赖注入 |
AOP&Aspects | 面向切面编程 |
Transaction | 提供声明式和编程式事务管理 |
Data Access/Integration | 提供数据访问/集成简化数据库操作 |
Web | 提供了面向Web应用程序的集成功能 |
2.3 解决的问题
- 主要解决了创建对象、管理对象的问题。我们可以把对象交给spring管理,我们主要关注于业务逻辑,其他的都交给spring。
- 同时解决java带来的繁重复杂的引用,使其轻量化
Spring的核心功能:控制反转(IOC)和面向切面(AOP);
Spring的本质核心:非常大的容器工厂
二、IOC(控制反转)
1、IOC
IOC即"控制反转",它是一种设计思想而非一个技术实现。描述了Java开发领域对象的创建以及管理的问题。通过Spring来管理对象的创建、配置和生命周期,这样相当于把控制权交给了Spring,不需要人工来管理对象之间复杂的依赖关系,这样做的好处就是解耦。
- 传统的开发方式:手动通过new关键字来创建对象,例如 classA aa = new classA();
- IOC思想的开发方式:不需要手动new和管理对象,而是通过IOC容器(Spring框架)来获取所需的对象。容器负责确保正确的对象在正确的时间被创建和注入,我们需要哪个对象,直接从IOC容器里面拿。
较传统来看,区别:将创建、管理对象的权利交给IOC容器(失去控制权),不用再考虑对象的创建、管理等一系列的事情(得到好处)
- 控制了什么:控制了对象(创建、实例化、管理对象的权利)
- 反转了什么:反转了控制权,控制权交给外部环境(Spring框架、IOC容器)
2、IOC思想
“你不用来找我,我来找你”
当要查找bean的时候,不是由我们主动地去容器查找,而是由容器主动将依赖注入我们
就是将组件对象的控制权交给容器,让容器自动将需要的Bean注入到相关的类中完成装配的过程
3、IOC容器
IOC容器的核心职责是负责对象的管理以及对象之间的依赖。分开来理解就是:容器负责对象的创建和管理,IOC则是完成对象之间的依赖注入。依赖注入的前提是要有容器的支撑,因为任何需要注入的对象都必须从容器中获取,则实现的第一步是先编写一个管理对象容器。
容器本身是指一个管理Bean对象的大工厂,控制反转就是实现依赖倒置原则的一种有效手段,两者结合,就是IOC容器 ( 控制反转的容器就是IOC容器 )
IOC容器没有IOC的加持就是一个普通的容器,不能实现控制反转和依赖倒置
4、为什么要使用IOC?
作用:减低计算机代码之间的耦合度
传统的由控制层调用业务层,业务层调用dao层的方式是直接依赖(强耦合)
这违反了开闭原则和依赖倒置原则
5、IOC的实现方式
5.1 依赖查找
高层模块自己去低层模块去查找(主动)
示例代码
// 在这里要在控制层创建业务层组件,这就是主动查询
public class UserController {
public String add(){
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 创建业务层组件
UserService service = context.getBean(UserService.class);
service.addUser();
return "success";
}
}
缺点:工厂创建的过多,主动从容器中查找依赖
解决-》依赖注入
5.2 依赖注入 (DI)
我们不需要去创建对象,容器会自动将对象注入
将低层模块的依赖注入到高层模块,高层模块放到IOC容器管理,由IOC容器给对象注入相应的依赖
方法一:Setter方法注入
示例代码
public class UserController {
/**
* 声明一个UserService接口
*/
private UserService userService;
/**
* 提供一个set方法专门用于提供给容器进行注入使用的
* @param userService
*/
public void setUserService(UserService userService){
this.userService = userService;
}
public String add(){
// 执行业务处理
userService.addUser();
return "success";
}
}
xml配置
<!-- 装配UserController -->
<bean id="userController" class="edu.nf.ch05.controller.UserController">
<!-- 注入UserService,
name对应的是set方法去掉set然后再将下一个首字母改为小写的名称,
例如setUserService对应的name名称就是userService
ref属性引用需要注入的bean的id, 这里就是上面装配的userService
这样容器就会自动将UserServiceImpl这个bean
通过set方法注入到UserController中
-->
<property name="userService" ref="userService"/>
</bean>
<!-- 装配UserService -->
<bean id="userService" class="edu.nf.ch05.service.impl.UserServiceImpl">
<!-- 同理,service也是使用同样的方式注入UserDao -->
<property name="userDao" ref="userDao"/>
</bean>
main函数
public class Main {
public static void main(String[] args) {
// 创建容器工厂
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 从容器中根据Bean的ID获取bean的实例
UserController controller = context.getBean(UserController.class);
controller.add();
}
}
方法二:构造器注入
示例代码
public class UserServiceImpl implements UserService {
/**
* 声明一个UserDao接口
*/
private UserDao userDao;
/**
* 通过构造方法注入具体的实现类
*/
public UserServiceImpl(UserDao userDao){
this.userDao = userDao;
}
@Override
public void addUser() {
userDao.save();
}
}
xml配置
<!-- 装配UserService -->
<bean id="userService" class="edu.nf.ch05.service.impl.UserServiceImpl">
<!--也可以通过构造方法注入,这里的name,对应的是构造方法的参数名 -->
<constructor-arg name="userDao" ref="userDao"/>
</bean>
案例:实体类的依赖注入(值注入)
一般不使用,因为实例类是不固定的
示例代码
@Data
public class User {
private String userName;
private Integer age;
private List<String> address;
private Set<String> tels;
private Map<String,String> card;
}
xml配置
<!-- 装配User -->
<bean id="user" class="edu.nf.ch05.entity.User">
<!-- 值类型的注入 -->
<property name="userName" value="simi"/>
<property name="age" value="17"/>
<!-- 注入list集合 -->
<property name="address">
<list>
<value>珠海</value>
<value>广州</value>
<value>深圳</value>
</list>
</property>
<!-- 注入Set集合 -->
<property name="tels">
<set>
<value>12432532525</value>
<value>35253332525</value>
<value>14646675525</value>
</set>
</property>
<!-- 注入Map集合 -->
<property name="card">
<map>
<entry key="工行" value="356375346353536"/>
<entry key="建行" value="435279834635532"/>
</map>
</property>
</bean>
测试实现
@Slf4j
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
User user = context.getBean(User.class);
log.info("姓名:" + user.getUserName());
log.info("年龄:" + user.getAge());
user.getTels().forEach(log::info);
user.getAddress().forEach(log::info);
user.getCard().forEach((k,v)->log.info(k+":"+v));
}
}
6、什么是Bean对象?
- 被spring管理的对象我们称之为Bean对象
- 不能够用new 实例化的组件,都不能称之为Bean(例如:接口、抽象类)
- 我们可以通过配置xml、注解去配置Bean对象,让spring去管理Bean对象。
三、Spring的应用
添加Spring核心依赖
<!-- 添加Spring核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.23</version>
</dependency>
spring-context依赖下关联了4个依赖:
spring-aop
spring-beans
spring-core
spring-expression
四、Bean的装配
1、添加spring文件
不需要配置模版,idea里面有,直接添加
2、创建xml文件
装配各种bean对象,用于创建实例,下面都以beans.xml命名
3、编写beans.xml文件
接口是不能够装配的
示例代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- id表示 当前装配的Bean在容器中的唯一标识
class指定Bean的完整类名,spring会使用这个完整类名进行反射
创建实例纳入容器中管理
-->
<bean id="userService" class="edu.nf.ch01.impl.UserServiceImpl"/>
<!-- 相当于
Object instance = Class.forName("edu.nf.ch01.impl.UserServiceImpl").instance();
Map<String,Object> container = new hashmap();
container.put("userService",instance);
-->
</beans>
4、创建容器工厂
在spring框架中存在多种不同容器工厂,每种容器工厂都有自身的特点和功能
/**
* 例如:当我们需要通过解析xml配置文件来初始化一个容器工厂时,可以使用
* ClassPathXmlApplicationContext这个容器工厂,而这些工厂最终实现的都是
* ApplicationContext接口)
*/
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
从容器中获取bean的实例的方式有:
-
方法一:根据Bean的id获取
UserService service = (UserService)context.getBean("userService");
-
方法二:根据id和类型
UserService service1 = context.getBean("userService", UserService.class);
-
方法三:根据类型
必须保证接口的实现类只有一个,那么可以不需要知道ID,只需要泛型类型即可
UserService service2 = context.getBean(UserService.class);
案例
第一:编写service接口和实现类
/**
* 不能实例化的组件是不允许创建为Bean(例如:接口、抽象类)
* @Date 2023-09-22
* @Author hy
*/
public interface UserService {
void say();
}
/**
* 实现类是普通的类,因此可以创建实例纳入Spring容器中管理
* @Date 2023-09-22
* @Author hy
*/
public class UserServiceImpl implements UserService {
@Override
public void say() {
System.out.println("say hello");
}
}
第二:编写beans.xml文件
<bean id="userService" class="edu.nf.ch01.impl.UserServiceImpl"/>
第三:测试
public static void main(String[] args) {
// 创建容器工厂
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 从容器中根据Bean的ID获取bean的实例
UserService service = (UserService)context.getBean("userService");
service.say();
}
五、装配自定义工厂
整合第三方的接口是自定义工厂Bean - FactoryBean这个接口
1、使用spring将编写的工具类纳入容器管理
示例代码
/**
* 自定义工厂,并交给Spring容器管理
* @Date 2023-09-22
* @Author hy
*/
public class PeopleServiceFactory {
/**
* 简单的工厂,用来创建PeopleServiceImpl实例
* @return
*/
public UserService create(){
return new PeopleServiceImpl();
}
}
xml配置
<!-- 装配自定义工厂方式一,spring会将我们编写的工厂类纳入容器管理 -->
<bean id="peopleFactoryBean"
class="edu.nf.ch01.factory.PeopleServiceFactory"/>
<!-- 告诉spring使用我们自定义的工厂来创建实例
factory-bean 引用上面自定义工厂的id,
factory-method 指定工厂中方法名 -->
<bean id="peopleService"
factory-bean="peopleFactoryBean"
factory-method="create"/>
案例1
第一:编写接口的实现类
public class PeopleServiceImpl implements UserService {
@Override
public void say() {
System.out.println("hello,people");
}
}
第二:定义自定义工厂(纳入容器管理)
public class PeopleServiceFactory {
public UserService create(){
return new PeopleServiceImpl();
}
}
第三:编写beans.xml文件
<bean id="peopleFactoryBean" class="edu.nf.ch01.factory.PeopleServiceFactory"/>
<!--使用我们自定义的工厂-->
<bean id="peopleService"
factory-bean="peopleFactoryBean"
factory-method="create"/>
第四:测试
public static void main(String[] args) {
// 使用自定义工厂创建实例
UserService peopleService = (UserService) context.getBean("peopleService");
peopleService.say();
}
2、实现FactoryBean接口 (推荐)
示例代码
/**
* 自定义工厂,实现FactoryBean接口
* @Date 2023-09-22
* @Author hy
*/
public class StudentServiceFactoryBean implements FactoryBean<UserService> {
/**
* 创建具体的Bean实例
* @return
* @throws Exception
*/
@Override
public UserService getObject() throws Exception {
return new StudentServiceImpl();
}
/**
* 返回Bean实例的Class类型
* @return
*/
@Override
public Class<?> getObjectType() {
return StudentServiceImpl.class;
}
}
xml配置
<!-- 当我们从容器中获取studentService这个bean的时候,
spring会调用StudentServiceFactoryBean的
getObject方法返回创建好的对象
-->
<bean id="studentService"
class="edu.nf.ch01.factory.StudentServiceFactoryBean"/>
案例2
第一:编写接口的实现类
public class StudentServiceImpl implements UserService {
@Override
public void say() {
System.out.println("hello,student");
}
}
第二:定义自定义工厂 (实现FactoryBean接口)
自定义工厂和xml配置是通用的,现阶段都能装配,后面xml会死掉,FactoryBean将会代替xml配置
public class StudentServiceFactoryBean implements FactoryBean<UserService> {
/**
* 创建具体的Bean实例
* @return
* @throws Exception
*/
@Override
public UserService getObject() throws Exception {
return new StudentServiceImpl();
}
/**
* 返回Bean实例的Class类型
* @return
*/
@Override
public Class<?> getObjectType() {
return StudentServiceImpl.class;
}
}
第三:编写beans.xml文件
<bean id="studentService" class="edu.nf.ch01.factory.StudentServiceFactoryBean"/>
第四:测试
public static void main(String[] args) {
// 实现FactoryBean接口创建Bean对象
UserService studentService = (UserService)context.getBean("studentService");
studentService.say();
}
六、Bean的作用域
1、Bean的作用域范围
(4种)
-
singleton: 单例,默认容器会为每一个Bean创建唯一的一个实例在容器中管理,
直到容器的销毁, Bean才会跟着销毁,
也是scope的默认值
-
prototype:原型,容器一开始并不会创建实例(只有Class对象),而是
当调用了getBean方法时才会根据Class创建一个新的实例,
这个实例并不会纳入容器中,使用完之后直接丢弃,
因此每次调用getBean方法时都会新建一个实例
(原型是一次性的,用完即毁)
-
request: 请求作用域(需要集成在web环境中),与servlet中的请求作用域保持一致,
bean会在一次请求响应后就销毁
-
session: 会话作用域(需要基础在web环境中),与servlet中的会话作用域保持一致,
当客户端关闭浏览器后,销毁了会话id, 服务端后就会自动销毁这个Bean
重点:
spring管理bean的生命周期是针对单例的bean, 通过原型创建的bean不会纳入spring容器中,
因此不会执行生命周期的方法
2、xml的配置
<!-- 装配UserService,scope属性指定 支持的Bean作用域范围-->
<bean id="userService"
class="edu.nf.ch02.service.UserService"
scope="singleton"/>
测试实现
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService service1 = context.getBean(UserService.class);
UserService service2 = context.getBean(UserService.class);
log.debug("Bean:" + service1);
log.debug("Bean:" + service2);
// 当 scope设置为“singleton”时service1 == service2;
// 当 scope设置为“prototype”时service1 != service2;
}
七、Bean的id和name属性
1、id属性
id属性表示Bean在容器的唯一标识,是不可重复的
<!-- bean.xml文件 -->
<bean id="userService"
class="edu.nf.ch02.service.UserService"
scope="prototype"/>
2、name属性
name属性,用于指定bean在容器中的别名
这个别名是可以有多个的(别名之间可以使用逗号或者空格隔开),
在获取bean的时候可以根据id也可以根据别名来获取。
在指定name以后, 可以不需要指定id, 但是name的第一个 名字就会自动作为id使用, 其他的仍然是别名
<!-- bean.xml文件 -->
<bean name="o1,o2,o3"
class="edu.nf.ch03.service.OrderService"/>
测试实现
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
OrderService service = context.getBean("o1", OrderService.class);
service.placeOrder();
}
八、JVM的对象创建以及初始化
->父类的静态变量
->父类的静态代码块
->子类的静态变量
->子类的静态代码块
->父类的实例变量
->父类的实例代码块
->子类的实例变量
->子类的实例代码块
->父类的构造方法
->子类的构造方法
class类中各方法的执行顺序:
静态代码块(运行时只执行一次) > 实例代码块 > 构造方法
示例代码
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 u1 = new User();
System.out.println("-----------");
User u2 = new User();
}
运行结果
public static void main(String[] args) {
User user = new User();
}
九、Bean的生命周期
Class文件:编译后的字节码文件(User.class)
ClassLoader: 类加载器,加载所有的xxx.class文件
类加载:将Class文件变成Class对象
1、bean的初始化方法
方法一:实现InitializingBean接口,接口包含一个afterPropertiesSet方法
示例代码
public class UserService implements InitializingBean {
/**
* Bean的初始化方法1,在构造方法后执行
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
log.info("Bean初始化,执行afterPropertiesSet方法");
}
}
方法二:自定义初始化方法,并通过init-method属性 来指定自定义的方法名
示例代码
public class UserService {
/**
* Bean的初始化方法2,在构造方法后执行
*/
public void init(){
log.info("Bean初始化,执行自定义的初始化方法");
}
}
xml配置
<bean id="userService"
class="edu.nf.ch04.service.UserService"
init-method="init"/>
注意:如果两种初始化方法同时存在,自定义方法是最后被执行
2、bean的销毁方法
方法一:实现DisposableBean接口,接口包含一个destroy方法
示例代码
@Slf4j
public class UserService implements DisposableBean {
@Override
public void destroy() throws Exception {
log.info("Bean销毁前执行的方法,来自DisposableBean接口");
}
}
方法二:自定义销毁方法,并通过destroy-method属性 来指定方法名
示例代码
@Slf4j
public class UserService {
public void myDestroy(){
log.info("自定义Bean销毁前执行的方法");
}
}
xml配置
<bean id="userService"
class="edu.nf.ch04.service.UserService"
destroy-method="destroy"/>
3、Bean的后置处理器
实现BeanPostProcessor接口,并装配后置处理器
后置处理器的方法是在调用初始化方法之前以及初始化方法执行完之后执行
示例代码
@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);
}
}
xml配置
<!--装配后置处理器,它会在Bean的初始化方法前后执行 -->
<bean class="edu.nf.ch04.processor.MyBeanPostProcessor"/>
测试
public static void main(String[] args) {
// 创建 应用程序上下文
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 关闭容器,容器会销毁所有的Bean, 在销毁前先执行相关的destroy方法
((ClassPathXmlApplicationContext)context).close();
}
注意:后置处理器的方法是针对所有的Bean对象,而不是单独的某一个Bean
4、总结:bean的生命周期流程
-> 执行对象的构造方法
-> 后置处理器的postProcessBeforeInitialization方法
-> InitializingBean接口的afterPropertiesSet方法
-> 自定义的init-method方法
-> 后置处理器的postProcessAfterInitialization方法
-> DisposableBean接口的destroy方法
-> 自定义的destroy-method方法
特别:原型的Bean在Spring容器中没有生命周期管理。
对于单例的Bean,Spring容器会在启动时创建并管理其生命周期,包括初始化和销毁。
但是原型的Bean是在每次请求时创建的,它的生命周期不受Spring容器管理。
由于原型的Bean在每次请求时都会创建一个新的实例,因此Spring容器不负责初始化和销毁原型的Bean,而是交给调用者来管理。这样可以更灵活地控制每个实例的生命周期,但也要注意在使用完后手动销毁原型Bean,避免出现资源泄漏的问题。
十、spring注解分类整理
1、Bean装配注解(组件注入)
1.1 @Component
@Component注解用于标识当前类为一个Bean, 这样就会被spring容器扫描到
相当于Setter方法和构造方法注入的bean属性方法
-
可以通过value属性来指定Bean的id
-
如果不指定value, 默认的id就是当前类名, 并将首字母改为小写(例如,当前类名为userDaoImpl)
示例代码
@Component("userDao")
@Slf4j
public class UserDaoImpl implements UserDao {
@Override
public void save() {
log.info("添加用户信息");
}
}
1.2 @Controller
(控制层使用)
/**
* 使用@Controller注解标注控制层的Bean,
* 用于取代@Component注解
*/
//@Component
@Controller
public class UserController {}
1.3 @Service
(业务层使用)
/**
* 使用@Service注解标注业务层的Bean,
* 用于取代@Component注解
*/
//@Component("userService")
@Service("userService")
public class UserServiceImpl implements UserService {}
1.4 @Repository
(dao层使用)
/**
* 持久层(Dao)使用@Repository注解
* 用于取代@Component注解
*/
//@Component("userDao")
@Repository("userDao")
@Slf4j
public class UserDaoImpl implements UserDao {}
注意:@Repository注解,@Service注解、@Controller注解与@Component注解的作用一样,
只是在mvc结构中,建议使用特定注解注入来区分各层
1.5 @RestController
-
@RestController注解是在spring4.0后新加入的一个注解,
-
同样用于标注为控制器的组件,与@Controller作用一样,
-
标注在类上,表示当前Controller类中所有的请求方法 都使用@ResponseBody注解来响应
package edu.nf.ch06.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
/**
* @RestController注解是在spring4.0后新加入的一个注解,
* 同样用于标注为控制器的组件,如果当前Controller中所有的请求
* 方法都需要使用@ResponseBody注解来响应,那么就可以使用
* 它标注在类上,而不需要在每一个方法上标注@ResponseBody
*/
//@Controller
@RestController
public class UserController {
//@ResponseBody
@GetMapping("/user")
public User getUser(){
User user = new User();
user.setUsername("simi");
return user;
}
}
2、注入注解
2.1 @Autowired
(spring官方注解)
相当于Setter方法注入的 property属性方法
相当于构造方法注入的 constructor-arg属性方法
@Autowired注解是Spring官方提供的注入注解,它可以声明在构造方法上、set方法上以及字段上
- 声明在构造方法上
/**
* 构造方法注入实现类
* 使用@Autowired注解进行注入
* @param userService
*/
@Autowired
public UserController(UserService userService){
this.userService = userService;
}
- 声明在set方法上
/**
* set方法注入
* 使用@Autowired注解进行注入
* @param userService
*/
@Autowired
public void setUserService(UserService userService){
this.userService = userService;
}
- 声明在字段上(不推荐使用)
/**
* 字段注入 (Spring不推荐从字段注入,因为字段私有不安全)
* 使用@Autowired注解进行注入
*/
@Autowired
private UserService userService;
2.1.1 类型注入
是指在只有一个实现类时,可以不指定bean的id属性
在构造方法中可以省略@Autowired注解,但set方法不可以省略
2.1.2 注入规则:
- @Autowired注解默认是根据类型注入
- 如果存在多个实现类的时候,则是根据参数名称进行注入。
- 此时的参数名称与实现类注入注解(@Component/@Service/@Repository)值一样
实现类代码
@Slf4j
@Service("stuService")
public class StudentServiceImpl implements UserService {
@Override
public void add() {
log.info("添加学生信息...");
}
}
@Slf4j
@Service("userService")
public class UserServiceImpl implements UserService {
public void add(){
log.info("添加用户信息...");
}
}
示例代码
@Component
public class UserController {
private UserService userService;
private UserService stuService;
/**
* set方法注入
* @param userService
*/
@Autowired
public void setUserService(UserService userService){
this.userService = userService;
}
/**
* set方法注入
* @param stuService
*/
@Autowired
public void setStuService(UserService stuService){
this.stuService = stuService;
}
2.2 @Qualifier - 复合型注解
(spring官方注解)
- 如果存在多个实现类并且参数名称不匹配,则会引发异常,
- 此时的参数名称与实现类注入注解(@Component/@Service/@Repository)值不一样
- 此时应该结合@Qualifier注解来指定要注入的bean,但是
- 这个注解只能用在普通的方法上 (构造方法不可用)
示例代码:
实现类的注入注解与参数名称不匹配
@Slf4j
@Service("stu")
public class StudentServiceImpl implements UserService {
@Override
public void add() {
log.info("添加学生信息...");
}
}
解决 - 使用@Qualifier注解
/**
* set方法注入
* @param stuService
*/
@Qualifier("stu")
@Autowired
public void setStuService(UserService stuService){
this.stuService = stuService;
}
2.3 @Primary
多选一 (多个注入对象,只会选择一个)
- 使用@Primary注解声明在某个实现类上,这样Spring就会优先注入这个实现类
- 在多个实现类时,但Spring容器又不知道注入哪一个的时候,使用Primary注解才会生效
@Slf4j
@Primary
public class StudentServiceImpl implements UserService {
@Override
public void add() {
log.info("添加学生信息...");
}
}
2.4 @Resource (JSR250)
使用JSR250(Java的规范提案),它设计了@Resource注解来支持依赖注入
- spring对这个注解也实现了支持,需要注意的是这个注解只能用在字段或者
- 普通的set方法上,并不支持构造方法注入。默认也是按照类型注入
/**
* name属性指定需要注入的bean的Id
* @param service
*/
@Resource(name = "userService")
public void setService(UserService service) {
this.service = service;
}
(@Resource注解已在JDK11中被移除,如果要使用需要额外添加依赖)
2.5 @Inject(JSR330)
使用JSR330标准提供的@Inject注解实现依赖注入,
- 这个注解并不存在JDK中,需要额外添加依赖
- 这个注解同样支持字段、构造方法、set方法注入
- 并且默认也是按照类型注入。
用法与@Autowired基本一致,都是用来装配注入的方法。
相当于Setter方法注入的 property属性方法,
相当于构造方法注入的 constructor-arg属性方法
/**
* 构造方法注入实现类
* 使用@Inject注解进行注入
* @param userService
*/
@Inject
public UserController(UserService userService) {
this.userService = userService;
}
2.6 @Named(JSR330)
-
当有多个实现类,并且方法参数与id不一致时,可以结合@Named注解来指定bean的id
-
又或者可以使用@Primary注解设置注入的优先级
/**
* 当前有多个实现类,并且方法参数与id也不一致(aa与userService不一致),
* 结合@Named注解来指定bean的id
* @param userService
*/
@Inject
@Named("userService")
public UserController(UserService aa) {
this.userService = aa;
}
2.7 @RequiredArgsConstructor
- Lombok迎合了spring4.2的新特性,实现了更加简洁的注入方式,
- 使用@RequiredArgsConstructor注解,lombok会自动添加一个带参的构造方法实现构造器的注入
- 注意:此时的字段必须是final修饰
示例代码
@RequiredArgsConstructor
public class UserController {
/**
* 使用是final修饰,就可以不用写构造方法
*/
private final UserService userService;
public void addUser(){
userService.save();
}
}
3、注解注入的xml配置
这里创建了一个application.xml文件,用于启动注解扫描
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 启用注解扫描,指定扫描的包 -->
<context:component-scan base-package="edu.nf.ch07"/>
</beans>
4、问题
注解没有提示 - 解决方法
5、添加注解依赖
5.1 添加JSR250标准注入注解依赖
jdk11的版本已经不支持 @Resource,如果要使用,需要添加此依赖
<!--添加JSR250标准注入注解-->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
5.2 添加JSR330标准注入注解依赖
<!-- JSR330标准注入注解 -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
十一、使用策略模式注入实现
策略模式:当有多个实现类时,只选择一种来实现的情况(多选一)
支付案例
将支付的实现类在支付上下文管理,在控制层中注入支付上下文,调用其方法实现
第一、编写支付接口和两个实现类
package edu.nf.ch11.service;
import java.math.BigDecimal;
/**
* 支付接口,对应有不同的实现
* @Date 2023-10-07
* @Author hy
*/
public interface Payment {
/**
* 支付方法
* @param money
*/
void pay(BigDecimal money);
}
实现类1
package edu.nf.ch11.service.impl;
import edu.nf.ch11.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* 支付宝支付
* @Date 2023-10-07
* @Author hy
*/
@Slf4j
@Service("Alibaba")
public class AliPayment implements Payment {
@Override
public void pay(BigDecimal money) {
log.info("支付宝支付金额;" + money.doubleValue());
}
}
实现类2
package edu.nf.ch11.service.impl;
import edu.nf.ch11.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* 微信支付
* @Date 2023-10-07
* @Author hy
*/
@Slf4j
@Service("WeChat")
public class WeChatPayment implements Payment {
@Override
public void pay(BigDecimal money) {
log.info("微信支付金额:" + money.doubleValue());
}
}
第二、编写支付上下文(用来管理支付的实现类)
package edu.nf.ch11.service;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Map;
/**
* 支付策略上下文
* 这里通过Spring注入所有的策略实现类
* 并完成具体的策略调用,
* 并且这个策略上下文也交给容器管理,
* 便于将上下文注入到其他类中(例如Controller)
* @Date 2023-10-07
* @Author hy
*/
@Service
/**
* 利用Lombok生成一个带参数的构造方法,
* 这样即可以通过构造方法直接注入
*/
@RequiredArgsConstructor
public class PaymentContext {
/**
* 注入一个map集合,Spring会将Payment接口的所有实现类
* 一并保存到map中
* key为支付类型(bean的id),value是具体的支付策略实现(也就是Payment接口的实现类)
*/
private final Map<String, Payment> paymentMap;
/**
* 根据支付类型选择具体的策略来完成支付
* @param paymentType 支付类型
* @param money 支付金额
*/
public void pay(String paymentType, BigDecimal money){
Payment payment = paymentMap.get(paymentType);
payment.pay(money);
}
}
第三、编写Controller
package edu.nf.ch11.controller;
import edu.nf.ch11.service.PaymentContext;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import java.math.BigDecimal;
/**
* @Date 2023-10-07
* @Author hy
*/
@Controller
@RequiredArgsConstructor
public class PaymentController {
/**
* 注入策略上下文
*/
private final PaymentContext context;
public void pay(String type, BigDecimal money){
context.pay(type,money);
}
}
第四、编写bean.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="edu.nf.ch11"/>
</beans>
第五、测试实现
package edu.nf.ch11;
import edu.nf.ch11.controller.PaymentController;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.math.BigDecimal;
/**
* @Date 2023-10-07
* @Author hy
*/
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
PaymentController controller = context.getBean(PaymentController.class);
// 调用控制层的方法
controller.pay("Alibaba",new BigDecimal(100));
}
}
十二、作用域代理
1、@Scope
@Scope注解用于设置Bean的作用域,等效于xml中的scope属性
-
当不指定value属性时,默认是单例
-
如果要使用原型,就必须指定为prototype
-
@Scope注解还可以声明在Bean方法上来设置Bean的作用域
@Scope("singleton")
@RequiredArgsConstructor
@Slf4j
public class UserController {
/**
* 构造方法注入UserService
*/
private final UserService userService;
public void addUser(){
log.info("业务层:" + userService);
//userService.add();
}
}
2、原型失效
原型失效的产生,当一个单例的bean去注入一个原型的bean,这个原型的bean默认是会失效的
- 因为在容器初始化单例的Bean时为了正确注入实例,
- 会将需要注入的对象预先创建出来并注入到当前的单例Bean中,
- 因此只要单例的Bean不销毁,被注入的这个对象也一并存在
示例代码
/**
* 控制层
*/
@Scope("singleton") // 单例
@RequiredArgsConstructor
@Slf4j
public class UserController {
/**
* 构造方法注入UserService
*/
private final UserService userService;
public void addUser(){
log.info("业务层:" + userService);
//userService.add();
}
}
/**
* 接口实现类
*/
@Scope("prototype") // 原型
public class UserServiceImpl implements UserService {
@Override
public void add() {
log.info("添加用户");
}
}
/**
* 测试
*/
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserController controller1 = context.getBean(UserController.class);
UserController controller2 = context.getBean(UserController.class);
controller1.addUser();
// 业务层:edu.nf.ch12.service.impl.UserServiceImpl@43015c69
controller2.addUser();
// 业务层:edu.nf.ch12.service.impl.UserServiceImpl@43015c69
// 测试结果:class对象实例相同,原型失效
}
3、代理作用域
代理作用域的作用就是解决原型失效
当一个单例的Bean注入原型Bean的时候,如果想让原型生效,
-
可以设置proxyMode属性,这个属性就是代理作用域,
-
其原理就是当初始化单例Bean的时候,并不会立刻完成注入,
而是将一个代理对象(暂时可理解为替身)设置到单例中,
-
当调用注入对象的方法时,此时替身会自动从原型容器中创建一个实例
并返回,保证原型的有效
示例代码
@Scope(value = "prototype" ,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserServiceImpl implements UserService {
@Override
public void add() {
log.info("添加用户");
}
}
/**
* 再次测试
*/
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserController controller1 = context.getBean(UserController.class);
UserController controller2 = context.getBean(UserController.class);
controller1.addUser();
// 业务层:edu.nf.ch12.service.impl.UserServiceImpl@6ad3381f
controller2.addUser();
// 业务层:edu.nf.ch12.service.impl.UserServiceImpl@53dbe163
// 测试结果:class对象实例不相同,原型生效
}
十三、注解结合Java配置类
取代原先配置xml文件的操作
Spring提倡使用注解和配置类来完成Bean的装配,而配置类就是全面取代xml配置文件的一种方式,
配置类一般用于整合其他配置 , 例如mybatis、springmvc等
1、配置类的注解
1.1 @Configuration
@Configuration的作用 - 标识一个类为合法的Spring配置类 - 声明一个配置类
/**
* 在类中声明
*/
@Configuration
public class AppConfig {
}
注意:当配置类上标注了@Configuration注解时是 Full模式(代理模式)
反之是 Lite模式(非代理模式)
1.2 @ComponentScan
@ComponentScan的作用 - 扫描指定的包,装配相关的Bean,等价于xml中的扫描
必须与@Configuration结合一起使用
/**
* 在类中声明
*/
@Configuration
@ComponentScan(basePackages = "edu.nf.ch13")
public class AppConfig {
}
注意:如果使用了@Component、@Service、@Controller、@Repository等注解,就需要使用@ComponentScan注解启动扫描,否则不需要使用@ComponentScan,直接在AppConfig类中装配即可
1.3 @PropertySource
@PropertySource的作用 - 加载指定的配置文件
下面案例中,db.properties就是在resources目录下的一个配置类
@Configuration
/**
* 将properties文件的属性注入到配置类的字段中
* 由于properties文件放在resources目录下,
* 编译之后会保存在classpath目录下,因此需要从
* classpath路径中查找资源文件
*/
@PropertySource("classpath:db.properties")
public class AppConfig {
1.4 @MapperScan
@MapperScan的作用 - 扫描dao接口的包,动态生成到接口的代理实现(生成代理对象)
// 等效于xml中的<mybatis:scan/>
@MapperScan(basePackages = "org.example.ch02.dao")
public class AppConfig {}
2、@Value的使用
@Value的使用 - 将外部配置文件的属性值注入到java类中(成员变量或方法参数)
下面案例中,${}中的driver就是配置类的一个属性值
/**
* 使用@Value注解结合spel表达式(${})进行值注入,
* 也就是将properties文件中的属性值注入到当前的字段中,
* spel表达式中对于的是properties文件中的key
*/
@Value("${driver}")
private String driver;
3、@Bean的使用
手动装配,一般不建议使用
@Bean注解用于在配置类中装配Bean,这种方法很类似
- 在xml中配置一个个的< bean id=“xx”/>
- 用了@Bean注解后,默认标注的方法名就是Bean的id
- 还可以通过name属性指定bean的别名
@Configuration
@ComponentScan(basePackages = "edu.nf.ch13")
public class AppConfig {
/**
* @Bean注解后默认标注的方法名就是Bean的id
* 还可以通过name属性指定bean的别名
* @return
*/
//@Bean(name={"aa","bb"})
@Bean
public UserService userService(){
return new UserServiceImpl();
}
}
注意:@Bean注解与@Component、@Service、@Controller、@Repository注解的作用一样
4、注入其他Bean的两种方式
方式一:通过参数实现注入
/**
* 通过参数实现注入
* @return
*/
@Bean
public UserController userController(UserService userService){
return new UserController(userService);
}
方式二:通过调用bean方法实现注入
@Configuration
@ComponentScan(basePackages = "edu.nf.ch13")
public class AppConfig {
/**
* @Bean注解在配置类中装配Bean
* @return
*/
@Bean
public UserService userService(){
return new UserServiceImpl();
}
/**
* 通过调用上面的bean方法实现注入
*
* @Scope注解还可以声明在Bean方法上来设置Bean的作用域
* @return
*/
@Bean
@Scope("prototype")
public UserController userController(){
return new UserController(userService());
}
}
5、创建容器工厂
public static void main(String[] args) {
// 创建基于注解和配置类的容器工厂
// 取代ClassPathXmlApplicationContext
// 构造方法指定配置类的Class对象
ApplicationContext context=new AnnotationConfigApplicationContext(AppConfig.class);
UserController controller = context.getBean(UserController.class);
controller.add();
}
案例1:配置文件实现
第一:编写配置文件 - beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 整合druid数据源连接池,其中DruidDataSource核心类纳入Spring容器中管理,并指定相关的初始化方法以及销毁方法-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 注入相关的连接属性 -->
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/city?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<!--注入连接池相关的配置属性 -->
<!-- 最大连接数量 -->
<property name="maxActive" value="200"/>
<!-- 预先初始化5个连接放入连接池 -->
<property name="initialSize" value="5"/>
<!--最小空闲连接数(当连接池连接释放时,最少的连接数,与初始化连接数保存一致)-->
<property name="minIdle" value="5"/>
<!-- 获取连接的最大等待时间,单位:毫秒 (超出时间即抛出异常,不再等待)-->
<property name="maxWait" value="2000"/>
<!-- 连接保存空闲不被驱逐出连接池的最小时间 -->
<property name="minEvictableIdleTimeMillis" value="300000"/>
<!-- 检测连接的间隔时间,单位:毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<!-- 检测连接是否有效 -->
<property name="testWhileIdle" value="true"/>
<!-- 放回连接池时是否检查连接有效 -->
<property name="testOnReturn" value="false"/>
<!-- 定义一条伪SQL,用于检查连接是否可用 -->
<property name="validationQuery" value="select 1"/>
<!-- 是否缓存PreparedStatement,mysql建议关闭 -->
<property name="poolPreparedStatements" value="false"/>
</bean>
</beans>
第二:实现
@Slf4j
public class Main {
public static void main(String[] args) throws SQLException {
// 创建 应用程序上下文
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 从容器中获取数据源(连接池)
DruidDataSource ds = (DruidDataSource) context.getBean(DataSource.class);
log.info("初始化连接数:" + ds.getInitialSize());
log.info("最小空闲连接数:" + ds.getMinIdle());
log.info("最大连接数:" + ds.getMaxActive());
// 从数据源中获取连接
Connection conn = ds.getConnection();
log.info("连接对象:" + conn);
}
}
案例2:注解结合配置类实现
第一:编写配置文件 - druid.properties
# 连接属性
driver = com.mysql.cj.jdbc.Driver
url = jdbc:mysql://localhost:3306/city
username = root
password = 123456
# 连接池属性
maxActive = 200
initialSize = 5
minIdle = 5
maxWait = 2000
minEvictableIdleTimeMillis = 300000
timeBetweenEvictionRunsMillis = 60000
testWhileIdle = true
testOnReturn = false
validationQuery = select 1
poolPreparedStatements = false
第二:编写配置类 - AppConfig
- 方法一: 使用@Value注解配置
package org.example.ch01.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
/**
* @Date 2023-10-16
* @Author hy
*/
@Configuration
@PropertySource("classpath:druid.properties")
public class AppConfig {
// 方法一,使用此方法
@Value("${driver}")
private String driver;
@Value("${url}")
private String url;
@Value("${user}")
private String username;
@Value("${password}")
private String password;
@Value("${maxActive}")
private Integer maxActive;
@Value("${initialSize}")
private Integer initialSize;
@Value("${minIdle}")
private Integer minIdle;
@Value("${maxWait}")
private Integer maxWait;
@Value("${minEvictableIdleTimeMillis}")
private Integer minEvictableIdleTimeMillis;
@Value("${timeBetweenEvictionRunsMillis}")
rivate Integer timeBetweenEvictionRunsMillis;
@Value("${testWhileIdle}")
private boolean testWhileIdle;
@Value("${testOnReturn}")
private boolean testOnReturn;
@Value("${validationQuery}")
private String validationQuery;
@Value("${poolPreparedStatements}")
private boolean poolPreparedStatements;
@Bean(initMethod = "init",destroyMethod = "close")
public DruidDataSource dataSource(){
// 创建数据源连接池
DruidDataSource ds = new DruidDataSource();
// 注入连接属性
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
// 注入连接池属性
ds.setInitialSize(initialSize);
ds.setMinIdle(minIdle);
ds.setMaxWait(maxWait);
ds.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
ds.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
ds.setTestWhileIdle(testWhileIdle);
ds.setTestOnReturn(testOnReturn);
ds.setValidationQuery(validationQuery);
ds.setPoolPreparedStatements(poolPreparedStatements);
return ds;
}
}
- 方法二:创建Properties对象
package org.example.ch01.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
/**
* @Date 2023-10-16
* @Author hy
*/
@Configuration
public class AppConfig {
@Bean(initMethod = "init",destroyMethod = "close")
public DruidDataSource dataSource(){
// 方法二,使用此方法
// druid.properties配置文件中的user要换成username
// 创建Properties对象
Properties prop = new Properties();
// 获取一个输入流来读取properties文件
InputStream input = AppConfig.class.getClassLoader()
.getResourceAsStream("druid.properties");
// 将输入流加载到properties独享中
prop.load(input);
// 通过DruidDataSourceFactory来创建
// DruidDataSource实例
DruidDataSource ds = (DruidDataSource) DruidDataSourceFactory
.createDataSource(prop);
return ds;
}
}
第三:实现
@Slf4j
public class Main {
public static void main(String[] args) throws SQLException {
// 创建 应用程序上下文
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
DruidDataSource ds = (DruidDataSource) context.getBean(DataSource.class);
log.info("初始化连接数:" + ds.getInitialSize());
log.info("最小空闲连接数:" + ds.getMinIdle());
log.info("最大连接数:" + ds.getMaxActive());
// 从数据源中获取连接
Connection conn = ds.getConnection();
log.info("连接对象:" + conn);
}
}
十四、配置类的Lite模式和Full模式
Full模式(代理模式)
- 在Full模式下配置类会被代理,
- Spring会为当前配置类创建一个代理对象,从而代理配置类中所有@Bean注解的方法。
- 这样每当调用配置类中的Bean方法之前,会从容器中进行检查Bean实例,并返回容器中存在的Bean对象。
- 配置类如果有代理,调用的就是代理对象的方法,即实例化对象是同一个
当配置类上标注了@Configuration注解时, 并且proxyBeanMethods的属性为true, 此时就是Full模式。
Lite模式(非代理模式)
- 在Lite模式下配置类不会被代理,
- 每次调用Bean方法只是纯粹的调用,而不是通过容器实现方法(没有容器的特性),
- 也就意味着调用方法上的所有注解都会失效,那么配置也就没有意义
代理模式的核心作用:对目标对象的行为进行增强(对调用目标对象的前后进行增强)
容器最简单的优势:可以复用
十五、@Import注解使用
1、@Import的作用
@Import注解用于装配Bean,
与@Component、@Service、@Controller、@Repository、@Bean等注解的作用一样
2、@Import的三种使用方法
2.1 导入(装配)普通的Bean
示例代码
@Configuration
@Import(UserController.class)
public class AppConfig {
}
2.2 导入其他的配置类(常用)
例如在项目中可以模块化配置类,包括mvc的配置类、mybatis配置类、Redis配置类、RabbitMQ配置类等等,那么可以在一个总配置类中导入其他这些配置类进行合并,这样维护性扩展性更强
示例代码
@Configuration
@Import({MvcConfig.class,MybatisConfig.class})
public class AppConfig {
}
案例代码
2.2.1 编写MvcConfig.java类
public class MvcConfig {
}
2.2.2 编写MybatisConfig.java类
public class MybatisConfig {
}
2.3 实现选择性导入
即按照指定的逻辑来导入相关的类
这种方式需要自定义一个导入选择器交给Spring执行
示例代码
@Configuration
@Import(AnnoImportSelector.class)
public class AppConfig {
}
案例代码
2.3.1 编写自定义注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnno {
}
2.3.2 编写自定义导入选择器
/**
* @Date 2023-10-08
* @Author hy
* 自定义导入选择器,如果类上标注了@MyAnno的注解,
* 就会将其纳入Spring容器中管理
*/
@Slf4j
@MyAnno
public class AnnoImportSelector implements ImportSelector {
/**
* 自定义导入逻辑
* @param importingClassMetadata
* @return 返回值就是所有需要导入的类的完整类名
*/
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 创建一个集合保存带有注解的类的完整类名
List<String> classNameList = new ArrayList<>();
// 解析类上是否存在@MyAnno注解
if(UserController.class.isAnnotationPresent(MyAnno.class)){
classNameList.add(UserController.class.getName());
}
// 将集合转换为字符串数组
return StringUtils.toStringArray(classNameList);
}
}
2.3.3 在实现类中添加自定义注解
@Slf4j
@MyAnno
public class UserController {
public void add(){
log.info("添加用户");
}
}
2.3.4 实现方法
public static void main(String[] args) {
ApplicationContext context=new AnnotationConfigApplicationContext(AppConfig.class);
UserController controller = context.getBean(UserController.class);
controller.add();
}
十六、代理模式
1、静态代理
代理对象在编译期确定(编译时:就把代码编译成字节码文字)
好处:代理的逻辑是由我们自己编写的
弊端:只能针对一个接口来代理, 目标对象一旦发生改变, 代理类就会发生改变,不具备任何灵活性
(有多少个目标对象就要创建多少个代理类)
目标对象
@Slf4j
public class B implements DemoInf {
@Override
public void doSomething() {
log.info("执行目标对象的方法");
}
}
代理对象
package edu.nf.ch17.simple.impl;
import edu.nf.ch17.simple.DemoInf;
import lombok.extern.slf4j.Slf4j;
/**
* 代理对象,代理对象的核心作用就是
* 对目标对象的行为进行增强
* @Date 2023-10-10
* @Author hy
*/
@Slf4j
public class DemoProxy implements DemoInf {
/**
* 传入一个被代理的对象
* 为什么要传入接口,而不是对象 - 调用更灵活,只要是接口的实现类都能够被代理
*/
private DemoInf demoInf;
public DemoProxy(DemoInf demoInf) {
this.demoInf = demoInf;
}
@Override
public void doSomething() {
// 调用前增强
before();
// 调用目标对象的方法
demoInf.doSomething();
// 调用后增强
after();
}
private void before(){
log.info("目标方法调用前...");
}
private void after(){
log.info("目标方法调用后...");
}
}
2、动态代理
在运行时动态的创建代理对象(运行时:源码已经编译了,已经跑起来了),不需要编写代理类
方式一:JDK提供的动态代理
JDK动态代理是基于接口来生成代理对象
需要实现一个InvocationHandler接口,并且结合Proxy代理生成工具来动态创建代理对象。
唯一缺点:目标对象一定要实现接口,若不能实现接口则不能使用jdk动态代理
第一:编写目标对象实现的接口
public interface UserService {
void add();
}
注意:JDK的动态代理要求目标对象一定要有实现接口,否则无法创建代理对象,
因为JDK动态代理是基于接口来生成代理对象。
第二:编写目标对象
@Slf4j
public class UserServiceImpl implements UserService {
@Override
public void add() {
log.info("添加用户信息...");
}
}
第三:编写一个回调处理器,用于调用目标对象的方法
package edu.nf.ch17.jdk;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* 动态代理执行的回调处理器
* 回调处理器必须实现InvocationHandler接口
* @Date 2023-10-10
* @Author hy
*/
@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("目标方法调用后...");
}
}
第四:实现
package edu.nf.ch17.jdk;
import edu.nf.ch17.jdk.impl.UserServiceImpl;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
/**
* @Date 2023-10-10
* @Author hy
* 回调处理器的作用是 - 负责调用目标对象的行为
* 回调处理器是给谁用的 - 动态代理
*/
public class Main {
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方法需要提供三个参数
// 参数一:需要提供一个类加载器去加载动态创建出来的代理字节码
// 从而实例化一个代理对象
// 参数二:目标对象实现的所有接口的Class,因为jdk动态代理
// 是一定要根据接口来创建一个代理对象,创建出来的
// 这个代理对象会自动实现这些接口
// 参数三:自定义回调处理器
// 返回的就是已经创建好的对象
UserService proxy = (UserService) Proxy.newProxyInstance(loader, interfaces, handler);
// 代理对象调用回调处理器,回调处理器调用目标方法
// 调用代理对象的任何方法,都会去调用回调处理器的invoke的方法
// 来完成代理的调用
proxy.add();
}
}
2.1.1 结合回调控制器实现
回调处理器的作用是 - 负责调用目标对象的行为
回调处理器是给谁用的 - 动态代理
方式二:cglib动态代理
不实现任何接口的情况下,用cglib代理框架为目标对象动态创建一个子类对象来实现代理
第一:添加cglib依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
第二:添加目标对象
@Slf4j
public class UserService {
public void add(){
log.info("添加用户");
}
}
第三:编写一个方法拦截器,用于调用目标对象的方法
package edu.nf.ch17.cglib.student;
import lombok.extern.slf4j.Slf4j;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* @Date 2023-10-11
* @Author hy
* cglib要求编写一个方法拦截器用于调用目标对象的方法
* 本质上和JDK的InvocationHandler的作用是一样的
* 需要实现MethodInterceptor接口
*/
@Slf4j
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 returnValue = methodProxy.invokeSuper(proxy,args);
log.info("调用目标对象的方法后...");
return returnValue;
}
}
第四:实现
public static void main(String[] args) {
// 创建代理生成器
Enhancer enhancer = new Enhancer();
// 要告诉代理生成器需要代理的父类
enhancer.setSuperclass(UserService.class);
// 设置方法拦截器(回调处理器)
enhancer.setCallback(new UserServiceMethodInterceptor());
// 创建代理对象(运行时动态创建的子类对象就是代理对象)
UserService service = (UserService)enhancer.create();
// 调用代理对象的方法
service.add();
}
JDK动态代理和Cglib动态代理的区别:
实现JDK动态代理时,目标对象必须要实现接口,代理对象是基于目标对象实现的接口而生成的代理对象。
实现Cglib动态代理,目标对象不需要实现接口,它是基于继承来实现代理对象,是在目标对象动态运行时生成一个子类,通过目标对象代理出来的子类对父类进行功能的增强。
十七、实例创建的过程
(源文件)User.java
->(javac处理器把.java文件处理成.class文件(字节码文件))User.class
->(类加载器将.class文件解析成字节数组)byte[]
->(加载到JVM中,并将字节数组创建成Class对象)Class对象
->(根据Class对象创建实例)User user = new User();
(动态代理没有源文件到字节码文件的步骤)
Class类是一个模版,只有一个
Clss对象是根据Class类生成的对象,有多个
例如: class User{} 只有一个,而User user1 = new User();像这样的对象可以有多个
十八、连接池复用原理
将连接池放到容器中管理,实现连接池的复用
案例
第一、配置类
package edu.nf.ch17.spring.config;
import edu.nf.ch17.spring.ConnectionPool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.sql.SQLException;
/**
* 配置类
* @Date 2023-10-10
* @Author hy
*
*/
@Configuration
/**
* 将properties文件的属性注入到配置类的字段中
* 由于properties文件放在resources目录下,
* 编译之后会保存在classpath目录下,因此需要从
* classpath路径中查找资源文件
*/
@PropertySource("classpath:db.properties")
@Slf4j
public class AppConfig {
/**
* 使用@Value注解结合spel表达式(${})进行值注入,
* 也就是将properties文件中的属性值注入到当前的字段中,
* spel表达式中对于的是properties文件中的key
*/
@Value("${url}")
private String url;
@Value("${user}")
private String username;
@Value("${password}")
private String password;
/**
* 连接池大小
*/
@Value("${size}")
private Integer size;
@Bean
public ConnectionPool connectionPool() throws SQLException {
// 创建连接池并设置相关属性
ConnectionPool pool = new ConnectionPool();
pool.setUrl(url);
pool.setUsername(username);
pool.setPassword(password);
pool.setSize(size);
// 初始化连接池大小
pool.init();
return pool;
}
}
第二、回调控制器
package edu.nf.ch17.spring;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.LinkedList;
/**
* 回调控制器
* @Date 2023-10-10
* @Author hy
*/
public class ConnectionInvocationHandler implements InvocationHandler {
/**
* 目标对象(被代理的对象)
*/
private Connection connection;
/**
* 连接池
*/
private LinkedList<Connection> pool;
public ConnectionInvocationHandler(Connection connection, LinkedList<Connection> pool) {
this.connection = connection;
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(connection, args);
}
}
}
第三、连接池
package edu.nf.ch17.spring;
import lombok.Setter;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.LinkedList;
/**
* 连接池
* @Date 2023-10-10
* @Author hy
*/
@Setter
public class ConnectionPool {
/**
* 连接池(存放连接的集合)
* LinkedList:链表的数据结构,从头部拿,尾部放
*/
private LinkedList<Connection> pool = new LinkedList<>();
/**
* 连接属性
*/
private String url;
private String username;
private String password;
/**
* 池大小
*/
private Integer size;
/**
* 在构造方法中初始化连接池大小
* @param
*/
public void init(){
for (int i = 0; i < size; i++) {
try {
// 从数据库获取连接对象
Connection conn = DriverManager.getConnection(url, username, password);
// 对连接对象创建代理
conn = createProxy(conn);
// 将代理过的连接对象保存到池中(这里是将连接对象放到代理中,再把代理保存到池中)
pool.add(conn);
} catch (SQLException e) {
throw new RuntimeException("Init Connection pool error.." + e);
}
}
}
/**
* 为连接对象创建代理
* @param target
* @return
*/
private Connection createProxy(Connection target){
// 创建回调控制器
ConnectionInvocationHandler handler = new ConnectionInvocationHandler(target,pool);
// 获取连接对象的所有接口
//Class<?>[] interfaces = target.getClass().getInterfaces();
Class<?>[] interfaces = new Class[]{Connection.class};
// 通过当前类获取一个类加载器
ClassLoader loader = ConnectionPool.class.getClassLoader();
// 返回创建好的代理对象
return (Connection) Proxy.newProxyInstance(loader, interfaces, handler);
}
/**
* 从池中获取代理连接
* @return
*/
public Connection getConnection(){
return pool.removeFirst();
}
/**
* 查看连接池的大小
* @return
*/
public int size(){
return pool.size();
}
}
第四、实现
package edu.nf.ch17.spring;
import edu.nf.ch17.spring.config.AppConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.sql.Connection;
import java.sql.SQLException;
/**
* @Date 2023-10-10
* @Author hy
*/
@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 connection = pool.getConnection();
log.info("连接池个数:" + pool.size());
connection.close();
log.info("连接池个数:" + pool.size());
}
}
十九、AOP(面向切面编程)
编程的变化:
面向过程(把一个业务按部就班从0到1的实现,不易扩展)-> 面向对象(封装、继承、多态)-> 面向切面
1、概念
AOP是面向对象编程的一种额外的功能业务补充(动态代理增强)
目的:为了将业务目标进行额外的增强或者扩展。
Spring中的AOP是基于JDK动态代理和CGLIB动态代理实现的。
2、应用场景
2.1 日志操作
可以在业务方法前后进行日志的记录,不需要每个业务方法中都编写重复的代码
2.2 权限管理
可以在调用目标方法前确定是否有权限
2.3 事务管理
可以在调用业务方法前开启事务,方法执行完成后提交事务
3、AOP术语
3.1 切面(Aspect)
切面是用于编写切面逻辑的一个类,这个类很类似于JDK动态代理中的回调处理器或者cglib中的方法拦截器,主要就是将需要增强目标对象的功能代码编写在这个类中,而这些功能增强的代码就是切面逻辑。
3.2 通知/增强(Advice)
增强就是对目标行为植入额外的逻辑代码,从而增强原有的功能。增强分为五种类型:
1)前置通知(在目标方法调用之前执行)
2)后置通知(在目标方法正确返回之后执行)
3)环绕通知(在目标方法调用前后执行)
4)异常通知(当目标方法抛出异常时执行,并且不会执行后置通知)
5)最终通知(不管目标方法有无异常都会执行)
3.3 切入点(Pointcut)
从哪个地方开始找目标对象的方法
切入点类似一个切入的坐标,目的就是要找到目标对象的哪些方法进行切入。切入点可以使用表达式进行描述。
3.4 连接点(Joinpoint)
找到的目标对象方法,需要增强的目标方法
目标对象的方法(被切入的方法)就称之为连接点,一个切入点可以对应目标对象的的多个连接点。
3.5 代理(Proxy)
在运行时动态创建的对象,称之为代理对象,负责调用目标对象的方法,并执行增强功能。
3.6 目标(Target)
被代理的对象就是目标对象。
3.7 织入(Weaver)
将通知应用到目标对象的方法上,使其增强原有的功能的这个过程就是织入
将切面中的增强逻辑 应用到目标具体的连接点上,并产生代理的过程称之为织入。
因此通常描述为“将通知织入到具体的目标”。
织入的时机可以分为以下几种:
-
类加载时织入,需要特殊的类加载器(LTW)
-
编译时织入,需要特殊的编译器(CTW)
-
运行时织入,通常使用JDK或者CGLIB在程序运行创建代理对象,
spring就是基于运行时织入的。(注意:spring仅仅只是用到了AspectJ的切入点表达式和注解,但并没有使用AspectJ的类加载和编译时织入功能,而是使用JDK和CGLIB在运行时生成代理对象。)
4、Spring AOP xml配置示例一
实现方法:AOP早期版本,通过ProxyFactoryBean代理工厂实现
案例1 目标对象有接口
使用JDK动态代理
第一:编写目标对象
@Slf4j
public class UserServiceImpl implements UserService {
@Override
public void add() {
log.info("添加用户");
//throw new RuntimeException("自定义异常");
}
@Override
public void delete() {
log.info("删除用户");
}
}
第二:编写切面类
package edu.nf.ch18.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.ThrowsAdvice;
import java.lang.reflect.Method;
/**
* 定义一个切面类,将需要增强的逻辑方法编写在这个类中。
* 这些增强的逻辑方法的专业术语叫做增强或者通知。
* 增强的类型一共有五种
* (前置通知、后置通知、环绕通知、异常通知、最终通知)
* 前置通知需要实现MethodBeforeAdvice接口
* 后置通知需要实现AfterReturningAdvice接口
* 环绕通知需要实现MethodInterceptor接口
* 异常通知需要实现ThrowsAdvice接口 (当异常通知被执行,后置通知就不会再执行)
*
* @Date 2023-10-12
* @Author hy
*/
@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.getMessage());
}
}
第三:编写配置文件beans.xml
<!-- 装配UserService(目标对象), 有实现接口 -->
<bean id="userService" class="edu.nf.ch18.service.impl.UserServiceImpl"/>
<!-- 装配切面 -->
<bean id="serviceAspect" class="edu.nf.ch18.aspect.ServiceAspect"/>
<!-- 装配UserService代理,让spring在运行时动态创建一个代理对象,
代理对象是通过ProxyFactoryBean这个代理工厂创建出来的,
并且这个代理对象也会自动纳入容器中管理 -->
<bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- ProxyFactoryBean中有一个proxyInterfaces属性
用于注入接口信息,如果有接口,spring就会使用JDK动态代理 -->
<property name="proxyInterfaces">
<list>
<value>edu.nf.ch18.service.UserService</value>
</list>
</property>
<!-- ProxyFactoryBean中还有一个target属性,
用于注入目标对象,这样spring才知道为谁创建代理,
目标对象可以引用上面的bean的id -->
<property name="target" ref="userService"/>
<!-- ProxyFactoryBean中还有一个interceptorNames属性
用于注入切面,可以配置多个切面 -->
<property name="interceptorNames">
<list>
<!-- 引用上面配置切面的Bean的id -->
<value>serviceAspect</value>
</list>
</property>
</bean>
第四:实现
public static void main(String[] args) {
// 创建容器工厂
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 从容器中获取代理对象
UserService service = context.getBean("userServiceProxy", UserService.class);
service.add();
service.delete();
}
案例2 目标对象没有接口
使用Cglib动态代理
第一:编写目标对象
@Slf4j
public class StuService {
public void add(){
log.info("添加学生信息");
}
}
第二:编写切面类
package edu.nf.ch18.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.ThrowsAdvice;
import java.lang.reflect.Method;
/**
* 切面类
* @Date 2023-10-12
* @Author hy
*/
@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;
}
/**
* 异常通知
* @param e
*/
public void afterThrowing(Exception e){
log.info("异常通知.." + e.getMessage());
}
}
第三:编写配置文件beans.xml
<!-- 装配StuService(目标对象),没有实现任何接口 -->
<bean id="stuService" class="edu.nf.ch18.service.StuService"/>
<!-- 装配切面 -->
<bean id="serviceAspect" class="edu.nf.ch18.aspect.ServiceAspect"/>
<!-- 装配StuService代理 -->
<bean id="stuServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 如果目标对象没有实现任何接口, 则spring会使用CGLIB来创建代理 -->
<!-- 注入目标对象 -->
<property name="target" ref="stuService"/>
<!-- 注入切面 -->
<property name="interceptorNames">
<list>
<value>serviceAspect</value>
</list>
</property>
</bean>
第四:实现
public static void main(String[] args) {
// 创建容器工厂
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 从容器中获取代理对象
StuService stuService = context.getBean("stuServiceProxy", StuService.class);
stuService.add();
}
5、Spring AOP xml配置示例二
实现方法:通过切入点表达式
第一:编写目标对象
@Slf4j
public class UserService {
public void add(){
log.info("添加用户");
}
}
第二:编写切面类
package edu.nf.ch19.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.ThrowsAdvice;
import java.lang.reflect.Method;
/**
* 切面类
* @Date 2023-10-12
* @Author hy
*/
@Slf4j
public class ServiceAspect implements MethodBeforeAdvice, AfterReturningAdvice, MethodInterceptor, ThrowsAdvice {
/**
* 环绕通知
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("环绕通知前..");
// 调用目标对象方法
Object returnVal = invocation.proceed();
log.info("环绕通知后..");
return returnVal;
}
/**
* 后置通知
* @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 method
* @param args
* @param target
* @throws Throwable
*/
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
log.info("前置通知..");
}
/**
* 异常通知
* @param e
*/
public void afterThrowing(Exception e){
log.info("异常通知.." + e.getMessage());
}
}
第三:编写配置类beans.xml
<!-- 装配UserService(目标对象) -->
<bean id="userService" class="edu.nf.ch19.service.UserService"/>
<!-- 装配切面,将切面纳入spring容器中管理 -->
<bean id="serviceAspect" class="edu.nf.ch19.aspect.ServiceAspect"/>
<!-- AOP配置,proxy-target-class属性设置为true,
表示强制使用CGLIB生成代理,无论目标对象有没有实现接口,
如果设置为false,表示有接口用JDK生成代理,无接口用CGLIB生成代理 -->
<aop:config proxy-target-class="true">
<!-- 配置切入点,Spring使用了AspectJ的切入点表达式
来实现了AOP中切入点的概念,通过切入点表达式
可以找到需要增强的目标的方法。而找到的这些目标方法
就称为连接点。id属性指定一个切入点的唯一标识,
expression用于声明切入点表达式,
切入点表达式的语法:
execution(访问修饰符 包名.类名.方法名(参数类型))
也可以使用通配符来扩大切入点的范围
execution(访问修饰符 包名.*.*(..)
-->
<aop:pointcut id="myPointcut" expression="execution(* edu.nf.ch19.service.*.*(..))"/>
<!-- 配置通知器(也就是切面),使用advice-ref属性
引用上面装饰的切面, pointcut-ref引用切入点的id
-->
<aop:advisor advice-ref="serviceAspect" pointcut-ref="myPointcut"/>
</aop:config>
第四:实现
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService service = context.getBean(UserService.class);
service.add();
}
}
6、Spring AOP xml配置示例三
实现方法:自定义通知
第一:编写自定义通知类
package edu.nf.ch20.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 切面
* @Date 2023-10-13
* @Author hy
*/
@Slf4j
public class ServiceAspect{
/**
* 自定义前置通知,可以给一个参数
* 这个参数为连接点(JoinPoint)
* 通过这个连接点可以拦截目标方法的参数等信息
* @param jp 连接点(JoinPoint)
*/
public void before(JoinPoint jp){
log.info("执行前置通知,拦截的目标方法参数:" + jp.getArgs()[0]);
jp.getArgs()[0] = "xxx";
}
/**
* 后置通知
* @param jp 连接点
* @param returnVal 目标方法的返回值
*/
public void afterReturning(JoinPoint jp,Object returnVal){
log.info("后置通知,目标方法返回值:" + returnVal);
}
/**
* 环绕通知
* @param jp 连接点,继承自JoinPoint接口
* @return
*/
public Object around(ProceedingJoinPoint jp) throws Throwable {
log.info("环绕通知前,目标方法参数:" + jp.getArgs()[0]);
Object returnVal = jp.proceed();
log.info("环绕通知后,目标方法返回值:" + returnVal);
return returnVal;
}
/**
* 异常通知,当目标方法产生异常时会执行,
* 后置通知将不再生效
* @param jp 连接点
* @param e 目标方法产生的异常对象
*/
public void afterThrowing(JoinPoint jp,Exception e){
log.info("异常通知,异常信息:" + e.getMessage());
}
/**
* 最终通知
* 不管有没有异常产生最终通知都会被执行
* @param jp 连接点
*/
public void after(JoinPoint jp){
log.info("最终通知");
}
}
第二:编写目标对象
@Slf4j
public class UserService {
public String add(String name){
log.info("添加用户----" + name);
log.info("自定义异常" + 10/0);
return "success";
}
}
第三:配置xml文件 - beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 装配目标对象 -->
<bean id="userService" class="edu.nf.ch20.service.UserService"/>
<!-- 装配切面 -->
<bean id="serviceAspect" class="edu.nf.ch20.aspect.ServiceAspect"/>
<!-- 配置AOP -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut id="myPointcut"
expression="execution(* edu.nf.ch20.service.UserService.*(..))"/>
<!-- 配置切面,ref引用切面的bean的id -->
<aop:aspect ref="serviceAspect">
<!-- 配置各种通知,method属性指定通知的方法名,
pointcut-ref属性引用切入点的bean的id
-->
<!-- 配置前置通知 -->
<aop:before method="before" pointcut-ref="myPointcut"/>
<!-- 配置后置通知 -->
<aop:after-returning method="afterReturning"
returning="returnVal" pointcut-ref="myPointcut"/>
<!-- 配置环绕通知 -->
<aop:around method="around" pointcut-ref="myPointcut"/>
<!-- 配置异常通知,需要绑定异常通知的参数名-->
<aop:after-throwing method="afterThrowing"
pointcut-ref="myPointcut" throwing="e"/>
<!-- 配置最终通知 -->
<aop:after method="after" pointcut-ref="myPointcut"/>
</aop:aspect>
</aop:config>
</beans>
第四:实现
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService service = context.getBean(UserService.class);
service.add("程序猿");
}
}
Spring Aop 注解结合配置类示例
实现方法:使用@Aspect注解代替xml配置
第一:编写自定义切面类
package edu.nf.ch21.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 自定义切面类
* @Date 2023-10-13
* @Author hy
*/
@Slf4j
// 将切面纳入容器管理
@Component
// 使用@Aspect注解标识当前类为一个切面
@Aspect
public class ServiceAspect {
/**
* 声明一个切入点
*/
@Pointcut("execution(* edu.nf.ch21.service.UserService.*(..))")
public void pointcut(){
}
/**
* 前置通知,引用上面声明的切入点方法名
* @param joinPoint
*/
@Before("pointcut()")
public void before(JoinPoint joinPoint){
log.info("前置通知,参数:" + joinPoint.getArgs()[0]);
}
/**
* 后置通知
* @param jp
* @param returnVal
*/
@AfterReturning(value = "pointcut()",returning = "returnVal")
public void afterReturning(JoinPoint jp, Object returnVal){
log.info("后置通知,方法返回值:" + returnVal);
}
/**
* 环绕通知
* @param jp
* @return
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint jp) throws Throwable {
log.info("环绕通知前,方法参数:" + jp.getArgs()[0]);
Object returnVal = jp.proceed();
log.info("环绕通知后,方法返回值:" + returnVal);
return returnVal;
}
/**
* 异常通知
* @param jp
* @param e
*/
@AfterThrowing(value = "pointcut()",throwing = "e")
public void afterThrowing(JoinPoint jp,Exception e) {
log.info("异常通知:" + e.getMessage());
}
/**
* 最终通知
* @param jp
*/
@After("pointcut()")
public void after(JoinPoint jp) {
log.info("最终通知");
}
}
第二:添加AspectJ的切入点表达式依赖
<dependencies>
<!-- AspectJ是eclipse开源组织编写的一套强大的AOP框架
它拥有特殊的编译器和类加载器,因此可以在编译时创建
代理和类加载时创建代理,但由于Spring本身对AOP
的实现是基于运行时创建代理的,所以只能使用JDK和CGLIB
来创建代理,但Spring却使用了AspectJ的切入点表达式
以及相关的注解,使用起来更加的简单和方便 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.8</version>
<scope>compile</scope>
</dependency>
</dependencies>
第三:自定义配置类
package edu.nf.ch21.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* @Date 2023-10-13
* @Author hy
*/
@Configuration
@ComponentScan(basePackages = "edu.nf.ch21")
// 启用AspectJ注解处理器
// proxyTargetClass指定为ture表示强制使用CGLIB生成代理
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
}
第四:编写目标对象
@Slf4j
@Service
public class UserService {
public String add(String name){
log.info("添加用户----" + name);
//log.info("自定义异常" + 10/0);
return "success";
}
}
第五:实现
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService service = context.getBean(UserService.class);
service.add("simi");
}
}
二十、Aware接口
1、BeanNameAware 接口
实现将目标对象的bean名字注入到容器中
Aware接口(感知型接口),当spring容器发现某个bean实现了Aware接口以后,
- name就会为这个Bean注入一下容器核心对象,
- 比如某些业务场景中需要得到Bean的名字或者id时,可以通过此接口来获取
第一:自定义配置类
@Configuration
@ComponentScan(basePackages = "edu.nf.ch22.test1")
public class AppConfig {
}
第二:目标对象实现BeanNameAware接口
package edu.nf.ch22.test1.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.stereotype.Service;
/**
* @Date 2023-10-13
* @Author hy
*/
@Slf4j
@Service
public class UserService implements BeanNameAware {
/**
* bean的名字
*/
private String beanName;
/**
* 容器会通过set方法注入
* @param beanName
*/
@Override
public void setBeanName(String beanName) {
this.beanName = beanName;
}
public void add(){
log.info("添加用户,使用beanName:" + this.beanName);
}
}
第三:实现
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService service = context.getBean(UserService.class);
service.setBeanName("pig");
service.add();
}
}
2、ApplicationContextAware接口
将容器本身注入到容器中
实现ApplicationContextAware接口,使spring容器将感知到当前的bean注入需要注入的一个容器对象
第一:自定义配置类
@Configuration
@ComponentScan(basePackages = "edu.nf.ch22")
public class AppConfig {
}
第二:编写ApplicationContextholder
package edu.nf.ch22.config;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* 获取ApplicationContext容器
* @Date 2023-10-13
* @Author hy
*/
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
/**
* 声明容器
*/
private static ApplicationContext applicationContext;
/**
* 通过set方法将容器本身注入进来
* @param applicationContext
* @throws BeansException
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public static ApplicationContext getApplicationContext(){
return applicationContext;
}
/**
* 封装了容器getBean方法
* @param id
* @param clazz
* @return
* @param <T>
*/
public static <T> T getBean(String id, Class<T> clazz){
return applicationContext.getBean(id,clazz);
}
/**
* 封装了容器getBean方法
* 如果接口的实现类只有一个,可以不需要指定id,只需要指定类型即可
* @param clazz
* @return
* @param <T>
*/
public static <T> T getBean(Class<T> clazz){
return applicationContext.getBean(clazz);
}
}
第三:编写目标对象
@Service
@Slf4j
public class StuService {
public void add(){
//直接使用ApplicationContextHolder从容器中获取bean
UserService service = ApplicationContextHolder.getBean(UserService.class);
service.add();
}
}
第四:实现
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
StuService service = context.getBean(StuService.class);
service.add();
}
}
二十一、容器事件
把需要的任务发布给容器处理,基于事件的发布和订阅模型
第一:编写-配置类
@Configuration
@ComponentScan(basePackages = "edu.nf.ch23")
public class AppConfig {
}
第二:编写-自定义事件
package edu.nf.ch23.event;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Date 2023-10-13
* @Author hy
* 自定义事件对象,这个对象用于发布给spring容器,
* 容器就会自动处理这个事件
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MyEvent {
/**
* 事件消息
*/
private String message;
}
第三:编写-自定义事件监听器
package edu.nf.ch23.event;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* @Date 2023-10-13
* @Author hy
* 自定义事件监听器,用于监听用户发布的事件并进行处理,
* 监听器需要纳入容器管理
*/
@Component
@Slf4j
public class MyEventListener {
/**
* 自定义事件监听方法,容器会将用户发布的
* 事件对象传入这个方法中进行事件处理
* @param event
* @EventListener用于标识当前方法为监听方法
*/
@EventListener
public void handlerEvent(MyEvent event){
log.info("处理事件:" + event.getMessage());
}
}
第四、实现
public class Main {
public static void main(String[] args) {
ApplicationContext context = new
AnnotationConfigApplicationContext(AppConfig.class);
// 创建事件对象
MyEvent event = new MyEvent("hello,world");
// 向容器发布事件
context.publishEvent(event);
}
}
二十二、定时任务
定时任务线程池:可以定时执行任务
通常情况下,一个线程执行一个任务,多个线程执行多个任务,
当不定义线程的情况下,默认只有一个线程,
那么在多个任务时,一个线程执行完才会执行下一个线程,
第一:编写配置类
package edu.nf.ch24.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
/**
* @Date 2023-10-13
* @Author hy
*/
@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;
}
}
第二、编写-目标对象事件
@Slf4j
@Service
public class OrderService {
/**
* 备份
*/
public void backup(){
log.info("备份订单");
}
}
第三、编写-定时器
package edu.nf.ch24.task;
import edu.nf.ch24.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @Date 2023-10-13
* @Author hy
* 订单定时器,定时触发订单的备份逻辑
*/
@Component
@RequiredArgsConstructor
public class OrderTask {
private final OrderService service;
/**
* 定时任务的办法
* 使用@Scheduled注解标注当前方法为一个定时任务方法
* 并且使用cron表达式来设定执行的时间
*
* cron表达式说明:
* cron表达式主要由6项字符组成,每个字符中间使用空格隔开。
* 则6项字符分别代表:秒、分、时、日、月、周几
* 特殊字符"*" :匹配所有值
* 特殊字符"?":不关心,无所谓,通常用于匹配在周几中
* 特殊字符",":与
* 特殊字符"/":增量值
* 特殊字符"-":区间
* 例子:
* "0 * * * * ?" : 每分钟(当秒为0的时候)
* "0 0 * * * ?" : 每小时(当秒和分为0的时候)
* "0/5 * * * * ?":每5秒
* "0 5/15 * * * ?":增量执行,每小时的5分、20分、35分、50分
* "0 0 9,13 * * ?" : 每天的9点和13点
* "0 0 8-10 * * ?":每天的8点、9点、10点
* "0 0 0 25 12 ?":每年的12月25日0点0分0秒
* "0 30 10 * * ?":每天10点半
*/
@Scheduled(cron = "0/5 * * * * ?")
public void executeBackup(){
service.backup();
}
}
第四、实现
public class Main {
public static void main(String[] args) {
ApplicationContext context = new
AnnotationConfigApplicationContext(AppConfig.class);
// 定时不能让配置结束,设置死循环
while (true){}
}
}
二十三、mybatis整合Spring容器(CRUD)
业务注解 - 扫描
配置类 - 其他框架,mybatis、springmvc…整合
第一:添加依赖
<dependencies>
<!-- 添加druid数据源连接池依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>
<!--添加spring依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.27</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.27</version>
</dependency>
<!--添加mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!--添加日志依赖-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.3.8</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>
<!--添加mybatis整合spring的依赖jar -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
<!-- 添加分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.3</version>
</dependency>
</dependencies>
第二:编写实体类 - City
@Data
public class City {
private Integer cityId;
private String cityName;
private String cityCode;
private String province;
}
第三:创建目标对象的接口 - CityDao
public interface CityDao {
/**
* 根据省份查询
* @param province
* @return
*/
List<City> list(String province);
/**
* 分页查询
* @param pageNum
* @param pageSize
* @return
*/
List<City> page(@Param("pageNum") Integer pageNum,
@Param("pageSize") Integer pageSize);
}
第四: 编写配置类文件 - CityMapper.xml
单表条件查询
<?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">
<mapper namespace="org.example.ch02.dao.CityDao">
<resultMap id="cityMap" type="org.example.ch02.entity.City">
<id property="cityId" column="city_id"/>
<result property="cityName" column="city_name"/>
<result property="cityCode" column="city_code"/>
<result property="province" column="province"/>
</resultMap>
<select id="list" parameterType="string" resultMap="cityMap">
select city_id,city_name,city_code,province from city_info
where province = #{province}
</select>
<!--结合代理实现分页,不需要写limit,它会自动生成 -->
<select id="page" resultMap="cityMap">
select city_id,city_name,city_code,province from city_info
</select>
</mapper>
第五:编写目标对象的service接口 - CityService
public interface CityService {
// 根据省份查询
List<City> listCity(String province);
// 分页查询
PageInfo<City> page(Integer pageNum, Integer pageSize)
}
第六:编写service实现类 - CityServiceImpl
package org.example.ch02.service.impl;
import com.github.pagehelper.PageInfo;
import lombok.RequiredArgsConstructor;
import org.example.ch02.config.AppConfig;
import org.example.ch02.dao.CityDao;
import org.example.ch02.entity.City;
import org.example.ch02.service.CityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @Date 2023-10-16
* @Author hy
*/
@Service("cityService")
@RequiredArgsConstructor
public class CityServiceImpl implements CityService {
/**
* 这里注入的dao是一个代理对象
*/
private final CityDao cityDao;
@Override
public List<City> listCity(String province) {
return cityDao.list(province);
}
@Override
public PageInfo<City> page(Integer pageNum, Integer pageSize) {
List<City> list = cityDao.page(pageNum, pageSize);
// 将分页结果封装成PageInfo
return new PageInfo<>(list);
}
}
第七:编写配置文件 - druid.properties
配置连接和连接池属性
# 连接属性
driver = com.mysql.cj.jdbc.Driver
url = jdbc:mysql://localhost:3306/city
username = root
password = 123456
# 连接池属性
maxActive = 200
initialSize = 5
minIdle = 5
maxWait = 2000
minEvictableIdleTimeMillis = 300000
timeBetweenEvictionRunsMillis = 60000
testWhileIdle = true
testOnReturn = false
validationQuery = select 1
poolPreparedStatements = false
typeAliasesPackage = org.example.ch02.entity
mapperLocations = classpath:mappers/*xml
案例1:配置文件实现
第八:编写配置类文件 - beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">
<!--启用扫描 -->
<context:component-scan base-package="org.example.ch02"/>
<!-- 整合druid数据源连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 注入相关的连接属性 -->
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/city?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="maxActive" value="200"/>
<!-- 预先初始化5个连接放入连接池 -->
<property name="initialSize" value="5"/>
<!--最小空闲连接数(当连接池连接释放时,最少的连接数,与初始化连接数保存一致)-->
<property name="minIdle" value="5"/>
<!-- 获取连接的最大等待时间,单位:毫秒 (超出时间即抛出异常,不再等待)-->
<property name="maxWait" value="2000"/>
<!-- 连接保存空闲不被驱逐出连接池的最小时间 -->
<property name="minEvictableIdleTimeMillis" value="300000"/>
<!-- 检测连接的间隔时间,单位:毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<!-- 检测连接是否有效 -->
<property name="testWhileIdle" value="true"/>
<!-- 放回连接池时是否检查连接有效 -->
<property name="testOnReturn" value="false"/>
<!-- 定义一条伪SQL,用于检查连接是否可用 -->
<property name="validationQuery" value="select 1"/>
<!-- 是否缓存PreparedStatement,mysql建议关闭 -->
<property name="poolPreparedStatements" value="false"/>
</bean>
<!-- 装配SqlSessionFactory,也就是整合mybatis到spring,
关键点在于将mybatis的相关配置放在spring中配置,并由
容器来管理和创建SqlSessionFactory
-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--SqlSessionFactoryBean需要注入DataSource -->
<property name="dataSource" ref="dataSource"/>
<!--注入其他配置属性值-->
<!-- 指定实体类的包,用于创建别名 -->
<property name="typeAliasesPackage" value="org.example.ch02.entity"/>
<!-- mapper映射配置文件的路径 -->
<property name="mapperLocations" value="classpath:mappers/*xml"/>
<!-- 设置分页插件 -->
<property name="plugins">
<!-- 装配分页拦截器 -->
<bean class="com.github.pagehelper.PageInterceptor">
<!-- 给分页拦截器注入相关的属性 -->
<property name="properties">
<props>
<!-- 数据方言 -->
<prop key="helperDialect">mysql</prop>
<!-- 启用分页参数注解支持 -->
<prop key="supportMethodsArguments">true</prop>
<!-- 分页合理化 -->
<prop key="reasonable">true</prop>
</props>
</property>
</bean>
</property>
</bean>
<!-- 配置扫描dao接口所在的包,这样会利用动态代理
在运行时创建所有dao接口的实现类,并自动注入到spring容器中,
跟MapperScan作用一样 -->
<mybatis:scan base-package="org.example.ch02.dao"/>
</beans>
第九:配置文件实现
@Slf4j
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 从容器中获取动态创建的CityDao的代理对象
CityService service = context.getBean(CityService.class);
PageInfo<City> pageInfo = service.page(1, 5);
pageInfo.getList().forEach(city -> log.info(city.getCityName()));
}
}
案例2:注解结合配置类实现
第八:配置类
package org.example.ch02.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* @Date 2023-10-16
* @Author hy
*/
@Configuration
// 扫描dao接口的包,动态生成到接口的代理实现
// 等效于xml中的<mybatis:scan/>
@MapperScan(basePackages = "org.example.ch02.dao")
// 扫描指定的包,装配相关的Bean
@ComponentScan(basePackages = "org.example.ch02")
public class AppConfig {
/**
* 整合druid数据源连接池,装配DruidDataSource
* @param dataSource
* @return
*/
@Bean(initMethod = "init",destroyMethod = "close")
public DruidDataSource dataSource() throws Exception {
// 创建Properties对象
Properties prop = new Properties();
// 获取一个输入流来读取properties文件
InputStream input = AppConfig.class.getClassLoader()
.getResourceAsStream("druid.properties");
// 将输入流加载到properties独享中
prop.load(input);
// 通过DruidDataSourceFactory来创建
// DruidDataSource实例
DruidDataSource ds = (DruidDataSource) DruidDataSourceFactory
.createDataSource(prop);
return ds;
}
/**
* 整合mybatis,装配SqlSessionFactory
* @param dataSource
* @return
*/
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DruidDataSource dataSource) throws IOException {
// 创建SqlSessionFactoryBean
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
// 注入数据源
// 直接调用上面的方法
//factoryBean.setDataSource(dataSource());
// 以参数传递
factoryBean.setDataSource(dataSource);
// 注入mybatis其他设置
// 设置实体包,使用别名
factoryBean.setTypeAliasesPackage("org.example.ch02.entity");
// 设置mapper映射文件的路径
// 先创建一个资源路径解析器来查找源文件
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources("classpath:mappers/*.xml"));
// 设置分页插件,创建分页拦截器
PageInterceptor interceptor = new PageInterceptor();
// 设置属性
Properties properties = new Properties();
properties.setProperty("helperDialect","mysql");
properties.setProperty("supportMethodsArguments","true");
properties.setProperty("reasonable","true");
// 将分页属性设置到拦截器中
interceptor.setProperties(properties);
// 最后将分页拦截器设置到SqlSessionFactoryBean的插件中
factoryBean.setPlugins(interceptor);
// 返回SqlSessionFactoryBean给容器
return factoryBean;
}
}
第九:实现
@Slf4j
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 从容器中获取动态创建的CityDao的代理对象
CityService service = context.getBean(CityService.class);
PageInfo<City> pageInfo = service.page(1, 5);
pageInfo.getList().forEach(city -> log.info(city.getCityName()));
}
}
二十四、minio整合Spring容器
1、添加依赖
<dependencies>
<!-- minio客户端-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.4</version>
</dependency>
<!-- 添加servlet依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- 添加webmvc依赖,会将Spring的核心包一并依赖进来 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.23</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>
<!-- 如果使用了JSON作为响应数据,需要依赖Jackson包,
它是spring官方默认集成的JSON序列化工具 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.1</version>
</dependency>
<!-- 集成hibernate bean验证器 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.13.final</version>
</dependency>
<!-- 上传组件 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
</dependencies>
2、配置核心请求总控器
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!-- 配置核心请求总控器,负责接收所有的请求,并根据映射的url地址将请求分发给具体控制器的方法来处理 -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
3、整合Minio,装配MinioFactoryBean
WEB-INF/dispatcher-servlet.xml
最好使用配置类实现,xml配置在后面将不适用(死亡)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 扫描 -->
<context:component-scan base-package="edu.nf.ch08"/>
<!-- 启用mvc注解驱动 -->
<mvc:annotation-driven/>
<!-- 默认servlet处理静态资源处理 -->
<mvc:default-servlet-handler/>
<!-- 装配上传附件解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 限制文件上传的总大小(单位:字节),不配置此属性默认不限制 -->
<property name="maxUploadSize" value="104857600"/>
<!-- 设置文件上传的默认编码-->
<property name="defaultEncoding" value="utf-8"/>
</bean>
<!--整合Minio,装配MinioFactoryBean-->
<bean id="minioFactoryBean" class="edu.nf.ch08.fatory.MinioFactoryBean">
<property name="url" value="http://127.0.0.1:9000"/>
<property name="username" value="minioadmin"/>
<property name="password" value="minioadmin"/>
<property name="bucket" value="myapp"/>
</bean>
</beans>
4、创建实体类
package edu.nf.ch08.controller.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
/**
* 商品vo对象,用于保存页面提交的数据
* 后续将这个vo拷贝到entity中
* @Date 2023-10-24
* @Author hy
*/
@Data
public class ProductVO {
/**
* 商品名称
*/
private String productName;
/**
* MultipartFile是springmvc封装的上传附件对象
* 商品图片
*/
private MultipartFile[] file;
/**
* 图片大小
*/
private String size;
}
5、创建结果集类
package edu.nf.ch08.controller.vo;
import lombok.Data;
import org.springframework.http.HttpStatus;
/**
* @Date 2023-10-24
* @Author hy
*/
@Data
public class ResultVO <T>{
// 设置默认值为200
private Integer code = HttpStatus.OK.value();
private String message;
private T data;
}
6、实现文件上传和下载
package edu.nf.ch08.controller;
import edu.nf.ch08.controller.vo.ProductVO;
import edu.nf.ch08.controller.vo.ResultVO;
import io.minio.GetObjectArgs;
import io.minio.ListObjectsArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.messages.Item;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.InputStreamSource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.net.URLEncoder;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
/**
* @Date 2023-10-26
* @Author hy
*/
@Slf4j
@RestController
@RequiredArgsConstructor
public class MinioController {
/**
* 注入minioClient
*/
private final MinioClient minioClient;
/**
* 文件上传
* path路径参数(path)就是桶下面的子目录
*
* 直接将本地文件上传到远程minio,一次读写
* @param path 上传文件路径
* @param files 上传文件
* @return
*/
@PostMapping("/upload/{path}")
public ResultVO upload(@PathVariable("path") String path,MultipartFile[] files) throws Exception {
// 循环遍历上传的附件
for (MultipartFile file : files) {
// 获取文件名
String filName = file.getOriginalFilename();
// 获取文件的输入流读取文件内容
InputStream inputStream = file.getInputStream();
// 将文件上传到minio服务器
minioClient.putObject(PutObjectArgs.builder()
.bucket("myapp")
// 远程上传的路径
.object(path + "/" + filName)
// 设置一个输入流,-1表示读到文件的末尾
.stream(inputStream, file.getSize(), -1)
// 文件的类型
.contentType(file.getContentType())
.build());
}
return new ResultVO();
}
/**
* 文件下载
*
* @param path 远程文件夹
* @param fileName 文件名
* @return
* @throws Exception
*/
@GetMapping("/download/{path}/{fileName}")
public ResponseEntity<InputStreamSource> download(
@PathVariable("path") String path,
@PathVariable("fileName") String fileName) throws Exception{
// 根据文件名从minio获取一个远程的输入流
InputStream inputStream = minioClient.getObject(
GetObjectArgs.builder()
.bucket("myapp")
// 远程文件路径
.object(path + "/" + fileName)
.build());
// 设置响应头,告诉浏览器响应流数据
HttpHeaders headers = new HttpHeaders();
// 对文件名进行编码,防止在响应头中出现乱码
fileName = URLEncoder.encode(fileName,"UTF-8");
// 设置头信息,将响应内容处理的方式设置为附件下载
headers.setContentDispositionFormData("attachment",fileName);
// 设置响应类型为流类型
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// 创建InputStreamResource对象封装输入流,用于读取服务器文件
InputStreamSource isr = new InputStreamResource(inputStream);
// 创建ResponseEntity对象(封装InputStreamResource、响应头、响应状态码)
return new ResponseEntity<>(isr,headers, HttpStatus.CREATED);
}
/**
* 根据路径目录查询文件列表
* 从远程桶中查询
* @param path
* @return
*/
@GetMapping("/list/{path}")
public ResultVO<List<String>> listFiles(@PathVariable("path") String path){
//List<String> list = new ArrayList<>();
List<ProductVO> list = new ArrayList<>();
// 使用minio获取列表
minioClient.listObjects(ListObjectsArgs
.builder()
.bucket("myapp")
// 要浏览的文件夹
.prefix(path + "/")
.build())
// 循环遍历集合,每一个itemResult都封装了一个文件信息
.forEach(itemResult -> {
// 获取文件名保存到list集合中
try {
Item item = itemResult.get();
// 获取文件大小
long size = item.size();
// 获取文件名
String fileName =item.objectName();
// 去掉前缀(目录)
fileName = StringUtils.getFilename(fileName);
// 创建商品对象
ProductVO productVO = new ProductVO();
// 添加属性
productVO.setProductName(fileName);
// 调用方法设置文件大小
productVO.setSize(formatSize(size));
// 加入到list集合中
list.add(productVO);
} catch (Exception e){
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
});
ResultVO vo = new ResultVO();
vo.setData(list);
return vo;
}
public String formatSize(long size){
DecimalFormat df = new DecimalFormat("#.00");
String fileSize = "";
if(size == 0){
return fileSize;
}
if(size <= 1024) {
fileSize = size + "B" ;
}else if(size <= 1048576){
fileSize = df.format((double)size / 1024) + "KB" ;
}else if (size <= 1073741824){
fileSize = df.format((double)size / 1048576) + "MB" ;
}else{
fileSize = df.format((double)size / 1073741824) + "GB" ;
}
return fileSize;
}
}
7、自定义MinioFactoryBean工厂
整合第三方的接口
package edu.nf.ch08.fatory;
import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.FactoryBean;
/**
* @Date 2023-10-26
* @Author hy
*/
@Setter
@Slf4j
public class MinioFactoryBean implements FactoryBean<MinioClient> {
/**
* 注入相关的配置属性
*/
// 访问的url地址
private String url ;
// 账户
private String username;
// 密码
private String password;
// 桶
private String bucket;
// 创建MinioClient对象
private static MinioClient minioClient;
/**
* 创建Bean,实例化MinioClient
* @return
* @throws Exception
*/
@Override
public MinioClient getObject() throws Exception {
// 创建minioClient
minioClient = MinioClient.builder()
.endpoint(url)
.credentials(username,password)
.build();
// 初始化桶
initBucket(minioClient);
log.info("已初始化桶:" + bucket);
return minioClient;
}
/**
* 返回Bean实例的Class对象
* @return
*/
@Override
public Class<?> getObjectType() {
return MinioClient.class;
}
/**
* 初始化桶
* @param minioClient
* @throws Exception
*/
private void initBucket(MinioClient minioClient) throws Exception {
// 先判断桶是否存在,不存在则创建
if(!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build())){
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
}
}
}
8、页面实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/jquery-1.12.4.js"></script>
</head>
<body>
<h2>文件上传</h2>
<form id="f1" enctype="multipart/form-data">
文件:<input type="file" name="files" multiple/>
<input type="button" value="上传"/>
</form>
<!--显示上传的文件-->
<div id="list"></div>
<script>
// 查询文件列表
function listFiles(path){
$.ajax({
url: '../list/' + path,
type: 'get',
success:function (result){
if(result.code == 200){
$('#list').empty();
$.each(result.data,function (index,obj){
console.info(obj);
$('#list').append(`<a href='../download/images/${obj.productName}'>` + obj.productName + '--'+ obj.size +`</a><br>`);
});
}
}
})
}
$(function (){
// 页面加载时查询一次列表
listFiles('images');
$(':button').on('click',function (){
let formData = new FormData($('#f1')[0]);
$.ajax({
url: '../upload/images',
type: 'post',
data: formData,
processData: false,
contentType: false,
success: function (result){
if(result.code == 200){
listFiles('images');
}else{
alert("上传失败")
}
}
});
});
})
</script>
</body>
</html>
二十五、错误码
500
发生自定义异常时,也会报500错误,这时应该查看idea的控制台输出
参数值不为空仍报500的情况,Controller层的注入注解为@Controller,应该改为@RestController;
数据库修改成功仍报500的情况,检查返回值类型是否一致; 例如:dao有返回值,controller层却没有输出返回值。
503
排除其他资源配置问题,@Serice、@Repository等注入注解放在接口类中,应该改为放在接口的实现类中;
404
路径错误
输入正确页面路径仍报404, 考虑静态资源处理问题
401
未认证,用户未登录
403
没有权限,用户没有相关权限