这篇文章中我们将会介绍Spring的框架以及本体内容,包括核心容器,注解开发,AOP以及事务等内容
那么简单说明一下Spring的必要性:
Spring的核心内容:
Spring可进行的框架整合:
在接下来的文章中,我们会学习Spring的框架思想,学习Spring的基本操作,结合案例熟练掌握
温馨提醒:在学习本篇文章前请先学习JavaWeb相关内容
(HTTP,Tomcat,Servlet,Request,Response,MVC,Cookie,Session,Ajax,Vue等内容)
Spring发展至今已经形成了一套开发的生态圈,Spring提供了相当多的项目,每个项目用于完成特定功能
我们常用的主流技术包括有:
在系统学习Spring之前,我们需要先来了解FrameWork系统结构
我们现在所使用的Spring FrameWork是4.0版本,已经趋于稳定
下面我们对架构图进行解释:
我们可以在官方中获得如此评价:
首先我们思索一下我们之前的业务层与数据层:
// 数据层接口
public interface BookDao {
public void save();
}
// 数据层实现
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
}
// 业务层接口
public interface BookService {
public void save();
}
// 业务层实现
public class BookServiceImpl implements BookService {
private BookDao bookDao;
public void save() {
bookDao.save();
}
}
如果我们修改BookDaoImpl内容,那么相对应的业务层实现中的bookDao的new实现也要进行修改,甚至下方方法的对象也要进行修改
代码书写现状:
解放方案:
IoC(Inversion of Control)控制反转思想:
DI(Dependency Injection)依赖注入:
Spring技术对Ioc思想进行了实现:
Spring提供了一个容器,被称为Ioc容器,用来充当IoC思想的外部
IoC容器负责对象的创建,初始化等一系列工作,被创建和管理的对象在IoC容器中被称为Bean
// 数据层实现
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save …");
}
}
// IoC容器
/*
包含
dao
service
两者可以建立连接
*/
// 业务层实现
public class BookServiceImpl implements BookService {
private BookDao bookDao;
public void save() {
bookDao.save();
}
}
目的:充分解耦
最终效果:
首先我们需要明白IoC的使用规则:
下面我们给出IoC入门的详细步骤:
创建Maven项目,在pom.xml中导入坐标
创建Spring.xml的配置包(applicationContext.xml,导入坐标后xml中更新该XML)
<!--2.配置bean-->
<!--bean标签标示配置bean
id属性标示给bean起名字
class属性表示给bean定义类型(注意需要是实现类)-->
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl"/>
主函数
package com.itheima;
import com.itheima.dao.BookDao;
import com.itheima.service.BookService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App2 {
public static void main(String[] args) {
//3.获取IoC容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
//4.获取bean(根据bean配置id获取)
//BookDao bookDao = (BookDao) ctx.getBean("bookDao");
//bookDao.save();// 注意:需要类型转化
BookService bookService = (BookService) ctx.getBean("bookService");
bookService.save();
}
}
首先我们需要明白DI的使用规则:
下面我们给出DI入门的详细步骤(基于IoC入门):
删除new方法
public class BookServiceImpl implements BookService {
//5.删除业务层中使用new的方式创建的dao对象
private BookDao bookDao;
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}
创建对象的set方法
public class BookServiceImpl implements BookService {
//5.删除业务层中使用new的方式创建的dao对象
private BookDao bookDao;
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
//6.提供对应的set方法
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
}
创建Dao和Service的连接
<!--2.配置bean-->
<!--
bean标签标示配置bean
id属性标示给bean起名字
class属性表示给bean定义类型
-->
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<!--7.配置server与dao的关系-->
<!--
注意:在server中配置关系
property标签表示配置当前bean的属性
name属性表示配置哪一个具体的属性
ref属性表示参照哪一个bean
-->
<property name="bookDao" ref="bookDao"/>
</bean>
Bean是保存在IoC中的对象,我们通过配置的方式获得Bean
下面我们从三个方面分别讲解Bean:
首先我们先介绍bean本身性质:
类别
描述
名称
bean
类型
标签
所属
beans标签
功能
定义Spring核心容器管理对象
格式
属性列表
id:bean的id,使用容器可以通过id值获得对应的bean,在一个容器中id值唯一
class:bean的类型,即配置的bean的全路径类名
范例
然后我们介绍一下bean的别名:
类别
描述
名称
name
类型
标签
所属
bean标签
功能
定义bean的别名,可定义多个,使用逗号,分号,空格分隔
范例
正常情况下,使用id和name都可以获得bean,但推荐还是使用唯一id
获得bean无论通过id还是name获取,如果无法找到则抛出异常NosuchBeanDefinitionException
最后我们介绍一下bean的作用范围scope:
类别
描述
名称
scope
类型
标签
所属
bean标签
功能
定义bean的作用范围,可选范围如下:
singleton:单列(默认)
prototype:非单列
范例
这里的scope指产生对象的数量
我们的scope在默认情况下是singleton,因为很多对象只需要创建一次,多次创建会导致内存膨胀
合适交给容器进行管理的bean(singleton):
- 表现层对象
- 业务层对象
- 数据层对象
- 工具对象
不合适交给容器进行管理的bean(prototype):
- 封装实体的域对象(带有状态的bean)
bean的实例化通常分为四种方法,我们在下面一一介绍:
我们需要在数据类中提供构造方法,配置条件中不需要改变
// 数据类
public class BookDaoImpl implements BookDao {
public BookDaoImpl() {
System.out.println("book dao constructor is running ....");
}
public void save() {
System.out.println("book dao save ...");
}
}
<?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">
<!--方式一:构造方法实例化bean-->
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
</beans>
若无参构造方法不存在,则抛出异常BeanCreationException
我们在之前的案例中存在有对象工厂的说法,我们可以设置工厂并调用其方法得到bean
// 静态工厂
package com.itheima.factory;
import com.itheima.dao.OrderDao;
import com.itheima.dao.impl.OrderDaoImpl;
//静态工厂创建对象
public class OrderDaoFactory {
public static OrderDao getOrderDao(){
System.out.println("factory setup....");
return new OrderDaoImpl();
}
}
<?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">
<!--方式二:使用静态工厂实例化bean-->
<bean id="orderDao" class="com.itheima.factory.OrderDaoFactory" factory-method="getOrderDao"/>
</beans>
和静态工厂相同,但不同点是方法不是静态,我们需要提前创建一个bean
// 实例工厂
package com.itheima.factory;
import com.itheima.dao.UserDao;
import com.itheima.dao.impl.UserDaoImpl;
//实例工厂创建对象
public class UserDaoFactory {
public UserDao getUserDao(){
return new UserDaoImpl();
}
}
<?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">
<!--方式三:使用实例工厂实例化bean-->
<bean id="userFactory" class="com.itheima.factory.UserDaoFactory"/>
<!--
factory-bean:实例工厂本身bean
factory-method:使用调用bean的方法
-->
<bean id="userDao" factory-method="getUserDao" factory-bean="userFactory"/>
</beans>
除了我们之前自己定义的工厂外,Spring提供了一种官方版本的FactoryBean
// FactoryBean工厂(需接口,< >中填写数据类接口)
package com.itheima.factory;
import com.itheima.dao.UserDao;
import com.itheima.dao.impl.UserDaoImpl;
import org.springframework.beans.factory.FactoryBean;
//FactoryBean创建对象
public class UserDaoFactoryBean implements FactoryBean<UserDao> {
//代替原始实例工厂中创建对象的方法
// 返回创建对象类型为UserDaoImpl()
public UserDao getObject() throws Exception {
return new UserDaoImpl();
}
// 这里填写接口类型
public Class<?> getObjectType() {
return UserDao.class;
}
// 可以修改来修改其scope属性
public boolean isSingleton() {
return false;
}
}
<?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">
<!--方式四:使用FactoryBean实例化bean-->
<bean id="userDao" class="com.itheima.factory.UserDaoFactoryBean"/>
</beans>
我们先来接单介绍生命周期相关概念:
接下来我们介绍生命周期控制方法:
由数据层提供方法,在xml配置文件中设置该方法
// 数据层
package com.itheima.dao.impl;
import com.itheima.dao.BookDao;
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
//表示bean初始化对应的操作
public void init(){
System.out.println("init...");
}
//表示bean销毁前对应的操作
public void destory(){
System.out.println("destory...");
}
}
<!--配置文件-->
<?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">
<!--init-method:设置bean初始化生命周期回调函数-->
<!--destroy-method:设置bean销毁生命周期回调函数,仅适用于单例对象-->
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl" init-method="init" destroy-method="destory"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
</bean>
</beans>
Spring为创建提供了两个接口,我们只需要继承并实现该方法即可
package com.itheima.service.impl;
import com.itheima.dao.BookDao;
import com.itheima.service.BookService;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
// InitializingBean,DisposableBean 分别对应afterPropertiesSet,destroy方法,代表创建和销毁
public class BookServiceImpl implements BookService, InitializingBean, DisposableBean {
private BookDao bookDao;
public void setBookDao(BookDao bookDao) {
System.out.println("set .....");
this.bookDao = bookDao;
}
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
public void destroy() throws Exception {
System.out.println("service destroy");
}
public void afterPropertiesSet() throws Exception {
System.out.println("service init");
}
}
<?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">
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl" init-method="init" destroy-method="destory"/>
<!--直接调用即可-->
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
</bean>
</beans>
我们需要提及一下bean的销毁时机:(了解即可)
所以如果我们希望销毁bean观察到destroy的实现,需要手动关闭:
手动关闭容器方法:
package com.itheima;
import com.itheima.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AppForLifeCycle {
public static void main( String[] args ) {
// 注意:这里需要采用ClassPathXmlApplicationContext作为对象,因为只有这个类才具有close方法
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
bookDao.save();
//关闭容器
ctx.close();
}
}
注册关闭钩子,在虚拟机退出前先关闭容器再推出虚拟机
package com.itheima;
import com.itheima.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AppForLifeCycle {
public static void main( String[] args ) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
bookDao.save();
//注册关闭钩子函数,在虚拟机退出之前回调此函数,关闭容器
ctx.registerShutdownHook();
}
}
最后我们统计一下整体生命周期:
首先我们要知道类中传递数据的方法有两种:
然后我们要知道数据的类型大体分为两种:
所以我们把依赖注入方式分为四种:
setter注入
构造器注入
首先我们需要在bean种定义简单类型属性并提供可以访问的set方法
package com.itheima.dao.impl;
import com.itheima.dao.BookDao;
public class BookDaoImpl implements BookDao {
private String databaseName;
private int connectionNum;
//setter注入需要提供要注入对象的set方法
public void setConnectionNum(int connectionNum) {
this.connectionNum = connectionNum;
}
//setter注入需要提供要注入对象的set方法
public void setDatabaseName(String databaseName) {
this.databaseName = databaseName;
}
public void save() {
System.out.println("book dao save ..."+databaseName+","+connectionNum);
}
}
然后在配置中使用property标签value属性注入简单类型数据
<?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">
<!--注入简单类型-->
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<!--property标签:设置注入属性-->
<!--name属性:设置注入的属性名,实际是set方法对应的名称-->
<!--value属性:设置注入简单类型数据值-->
<property name="connectionNum" value="100"/>
<property name="databaseName" value="mysql"/>
</bean>
</beans>
首先我们需要在bean种定义引用类型属性并提供可以访问的set方法
package com.itheima.service.impl;
import com.itheima.dao.BookDao;
import com.itheima.dao.UserDao;
import com.itheima.service.BookService;
public class BookServiceImpl implements BookService{
private BookDao bookDao;
private UserDao userDao;
//setter注入需要提供要注入对象的set方法
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
//setter注入需要提供要注入对象的set方法
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
public void save() {
System.out.println("book service save ...");
bookDao.save();
userDao.save();
}
}
然后在配置中使用property标签ref属性注入引用类型数据
<?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">
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<property name="connectionNum" value="100"/>
<property name="databaseName" value="mysql"/>
</bean>
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/>
<!--注入引用类型-->
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<!--property标签:设置注入属性-->
<!--name属性:设置注入的属性名,实际是set方法对应的名称-->
<!--ref属性:设置注入引用类型bean的id或name-->
<property name="bookDao" ref="bookDao"/>
<property name="userDao" ref="userDao"/>
</bean>
</beans>
在bean中定义简单类型属性并提供可访问的set方法
public class BookDaoImpl implements BookDao{
private int connectionNumber;
pubilc void setConnectionNumber(int connectionNumber){
this.connectionNumber = connectionNumber;
}
}
配置中使用constructor-arg标签value属性注入简单类型数据
<?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">
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
根据构造方法参数名称注入
<constructor-arg name="connectionNum" value="10"/>
</bean>
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/>
</beans>
在bean中定义引用类型属性并提供可访问的构造方法
public class BookDaoImpl implements BookDao{
private BookBao bookBao;
pubilc void setConnectionNumber(int connectionNumber){
this.bookBao = bookBao;
}
}
配置中使用constructor-arg标签ref属性注入简单类型数据
<?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">
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<constructor-arg name="userDao" ref="userDao"/>
<constructor-arg name="bookDao" ref="bookDao"/>
</bean>
</beans>
在前面我们已经介绍了构造器的注入方法
但如果我们在bean中的数据名称发生改变,配置就不再适配,所以提供了一些方法来解决参数配置问题:
配置中使用constructor-arg标签type属性设置按形参类型注入
<!--解决形参名称的问题,与形参名不耦合-->
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
根据构造方法参数类型注入
<constructor-arg type="int" value="10"/>
<constructor-arg type="java.lang.String" value="mysql"/>
</bean>
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<constructor-arg name="userDao" ref="userDao"/>
<constructor-arg name="bookDao" ref="bookDao"/>
</bean>
配置中使用constructor-arg标签index属性设置按形参类型注入
<!--解决参数类型重复问题,使用位置解决参数匹配-->
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<!--根据构造方法参数位置注入-->
<constructor-arg index="0" value="mysql"/>
<constructor-arg index="1" value="100"/>
</bean>
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<constructor-arg name="userDao" ref="userDao"/>
<constructor-arg name="bookDao" ref="bookDao"/>
</bean>
依赖注入方式有以下选择标准:
在前面我们学习了手动注入的方法,但Spring其实为我们提供了一种依赖自动装配的语法:
自动装配方式:
自动装配语法:
<?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">
<bean class="com.itheima.dao.impl.BookDaoImpl"/>
<!--autowire属性:开启自动装配,通常使用按类型装配-->
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl" autowire="byType"/>
</beans>
依赖自动装配特征:
除了基本类型和引入类型外,我们有时也需要注入集合
下面我们简单介绍一下结合的注入:
// 数据类
package com.itheima.dao.impl;
import com.itheima.dao.BookDao;
import java.util.*;
public class BookDaoImpl implements BookDao {
private int[] array;
private List<String> list;
private Set<String> set;
private Map<String,String> map;
private Properties properties;
public void setArray(int[] array) {
this.array = array;
}
public void setList(List<String> list) {
this.list = list;
}
public void setSet(Set<String> set) {
this.set = set;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
public void save() {
System.out.println("book dao save ...");
System.out.println("遍历数组:" + Arrays.toString(array));
System.out.println("遍历List" + list);
System.out.println("遍历Set" + set);
System.out.println("遍历Map" + map);
System.out.println("遍历Properties" + properties);
}
}
<!--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">
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<!--数组注入-->
<!--
注意:
name:对应实现类中的内部成员名称
<>里的array等为固定词汇
-->
<property name="array">
<array>
<value>100</value>
<value>200</value>
<value>300</value>
</array>
</property>
<!--list集合注入-->
<property name="list">
<list>
<value>itcast</value>
<value>itheima</value>
<value>boxuegu</value>
<value>chuanzhihui</value>
</list>
</property>
<!--set集合注入-->
<property name="set">
<set>
<value>itcast</value>
<value>itheima</value>
<value>boxuegu</value>
<value>boxuegu</value>
</set>
</property>
<!--map集合注入-->
<property name="map">
<map>
<entry key="country" value="china"/>
<entry key="province" value="henan"/>
<entry key="city" value="kaifeng"/>
</map>
</property>
<!--Properties注入-->
<property name="properties">
<props>
<prop key="country">china</prop>
<prop key="province">henan</prop>
<prop key="city">kaifeng</prop>
</props>
</property>
</bean>
</beans>
针对一个新的数据源对象,我们采用两步来创建bean(我们以druid为案例):
导入druid坐标
<groupId>com.itheima</groupId>
<artifactId>spring_09_datasource</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!--这里导入druid坐标-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
</dependencies>
配置数据源对象作为Spring管理的bean
这个案例我们将会介绍如何加载properties文件,并将文件带入到property基本信息中
我们大致将步骤分为以下三步:
开辟context命名空间:
使用context命名空间,加载指定properties文件
使用${}读取加载的属性值
除了上述的基本操作,我们在context命名空间的使用中有很多需要注意的点:
不加载系统属性
加载多个properties文件
加载所有properties文件
加载properties文件标准格式
从类路径或jar包中搜索并加载properties文件
前面已经完成bean与依赖注入的相关知识学习,接下来我们主要学习的是IOC容器中的核心容器。
这里所说的核心容器,大家可以把它简单的理解为ApplicationContext,接下来我们从以下几个问题入手来学习下容器的相关知识:
案例中创建ApplicationContext的方式为(类路径下的XML配置文件):
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
除了上面这种方式,Spring还提供了另外一种创建方式为(文件的绝对路径):
ApplicationContext ctx = new FileSystemXmlApplicationContext("D:\\workspace\\spring\\spring_10_container\\src\\main\\resources\\applicationContext.xml");
方式一,就是目前案例中获取的方式:
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
这种方式存在的问题是每次获取的时候都需要进行类型转换
方式二:
BookDao bookDao = ctx.getBean("bookDao",BookDao.class);
这种方式可以解决类型强转问题,但是参数又多加了一个,相对来说没有简化多少。
方式三:
BookDao bookDao = ctx.getBean(BookDao.class);
这种方式就类似我们之前所学习依赖注入中的按类型注入。必须要确保IOC容器中该类型对应的bean对象只能有一个。
下面我们给出容器的层次图
使用BeanFactory来创建IOC容器的具体实现方式为:
public class AppForBeanFactory {
public static void main(String[] args) {
Resource resources = new ClassPathResource("applicationContext.xml");
BeanFactory bf = new XmlBeanFactory(resources);
BookDao bookDao = bf.getBean(BookDao.class);
bookDao.save();
}
}
为了更好的看出BeanFactory和ApplicationContext之间的区别,在BookDaoImpl添加如下构造函数:
public class BookDaoImpl implements BookDao {
public BookDaoImpl() {
System.out.println("constructor");
}
public void save() {
System.out.println("book dao save ..." );
}
}
如果不去获取bean对象,打印会发现:
BeanFactory是延迟加载,只有在获取bean对象的时候才会去创建
ApplicationContext是立即加载,容器加载的时候就会创建bean对象
ApplicationContext要想成为延迟加载,只需要按照如下方式进行配置
<?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">
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl" lazy-init="true"/>
</beans>
接下来我们对前面知识的一个总结,共包含如下内容:
在上述的开发中,我们采用xml配置文件的形式来说依旧显得有些复杂
这时我们就需要发挥Spring的优点:简化开发,通过注解来简化开发过程
下面我们会通过多个方面将Bean逐步转化为注解
在前面的内容中,我们的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"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--原生bean-->
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
</beans>
在后期,我们的bean可以采用注解的形式,直接在实现类中注解表示为bean
我们采用@Component定义bean,可以添加参数表示id,也可以不添加参数,后期我们采用class类的类型来进行匹配
package com.itheima.dao.impl;
import com.itheima.dao.BookDao;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
//@Component定义bean
@Component("bookDao")
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
}
package com.itheima.service.impl;
import com.itheima.dao.BookDao;
import com.itheima.service.BookService;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
//@Component定义bean
@Component
public class BookServiceImpl implements BookService {
private BookDao bookDao;
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}
@Componenty延伸出了三种类型,在实现手法上是一致,但可以具体使用于各种类中(仅用于自我识别)
@Controller:用于表现层bean定义
@Service:用于业务层bean定义
@Repository:用于数据层定义
package com.itheima.dao.impl;
import com.itheima.dao.BookDao;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
//@Component定义bean
//@Component("bookDao")
//@Repository:@Component衍生注解
@Repository("bookDao")
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save …");
}
}
package com.itheima.service.impl;
import com.itheima.dao.BookDao;
import com.itheima.service.BookService;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
//@Component定义bean
//@Component
//@Service:@Component衍生注解
@Service
public class BookServiceImpl implements BookService {
private BookDao bookDao;
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}
但是,在上述情况下,即使我们将@Component的类定义为bean
我们的xml文件是无法探测到的,所以我们需要配置相关扫描组件来扫描bean
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
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
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--
<context:component-scan />:表示扫描文件
base-package:表示扫描路径
-->
<context:component-scan base-package="com.itheima"/>
</beans>
我们前面所提到的注解开发属于2.5的附属版本
在Spring3.0版本,Spring就提供了纯注解开发模式,利用java类代替配置文件,开启了Spring快速开发时代
在之前我们的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"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
</beans>
但是我们可以通过创建单独的类来表示配置文件:
@Configuration:用于声明当前类为Spring配置类
@ComponentScan:用于扫描类文件(类似于
package com.itheima.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
//声明当前类为Spring配置类
@Configuration
//设置bean扫描路径,多个路径书写为字符串数组格式
@ComponentScan({"com.itheima.service","com.itheima.dao"})
public class SpringConfig {
}
注意:因为该类属于配置类,我们通常单列一个文件夹来表示
常用文件夹:config
命名规范:SpringConfig,UserConfig…
因为我们的开发不再依靠于xml配置文件,所以在主函数中的Spring容器获得方式也发生了改变:
package com.itheima;
import com.itheima.dao.BookDao;
import com.itheima.service.BookService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
public static void main(String[] args) {
// 这是我们之前的获取方式,采用路径获取xml文件
// ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
// 这是新的获取方式,直接提供配置类的类型
// AnnotationConfigApplicationContext加载Spring配置类初始化Spring容器
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
// 后面操作无需变化
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
System.out.println(bookDao);
//按类型获取bean
BookService bookService = ctx.getBean(BookService.class);
System.out.println(bookService);
}
}
既然我们的Bean开发从xml转移到注解开发,那么一些配置设置同样发生改变
首先我们介绍Scope范围的设置方式:
@Scope:定义bean的作用范围
package com.itheima.dao.impl;
import com.itheima.dao.BookDao;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Repository;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@Repository
//@Scope设置bean的作用范围(singleton或prototype),可以不添加默认singleton
@Scope("singleton")
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
}
然后我们介绍一下bean生命周期的init和destroy操作:
在Spring3.0中,省略掉了前面繁琐的依赖注入,我们的bean依赖注入只留下了自动装配这一操作:
使用@Autowired注解开启自动装配模式(按类型)
当存在相同类型时,我们采用@Qualifier开启按名自动装配
package com.itheima.service.impl;
import com.itheima.dao.BookDao;
import com.itheima.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class BookServiceImpl implements BookService {
//@Autowired:注入引用类型,自动装配模式,默认按类型装配
@Autowired
//@Qualifier:自动装配bean时按bean名称装配
@Qualifier("bookDao")
private BookDao bookDao;
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}
注意:自动装配基于反射设计创建对象并暴力反射对应属性为私有属性初始化数据,因此无需提供setter方法
注意:自动转配建议使用无参构造方法创建对象(默认),如果不提供对应构造方法,请提供唯一的构造方法
注意:@Qualifier是基于@Autowired实现的,必须保证先有Autowired才能存在Qualifier
除了上述的bean类型装配,我们的简单类型装配依旧存在:
我们采用@Value的形式来配置简单类型的值
package com.itheima.dao.impl;
import com.itheima.dao.BookDao;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;
@Repository("bookDao")
public class BookDaoImpl implements BookDao {
//@Value:注入简单类型(无需提供set方法)
@Value("123")
private String name;
public void save() {
System.out.println("book dao save ..." + name);
}
}
之所以使用@Value的形式配置,是因为我们的类型值不一定是由手动输入的,有可能来自于Properties资源:
首先我们需要在Springconfig中配置相关资源
package com.itheima.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@ComponentScan("com.itheima")
//@PropertySource加载properties配置文件
@PropertySource({"jdbc.properties"})
public class SpringConfig {
}
然后我们在数据层调用时,采用${}来匹配数据
package com.itheima.dao.impl;
import com.itheima.dao.BookDao;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;
@Repository("bookDao")
public class BookDaoImpl implements BookDao {
//@Value:注入简单类型(无需提供set方法)
@Value("${name}")
private String name;
public void save() {
System.out.println("book dao save ..." + name);
}
}
我们在实际开发中不仅仅需要对自己的bean进行管理,有时候可能需要引进其他的bean
下面我们以Druid为例进行讲解:
首先在pom.xml中导入Druid坐标
使用@Bean配置第三方Bean
// 该bean同样属于config文件,我们同样放置在config文件夹下
// 在后续我们将会讲解如何进行连接
package com.itheima.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.itheima.dao.BookDao;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
public class JdbcConfig {
// 1.定义一个方法获得要管理的对象
// 2.添加@Bean,表示当前方法的返回值是一个bean
// @Bean修饰的方法,形参根据类型自动装配
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/spring_db");
ds.setUsername("root");
ds.setPassword("123456");
return ds;
}
}
将独立的配置类加入核心配置(导入法)
// SpringConfig
package com.itheima.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import javax.sql.DataSource;
@Configuration
@ComponentScan("com.itheima")
//@Import:导入配置信息(如果需要多个,同样采用{}数组形式)
@Import({JdbcConfig.class})
public class SpringConfig {
}
// JdbcConfig
package com.itheima.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.itheima.dao.BookDao;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
//@Configuration
public class JdbcConfig {
//@Bean修饰的方法,形参根据类型自动装配
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
// 配置信息
return ds;
}
}
注意:除了上述的导入法外还存在有其他方法,但导入法属于主流,因此我们不介绍其他流派,感兴趣的同学可以去查阅一下
我们的第三方bean也可能需要导入部分资源,下面我们进行简单介绍:
简单类型依赖注入
package com.itheima.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.itheima.dao.BookDao;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
//@Configuration
public class JdbcConfig {
//1.定义一个方法获得要管理的对象
@Value("com.mysql.jdbc.Driver")
private String driver;
@Value("jdbc:mysql://localhost:3306/spring_db")
private String url;
@Value("root")
private String userName;
@Value("root")
private String password;
//2.添加@Bean,表示当前方法的返回值是一个bean
//@Bean修饰的方法,形参根据类型自动装配
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
}
依赖类型依赖注入
package com.itheima.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.itheima.dao.BookDao;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
public class JdbcConfig {
@Bean
public DataSource dataSource(BookDao bookDao){
// 我们只需要调用即可,系统会为我们自动装配
System.out.println(bookDao);
}
}
引入类型注入只需要为bean定义方法设置形参即可,容器会根据类型自动装配对象
最后我们通过和前述非注解开发的对比来结束这一章节:
功能
XML配置
注解
定义bean
bean标签:
- id标签
- class标签
@Component
- @controller
- @Service
- @Repository
@ComponentScan
设置依赖注入
Setter注入
构造器注入
自动装配
配置第三方bean
bean标签
静态工厂
实例工厂
FactoryBean
作用范围
scope属性
生命周期
标准接口
init-method
destroy-method
在前面的内容中我们已经学习了Spring的Framework的大部分内容
接下来让我们来整合我们之前所学习的内容,整体的运用Spring来简化操作
首先我们来详细讲解MyBatis的整合
在整合之前,我们回忆一下MyBatis的单体操作:
首先我们需要准备数据库内容(这里不做展示)
连接数据库配置文件
与数据库相关的实体类
package com.itheima.domain;
import java.io.Serializable;
public class Account implements Serializable {
private Integer id;
private String name;
private Double money;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
}
数据层(这里全做注解,采取Mapper全权管理的形式)
package com.itheima.dao;
import com.itheima.domain.Account;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
public interface AccountDao {
@Insert("insert into tbl_account(name,money)values(#{name},#{money})")
void save(Account account);
@Delete("delete from tbl_account where id = #{id} ")
void delete(Integer id);
@Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ")
void update(Account account);
@Select("select * from tbl_account")
List<Account> findAll();
@Select("select * from tbl_account where id = #{id} ")
Account findById(Integer id);
}
服务层
package com.itheima.service.impl;
import com.itheima.dao.AccountDao;
import com.itheima.domain.Account;
import com.itheima.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
public void save(Account account) {
accountDao.save(account);
}
public void update(Account account){
accountDao.update(account);
}
public void delete(Integer id) {
accountDao.delete(id);
}
public Account findById(Integer id) {
return accountDao.findById(id);
}
public List<Account> findAll() {
return accountDao.findAll();
}
}
主函数
import com.itheima.dao.AccountDao;
import com.itheima.domain.Account;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class App {
public static void main(String[] args) throws IOException {
// 1. 创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 2. 加载SqlMapConfig.xml配置文件
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml.bak");
// 3. 创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
// 4. 获取SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 5. 执行SqlSession对象执行查询,获取结果User
AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
Account ac = accountDao.findById(2);
System.out.println(ac);// 6. 释放资源
sqlSession.close();
}
}
在上述内容中,我们重点分析配置文件和主函数的内容,因为我们的Spring的主要目的是为了管理Bean
所以我们需要在MyBatis中找到符合要求的Bean:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--properties属于配置核心内容,属于Bean(负责连接数据库)-->
<properties resource="jdbc.properties"></properties>
<typeAliases>
<package name="com.itheima.domain"/>
</typeAliases>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</dataSource>
</environment>
</environments>
<!--映射配置包位置,属于Bean(负责业务层)-->
<mappers>
<package name="com.itheima.dao"></package>
</mappers>
</configuration>
import com.itheima.dao.AccountDao;
import com.itheima.domain.Account;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class App {
public static void main(String[] args) throws IOException {
// SqlSessionFactory属于主体Bean
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml.bak");
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
// SqlSession由SqlSessionFactory创建
SqlSession sqlSession = sqlSessionFactory.openSession();
AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
Account ac = accountDao.findById(2);
System.out.println(ac);
sqlSession.close();
}
}
所以我们的整体操作其实就是为了整合MyBatis的Bean
接下来我们给出具体操作:
导入相关包
创建配置环境Config
// SpringConfig(前面已讲解)
package com.itheima.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
@Configuration
@ComponentScan("com.itheima")
//@PropertySource:加载类路径jdbc.properties文件
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}
// JdbcConfig(前面已讲解)
package com.itheima.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 javax.sql.DataSource;
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
}
// MyBatisConfig(MyBatis重点内容)
package com.itheima.config;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
public class MybatisConfig {
//定义bean,SqlSessionFactoryBean,用于产生SqlSessionFactory对象
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("com.itheima.domain");
ssfb.setDataSource(dataSource);
return ssfb;
}
/*
SqlSessionFactoryBean属于mybatis-spring提供的新的对象,用于快速产生SqlSessionFactory对象
ssfb.setTypeAliasesPackage("com.itheima.domain");
对应于
<typeAliases>
<package name="com.itheima.domain"/>
</typeAliases>
ssfb.setDataSource(dataSource);
对应于
DataSource的配置信息
上述语句基本均为固定语句
只有Package的别名包需要修改内容
*/
//定义bean,返回MapperScannerConfigurer对象
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.itheima.dao");
return msc;
}
/*
MapperScannerConfigurer属于mybatis-spring提供的新的对象,返回MapperScannerConfigurer对象
我们同样只需要设置映射包setBasePackage
上述语句基本均为固定语句
只有Package的映射名包需要修改内容
*/
}
主函数(其他内容基本不做修改)
import com.itheima.config.SpringConfig;
import com.itheima.domain.Account;
import com.itheima.service.AccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
AccountService accountService = ctx.getBean(AccountService.class);Account ac = accountService.findById(1);
System.out.println(ac);
}
}
我们对于Junit的整合建立于Spring与MyBatis已经整合的基础上,所以上述内容请务必明白!
Spring整合Junit具有一定固定格式,我们直接写出步骤:
导入包
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<!--junit包-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--Spring与junit联系包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
书写Junit的Test代码内容
// 下述内容均在test文件夹下进行
package com.itheima.service;
import com.itheima.config.SpringConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
//设置类运行器(固定形式)
@RunWith(SpringJUnit4ClassRunner.class)
//设置Spring环境对应的配置类(匹配你所使用的Spring,注意需要写classes的形式)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
//支持自动装配注入bean
@Autowired
private AccountService accountService;
@Test
public void testFindById(){
System.out.println(accountService.findById(1));
}
@Test
public void testFindAll(){
System.out.println(accountService.findAll());
}
}
我们在开篇有提及到AOP,现在让我们来详细介绍一下AOP~
首先我们来介绍一下AOP:
AOP作用:
Spring理念:
AOP核心概念:
通俗解释:
实现类中的各个方法被称为连接点
如果我们希望在这些连接点中设置相同的部分,可以采用通知进行设置
我们利用通知和连接点进行连接,连接点就可以执行通知中的方法并且同时执行连接点的方法
被连接的连接点被称为切入点,存放通知的类被称为通知类
我们同样采用一个案例进行SpringAOP入门介绍
案例设定:测试接口执行效率
简化设定:在接口执行前输出当前系统时间
开发模式:XML or 注解(我们现在大部分使用注解)
具体操作:
导入坐标(pom.xml)
制作连接点方法(原始方法,不发生改变)
// 接口BookDao
com.itheima.dao;
public interface BookDao {
public void save();
public void update();
}
// 实现类BookDaoImpl
package com.itheima.dao.impl;
import com.itheima.dao.BookDao;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("book dao save ...");
}
public void update(){
System.out.println("book dao update ...");
}
}
制作共性功能(通知类与通知)
// 我们推荐单独列出一个AOP文件夹,写下所有通知相关代码
// MyAdvice通知类
package com.itheima.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/*
具体流程:
1.创建该类
2.将该类设置为Spring中的Bean集中管理:@Component
3.设置为切面类注明该类作用:@Aspect
4.首先写出具体共性方法method,正常书写即可
5.写出切入点pt:切入点定义依托一个不具有实际意义的方法进行,即无参数,无返回值,无方法体,最好私有
6.对切入点进行设置:@Pointcut;我们后续讲解
7.对切入点和通知进行连接:@Before;我们后续进行讲解
这里简单介绍一下@Pointcut("execution(void com.itheima.dao.BookDao.update())")
@Pointcut:注释
execution:表示运行
void:返回类型
com.itheima.dao.BookDao.update():地址+类/接口+方法+方法参数
*/
//通知类必须配置成Spring管理的bean
@Component
//设置当前类为切面类类
@Aspect
public class MyAdvice {
//设置切入点,要求配置在方法上方
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
//设置在切入点pt()的前面运行当前操作(前置通知)
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
/*
在运行后,我们会发现,每次调用方法后,在执行前给出当前系统时间
*/
为SpringConfig设置相关需求
// SpringConfig
package com.itheima.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.itheima")
//开启注解开发AOP功能(我们的通知类采用注解开发)
@EnableAspectJAutoProxy
public class SpringConfig {
}
我们先简单介绍AOP的大概工作流程便于讲解底层知识:
首先我们要注意切入点的读取问题:
然后我们会根据bean是否能匹配切入点来分别处理:
这里我们进行几个名词解释:
目标对象:我们的初始对象,被一个或者多个切面所通知的对象
代理对象:我们根据目标对象所衍生出来的对象,不再是原对象;我们希望通过对代理对象的修改来完成AOP操作
我们已经简单了解了SpringAOP的具体使用,接下来让我们来仔细分析AOP的各部分
首先我们先来介绍AOP的切入点和切入点表达式定义:
AOP切入点表达式大致分为两种:
切入点表达式的具体格式:
具体名词解释:
AOP切入点表达式通配符:
我们给出相关例子:
package com.itheima.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
//切入点表达式:
// 表示接口下的方法
// @Pointcut("execution(void com.itheima.dao.BookDao.update())")
// 表示实现类的方法
// @Pointcut("execution(void com.itheima.dao.impl.BookDaoImpl.update())")
// 表示任意返回类型的单个参数的BookDaoImpl实现类的update方法
// @Pointcut("execution(* com.itheima.dao.impl.BookDaoImpl.update(*))")
// 表示com开头两层文件(第三层为类或接口)的update方法
// @Pointcut("execution(void com.*.*.*.update())")
// 表示所有以e结尾方法
// @Pointcut("execution(* *..*e(..))")
// 表示以com开头所有无参方法
// @Pointcut("execution(void com..*())")
// 表示com.itheima下的任意文件夹下的以Service结尾的实现类的以find开头的方法
// @Pointcut("execution(* com.itheima.*.*Service.find*(..))")
//执行com.itheima包下的任意包下的名称以Service结尾的类或接口中的save方法,参数任意,返回值任意
@Pointcut("execution(* com.itheima.*.*Service.save(..))")
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
AOP切入点书写技巧:
- 所有代码按照规范开发,否则下述技巧失效
- 描述切入点通常描述接口,而不是描述实现类
- 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
- 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
- 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
- 接口名/类名书写名称与模块相关的采用*匹配,例如UserService采用*Service,绑定业务层接口名
- 方法名书写以动词进行精准匹配,名词采用*匹配,例如getById采用getBy*
- 参数规则较为复杂,根据业务方法灵活调整
- 通常不使用异常作为匹配规则
AOP通知描述了抽取的共性功能,根据共性功能抽取位置的不同,最终运行代码时要加入到合理的位置
AOP通知一共分为五种:
前置通知
package com.itheima.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Pointcut("execution(int com.itheima.dao.BookDao.select())")
private void pt2(){}
//@Before:前置通知,在原始方法运行之前执行
@Before("pt()")
public void before() {
System.out.println("before advice ...");
}
}
后置通知
package com.itheima.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Pointcut("execution(int com.itheima.dao.BookDao.select())")
private void pt2(){}
//@After:后置通知,在原始方法运行之后执行
@After("pt2()")
public void after() {
System.out.println("after advice ...");
}
}
环绕通知(重点)
package com.itheima.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Pointcut("execution(int com.itheima.dao.BookDao.select())")
private void pt2(){}
//@Around:环绕通知,在原始方法运行的前后执行
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
//表示对原始操作的调用
Object ret = pjp.proceed();
System.out.println("around after advice ...");
return ret;
}
}
/*
@Around注意事项:
1.环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
2.通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
3.对原始方法的调用可以不接收返回值,通知方法设置为void即可,如果接收返回值,必须设置为Object类型
4.原始方法的返回类型如果是void类型,通知方法的返回类型可以设置成void,也可以设置为Object
5.由于无法预知原始方法运行后是否出现问题,因此需要抛出异常,抛出Throwable对象
*/
返回后通知(了解)
package com.itheima.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Pointcut("execution(int com.itheima.dao.BookDao.select())")
private void pt2(){}
//@AfterReturning:返回后通知,在原始方法执行完毕后运行,且原始方法执行过程中未出现异常现象
@AfterReturning("pt2()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
}
抛出异常后通知(了解)
package com.itheima.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Pointcut("execution(int com.itheima.dao.BookDao.select())")
private void pt2(){}
//@AfterThrowing:抛出异常后通知,在原始方法执行过程中出现异常后运行
@AfterThrowing("pt2()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}
下面我们针对环绕通知给出一个案例讲解:
需求:任意业务层接口执行均显示其执行效率(执行时长)
// 我们这里只给出SpringAOP的代码解释
package com.itheima.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class ProjectAdvice {
//匹配业务层的所有方法
@Pointcut("execution(* com.itheima.service.*Service.*(..))")
private void servicePt(){}
//设置环绕通知,在原始操作的运行前后记录执行时间
@Around("servicePt()")
public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
//获取执行的签名对象
Signature signature = pjp.getSignature();
String className = signature.getDeclaringTypeName();
String methodName = signature.getName();
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
pjp.proceed();
}
long end = System.currentTimeMillis();
System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
}
}
我们可以注意到在上述通知中我们是存在有参数的,接下来我们针对这些参数做出相关解释~
通知可选参数:
注意:JoinPoint是ProceedingJoinPoint的父类
接下来我们分别从参数数据,返回值数据,异常数据三个方面进行讲解:
参数数据:JoinPoint对象描述了连接点方法的运行状态,可以获得到原始方法的调用参数
package com.itheima.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}
//JoinPoint:用于描述切入点的对象,必须配置成通知方法中的第一个参数,可用于获取原始方法调用的参数
@Before("pt()")
public void before(JoinPoint jp) {
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("before advice ..." );
}
@After("pt()")
public void after(JoinPoint jp) {
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("after advice ...");
}
//ProceedingJoinPoint:专用于环绕通知,是JoinPoint子类,可以实现对原始方法的调用
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
}
}
返回值数据
package com.itheima.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
Object ret = pjp.proceed(args);
return ret;
}
//设置返回后通知获取原始方法的返回值,要求returning属性值必须与方法形参名相同
@AfterReturning(value = "pt()",returning = "ret")
public void afterReturning(JoinPoint jp,String ret) {
System.out.println("afterReturning advice ..." + ret);
}
}
返回异常数据
package com.itheima.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 666;
Object ret = null;
try {
ret = pjp.proceed(args);
} catch (Throwable t) {
t.printStackTrace();
}
return ret;
}
//设置抛出异常后通知获取原始方法运行时抛出的异常对象,要求throwing属性值必须与方法形参名相同
@AfterThrowing(value = "pt()",throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing advice ..."+t);
}
}
下面我们针对数据处理给出一个案例讲解:
需求:对密码的尾部空格作出兼容性处理
package com.itheima.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class DataAdvice {
@Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))")
private void servicePt(){}
@Around("DataAdvice.servicePt()")
public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
for (int i = 0; i < args.length; i++) {
//判断参数是不是字符串
if(args[i].getClass().equals(String.class)){
args[i] = args[i].toString().trim();
}
}
Object ret = pjp.proceed(args);
return ret;
}
}
我们在之前的文章中已经多次提及过事务,这里再重新声明一下事务:
我们通过一个案例来进行事务的讲解:
需求:实现任意两个账户间转账操作
需求微缩:A账户减钱,B账户加钱
分析:
结果分析:
具体修改实施步骤:
业务层接口上添加Spring事务管理
package com.itheima.service;
import org.springframework.transaction.annotation.Transactional;
import java.io.FileNotFoundException;
import java.io.IOException;
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
@Transactional
public void transfer(String out,String in ,Double money) ;
}
/*
Spring注解式事务通常添加在业务层接口而不会添加到业务层实现类,降低耦合
注解式事务可以添加到业务方法上表示当前方法开始事务,也可以添加到接口上表示当前接口所有方法开启事务
*/
设置事务管理器
package com.itheima.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.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
//配置事务管理器,mybatis使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
/*
同MyBatis的配置java文件一样上述语句基本属于固定语句
事务管理器根据实现技术进行选择
MyBatis框架使用的是Jdbc事务
*/
开启注解式事务驱动
package com.itheima.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
这里我们介绍两个新概念:
- 事务管理员:发起事务方,在Spring中通常指业务层开启事务的方法(上述表示transfer方法)
- 事务协调员:加入事务方,在Spring中通常代表数据层方法,也可以是业务层方法(上述表示out和in方法)
Spring的事务通常用@Transactional注解来表示
我们同样可以为@Transactional注解携带一些信息来管理事务的属性
属性
作用
示例
readOnly
设置是否为只读事务
readOnly=true 只读事务
timeout
设置事务超时时间
timeout=-1永不超时
rollbackFor
设置事务回滚异常(class)
rollbackFor={NullPointException.class}
rollbackForClassName
设置事务回滚异常(String)
同上格式为字符串
noRollbackFor
设置事务不回滚异常(class)
noRollbackFor={NullPointException.class}
noRollbackForClassName
设置事务不回滚异常(String)
同上格式为字符串
propagation
设置事务传播行为
……..
除了上述属性外,我们还需要仔细介绍propagation属性:
在实际开发中我们会利用propagation属性完成一些特殊操作
我们采用一个案例来进行说明:
需求:在上述转账的基础上,无论失败成功均保存一条日志记录转账信息
需求微缩:A账户减钱,B账户加钱,数据库记录日志
分析:
实现效果预期:
存在问题:
新增代码:
给出日志相关信息
// 日志数据层
package com.itheima.dao;
import org.apache.ibatis.annotations.Insert;
public interface LogDao {
@Insert("insert into tbl_log (info,createDate) values(#{info},now())")
void log(String info);
}
// 日志业务层
package com.itheima.service.impl;
import com.itheima.dao.LogDao;
import com.itheima.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}
主函数
package com.itheima.service.impl;
import com.itheima.dao.AccountDao;
import com.itheima.service.AccountService;
import com.itheima.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.*;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private LogService logService;
public void transfer(String out,String in ,Double money) {
try{
accountDao.outMoney(out,money);
int i = 1/0;
accountDao.inMoney(in,money);
}finally {
logService.log(out,in,money);
}
}
}
修改后代码:
在日志业务层设置日志操作为单独事务
// 日志业务层
package com.itheima.service.impl;
import com.itheima.dao.LogDao;
import com.itheima.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}
最后我们给出事务传播行为表:
传播属性
事务管理员
事务协调员
REQUIRED(默认)
开启T
加入T
REQUIRED(默认)
无
新建T
REQUIRES_NEW
开启T
新建T
REQUIRES_NEW
无
新建T
SUPPORTS
开启T
加入T
SUPPORTS
无
无
NOT_SUPPORTED
开启T
无
NOT_SUPPORTED
无
无
MANDATORY
开启T
加入T
MANDATORY
无
ERROR
NEVER
开启T
ERROR
NEVER
无
无
NESTED
NESTED:设置savePoint,一旦事务回滚,事务将回滚到savePoint处,交由客户响应提交/回滚
好的,关于Spring的内容就介绍到这里,希望能为你带来帮助!
该文章属于学习内容,具体参考B站黑马程序员李老师的SMM框架课程
手机扫一扫
移动阅读更方便
你可能感兴趣的文章