SSM-Spring(一)-简介、IoC、依赖注入
简介
- Spring框架可以算是Java开发中最常用的框架,功能非常强大
- 最早之前java的三大框架是SSH(Struts(网络层)、Spring、Hibernate(dao层))
- 现在流行的java三大框架SSM(SpringMVC、Spring、MyBatis)
- 官方网站:https://spring.io/
- https://spring.io/projects/spring-framework
- 官网标语:Spring makes Java simple、modern、productive…
- Spring框架的几个核心概念
- IoC:In version of Control, 控制反转
- DI:Dependency Injection,依赖注入
- AOP:Aspect Oriented Programming, 面向切面编程
- 当前使用的版本是5.2.8
Spring的下载
- jar包、文档下载:https://repo.spring.io/
- 点击左边菜单第二个图标进入:Artifact Repository Browser
- 列表中找到libs-release-local点击箭头展开列表
- 点击org展开,点击springframework展开
- 找到Spring找到5.2.8.RELEASE点击展开
- 找到spring-5.2.8.RELEASE-dist.zip,点击,然后点击右边的download即可,里面包含了离线文档
- 源码下载:https://github.com/spring-projects/spring-framework
IoC容器
IoC的引入
1.基础的项目结构:servlet层、service层、dao层
//servlet层
public class PersonServlet {
//使用service层
private PersonService service = new PersonServiceImpl();
//模拟网络请求调用:删除一条数据
public void remove() {
service.remove(1);
}
//模拟客户端调用网络接口
public static void main(String[] args) {
PersonServlet servlet = new PersonServlet();
servlet.remove();
}
}
//service层
//接口
public interface PersonService {
boolean remove(Integer id);
}
//impl
public class PersonServiceImpl implements PersonService {
//使用dao层
private PersonDao dao = new PersonDaoImpl();
@Override
public boolean remove(Integer id) {
return dao.remove(id);
}
}
//dao层
//接口
public interface PersonDao {
boolean remove(Integer id);
}
//impl
public class PersonDaoImpl implements PersonDao {
@Override
public boolean remove(Integer id) {
System.out.println("PersonDaoImpl ------ remove: " + id);
return false;
}
}
- 存在问题
- 耦合严重:PersonServlet依赖于PersonServiceImpl,PersonServiceImpl依赖于PersonDaoImpl,一旦某个环境的类型改变,其他依赖必须也要改变,即servlet依赖于service层,service层依赖于dao层
- 如何解耦? —工厂设计模式
2.工厂设计模式来解耦
-
新增一个工厂类:
com.zh.factory.PersonFactory;
public class PersonFactory { public static PersonService getService (){ return new PersonServiceImpl(); //return new PersonServiceImpl2(); } public static PersonDao getDao(){ return new PersonDaoImpl(); } }
-
将servlet、service中的依赖分别替换如下
//PersonServlet中替换 private PersonService service = PersonFactory.getService(); //PersonServiceImpl中替换 private PersonDao dao = PersonFactory.getDao();
- 这样一来,如果修改service、dao的实现类,那么就只需要在工厂类PersonFactory中修改了
- 那么是否也可以将PersonFactory解耦呢?就是修改service、dao实现不需要修改PersonFactory?—配置文件
3.配置文件解耦
-
service、dao实现类的类名放到配置文件中去,PersonFactory只加载配置文件
//resources文件中添加配置文件factory.properties personService=com.zh.service.impl.PersonServiceImpl personDao=com.zh.dao.impl.PersonDaoImpl
-
改造PersonFactory类工厂代码如下:
public class PersonFactory { private static Properties properties; static { try (InputStream is = PersonFactory.class.getClassLoader().getResourceAsStream("factory.properties")) { properties = new Properties(); properties.load(is); } catch (Exception e) { e.printStackTrace(); } } public static <T> T get(String name) { try { // 类名 String clsName = properties.getProperty(name); Class cls = Class.forName(clsName); // 实例化对象 return (T) cls.newInstance(); } catch (Exception e) { e.printStackTrace();; } return null; } public static PersonService getService() { return get("personService"); } public static PersonDao getDao() { return get("personDao"); } }
-
这样,如果替换service、dao的实现类,只需要修改配置文件中的实现类字符串即可
4.封装一个通用的工厂
-
封装通用工厂GeneralFactory
public class GeneralFactory { private static Properties properties; static { try (InputStream is = GeneralFactory.class.getClassLoader().getResourceAsStream("factory.properties")) { properties = new Properties(); properties.load(is); } catch (Exception e) { e.printStackTrace(); } } public static <T> T get(String name) { try { // 类名 String clsName = properties.getProperty(name); Class cls = Class.forName(clsName); // 实例化对象 return (T) cls.newInstance(); } catch (Exception e) { e.printStackTrace();; } return null; } }
-
servlet、dao中改造
//PersonServlet private PersonService service = GeneralFactory.get("personService"); //PersonServiceImpl private PersonDao dao = GeneralFactory.get("personDao");
IoC是什么
- Spring已经内置了这样的全局工厂(ApplicationContext),不需要自己实现—IoC
- 即对javaEE语法的封装,封装一个全局工厂类(ApplicationContext),可以通过配置文件中配置的类字符串创建一个实例
基本使用-IoC容器
-
添加Maven依赖项
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.9.RELEASE</version> </dependency>
- 添加一个核心配置文件 applicationContext.xml
-
右击resources->new->XML Configuration File ->Spring Config->applicationContext.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就是代表Spring将要创建的对象,类似之前的factory.properties--> <bean id="personDao" class="com.zh.dao.impl.PersonDaoImpl"/> <bean id="personService" class="com.zh.service.impl.PersonServiceImpl"> <!-- property就是这个对象对应的属性,因此这个对象必须要有对应的这个属性,否则会报错 1. 会主动调用PersonServiceImpl对象的dao属性的set方法-setDao方法 2. 调用set方法传的的参数值为:ref引用的类型的实例对象,也要创建一个参数对象 3. 因此要在PersonServiceImpl对象中实现dao属性的set方法 --> <property name="dao" ref="personDao"/> </bean> <bean id="personServlet" class="com.zh.servlet.PersonServlet"> <property name="service" ref="personService"/> </bean> </beans>
- bean标签对应对象、property对应对象的属性,ref要传入的参数类型
-
那么如何获取这个xml文件,并出创建bean标签对应的对象呢?—全局类工厂
// 读取配置文件 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); //创建servlet对象 PersonServlet servlet = ctx.getBean("personServlet", PersonServlet.class); servlet.remove();
-
- 根据核心配置文件在PersonServlet添加私有的service属性、PersonServiceImpl中添加私有的dao属性,并分别实现其对应的set方法
-
PersonServlet
public class PersonServlet { private PersonService service; public void setService(PersonService service) { this.service = service; } //模拟网络请求调用:删除一条数据 public void remove() { service.remove(1); } public static void main(String[] args) { // 读取配置文件 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); //通过id获取bean对象,并强制转换传成PersonServlet类 PersonServlet servlet = ctx.getBean("personServlet", PersonServlet.class); servlet.remove(); } }
-
PersonServiceImpl
public class PersonServiceImpl implements PersonService { private PersonDao dao; public void setDao(PersonDao dao) { this.dao = dao; } @Override public boolean remove(Integer id) { return dao.remove(id); } }
-
IoC容器总结
- IoC简称:Inversion of Control,控制反转,对象的创建控制权转交给了Spring
- 对javaEE语法的封装,封装一个全局工厂类(ApplicationContext),可以通过配置文件中配置的类字符串(ben标签)创建一个实例,即核心一是配置文件applicationContext.xml、二是全局工厂类
- IoC容器指的是xml里面放了很多的bean,被创建完的对象都放在了一个容器中
- 注意:从Spring开始模型对象的文件就不能叫bean了,以后所说的bean都是spring那个xml文件中的bean标签,模型对象用domain文件存储
- IoC的一个很重要的作用:解耦,它实现了将dao、service、servlet三层进行解耦,只需要通过修改配置文件即可修改三层的依赖关系。
scope
- 可以通过scope属性控制IOC中创建的bean是否单例
- singleton:单例(默认)
- 通过同一个id值,在同一个IoC容器中(同一个ctx对象)获取的永远是同一个实例
- 在IoC容器创建的时候,就会创建bean,可以设置lazy-init为true修改创建时机
- 注意:这个单例仅仅是在这个容器中
- prototype: :非单例,每次getBean时创建一次bean
- singleton:单例(默认)
- 参考资料:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes
- 代码举例:
-
Dog类
public class Dog { //监听dog什么时间被创建 public Dog() { System.out.println("Dog----------"); } }
-
applicationContext.xml
<!--<bean id="dog" class="com.zh.domain.Dog"/>--> <!--与上面等价,每次通过getBean获取是同一个对象--> <bean id="dog" class="com.zh.domain.Dog" scope="singleton"/> <!--不同id值,与上面获取的不同--> <bean id="dog2" class="com.zh.domain.Dog" scope="singleton"/> <!--每次通过getBean获取不是同一个对象--> <bean id="dog3" class="com.zh.domain.Dog" scope="prototype"/> <!--dog、dog3都是创建容器时就已经创建了,dog2是调用getBean时创建--> <bean id="dog4" class="com.zh.domain.Dog" lazy-init="true"/>
-
测试代码:
@Test public void test() { //一个ctx代表创建一个IoC一个容器,创建这个容器的时候已经将xml中所有的bean对象创建 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); //在调用getBean前,dog对象已经被创建了 System.out.println(ctx.getBean("dog", Dog.class)); System.out.println(ctx.getBean("dog", Dog.class)); //与上面一样 System.out.println(ctx.getBean("dog2", Dog.class)); //与上面不一样 System.out.println(ctx.getBean("dog3", Dog.class)); System.out.println(ctx.getBean("dog3", Dog.class)); //与上面不一样,属性为prototype System.out.println(ctx.getBean("dog4", Dog.class)); //用到时才创建 //不同容器,对象不一样 ApplicationContext ctx2 = new ClassPathXmlApplicationContext("applicationContext.xml"); //在调用getBean前,dog对象已经被创建了 System.out.println(ctx2.getBean("dog", Dog.class)); }
-
Spring集成日志框架logback
-
Spring可以轻松整合日志框架:logback
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency>
-
通过导入logback,能通过日志看到
PersonServlet servlet = ctx.getBean("personServlet", PersonServlet.class);
具体做了什么21:41:12.407 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@4e9ba398 21:41:13.292 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 3 bean definitions from class path resource [applicationContext.xml] 21:41:13.439 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'personDao' 21:41:13.523 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'personService' 21:41:13.799 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'personServlet'
依赖注入DI(Dependency Injection)
概念
- 引入:
- 在上面讲解IoC的时候就讲解过依赖注入,比如: PersonServiceImpl这个类依赖于dao这个属性,通过applicationContext.xml这个
<property name="dao" ref="personDao"/>
属性标签,将ref这个类型的对象通过setter方法注入到了PersonServiceImpl对象中 - 注意: 这里面的ref引用的对象正好能在applicationContext.xml文件中的id找到
- 在上面讲解IoC的时候就讲解过依赖注入,比如: PersonServiceImpl这个类依赖于dao这个属性,通过applicationContext.xml这个
- 所谓的注入就是: 给applicationContext.xml配置的这些bean对象设置属性,等到真正使用这些bean时会根据这些设置进行实例化
- 如果说IOC是封装一个全局类来读取配置文件创建实例(bean)的话,那么DI就是封装一套API在IOC创建实例(bean)时依据配置文件中配置的实例(bean)属性来创建所有的属性值。
- 常见的注入内容(属性类型)可以分为3大类
- bean(自定义类型)
- 基本类型、String、BigDecimal
- 集合类型(数组、Map、List、Set、Properties)
- 常见的注入方式有2种,即要使用这套API的话,对应属性的内容必须实现2以下两种任意一种。
- 基于setter(属性)
- 基于Constructor(构造方法)
基于setter的注入
//新建一个项目,添加两个模型Dog、Person
public class Person {
private int age;
private String name;
private BigDecimal money;
private Dog dog;
private Set<String> nickNames;
private Properties friends;
}
- 基本类型、String、BigDecimal的注入:
- 实现Person类所有属性的setter方法
-
applicationContext.xml中如下
<bean id="person" class="com.zh.domain.Person"> <!--基本数据类型注入--> <property name="age" value="18"/> <property name="money" value="100.5"/> <property name="name" value="Jack"/> </bean>
- bean自定义类型
-
applicationContext.xml中如下
<bean id="dog" class="com.zh.domain.Dog"/> <bean id="person" class="com.zh.domain.Person"> <!--对象类型注入--> <!--方式1--> <!--<property name="dog" ref="dog"/>--> <!--方式2--> <property name="dog"> <ref bean="dog"/> </property> <!--方式3:这种方式,这只dog跟外面的bean中的dog不一样--> <!--<property name="dog">--> <!--<bean class="com.zh.domain.Dog"/>--> <!--</property>--> </bean>
-
-
集合类型
<!--集合方式注入--> <property name="nos"> <array> <value>11</value> <value>11</value> </array> </property> <property name="nos"> <list> <value>11</value> <value>11</value> </list> </property> <!-- LinkeHashSet --> <property name="pones"> <set> <value>Jack</value> <value>Rose</value> <value>James</value> </set> </property> <!-- LinkeHashSet --> <property name="friends"> <map> <entry key ="Jack" value="广州“/> <entry key ="Rose" value=”背景“/> <entry key ="James" value="上海”/> </map> </property> <property name="friends"> <props> <prop key="Jack">杰克</prop> <prop key="Rose">螺丝</prop> </props> </property>
另外一种方法:
-
在applicationContext.xml文件的beans根标签添加属性:(需要先在根标签加一个命名空间属性)
xmlns:p="http://www.springframework.org/schema/p"
-
使用如下:
<!-- //只适用于setter方法的注入: 1. 跟标签添加命名空间属性: xmlns:p="http://www.springframework.org/schema/p" 2. 通过p来注入属性 --> <bean id="dog" class="com.zh.domain.Dog"/> <!--这个dog-ref属性,是根据所有bean对象的id属性自动生成的--> <bean id="person" class="com.zh.domain.Person" p:name="Jack" p:age="18" p:dog-ref="dog"/>
基于Constructor的注入
-
前提条件: 必须实现实现Person属性对应的构造方法
public Person() { System.out.println("Person()"); } public Person(int age) { this.age = age; System.out.println("Person(" + age + ")"); } @ConstructorProperties({"age", "name"}) public Person(int age, String name) { this.age = age; this.name = name; System.out.println("Person(" + age + ", " + name + ")"); }
-
基本类型、String、BigDecimal的注入:
<bean id="person" class="com.zh.domain.Person"> <!--方法1:--> <!-- <constructor-arg type="int" value="22"/>--> <!-- <constructor-arg type="java.lang.String" value="Jack"/>--> <!--方法2:--> <!-- <constructor-arg index="0" value="22"/>--> <!-- <constructor-arg index="1" value="Jack"/>--> <!--方法3: 注意该方法使用:需要在构造方法前加上注解@ConstructorProperties({"age", "name"}) --> <constructor-arg value="Jack666" name="name"/> <constructor-arg value="28" name="age"/> </bean>
-
bean自定义类型
<bean id="dog" class="com.zh.domain.Dog"/> <!-- <bean id="person" class="com.zh.domain.Person">--> <!-- <constructor-arg ref="dog"/>--> <!-- <<!-- </bean>--> <bean id="person" class="com.zh.domain.Person"> <constructor-arg> <ref bean="dog"/> </constructor-arg> </bean> <!-- <bean id="person" class="com.zh.domain.Person">--> <!-- <constructor-arg>--> <!-- <bean id="dog" class="com.zh.domain.Dog"/>--> <!-- </constructor-arg>--> <!-- </bean>-->
-
集合类型
<bean id="person" class="com.zh.domain.Person"> <constructor-arg> <list> <value>Jack</value> <value>Rose</value> </list> </constructor-arg> </bean>
复杂对象的注入
- 指的是创建过程比较复杂的对象:比如创建一个对象不是通过new Class创建的,而是通过自定义的类方法、实例发方法创建的,那么在xml文件中改如何写呢?
实例工厂、静态工厂
-
创建一个ConnectionFactory类
//com.zh.obj.ConnectionFactory public class ConnectionFactory { private String driverClass; private String url; private String username; private String password; public void setDriverClass(String driverClass) { this.driverClass = driverClass; } public void setUrl(String url) { this.url = url; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } //类方法创建 public static Connection getConn1() throws Exception { Class.forName("com.mysql.jdbc.Driver"); return DriverManager.getConnection("jdbc:mysql://localhost:3306/test-mybatis", "root", "root"); } //实例方法创建 public Connection getConn2() throws Exception { Class.forName(driverClass); return DriverManager.getConnection(url, username, password); } }
-
applicationContext.xml文件
<!-- 方式1;静态工厂方法(调用ConnectionFactory.getConn1()) --> <bean id="conn1" class="com.zh.obj.ConnectionFactory" factory-method="getConn1"/> <!-- 方式2: 实例工厂方法(调用factory.getConn2()) --> <!-- 1. 先创建实例对象--> <bean id="factory" class="com.zh.obj.ConnectionFactory"> <property name="driverClass" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test-mybatis"/> <property name="username" value="root"/> <property name="password" value="root"/> </bean> <!-- 2.通过实例对象创建--> <bean id="conn2" factory-bean="factory" factory-method="getConn2"/>
FactoryBean
-
创建一个ConnectionFactoryBean类,并实现FactoryBean接口,而且实现接口对应的方法
//实现FactoryBean接口 public class ConnectionFactoryBean implements FactoryBean<Connection> { private String driverClass; private String url; private String username; private String password; //上面属性对应的set方法,略 //必须实现 @Override public Connection getObject() throws Exception { Class.forName(driverClass); return DriverManager.getConnection(url, username, password); } //必须实现 @Override public Class<?> getObjectType() { return Connection.class; } }
- 注意:定义这个类的目的并不是为了获取ConnectionFactoryBean对象,而是为了获取DriverManager.getConnection()这个对象(getObject可以拿到),用来连接数据库。
-
applicationContext.xml文件
<!--方式3: FactoryBean 1. 首先自动创建ConnectionFactoryBean对象 2. 自动给对象注入属性 3. 调用实例对象的getObject方法 --> <bean id="conn" class="com.zh.obj.ConnectionFactoryBean"> <property name="driverClass" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test-mybatis"/> <property name="username" value="root"/> <property name="password" value="root"/> </bean>
- Spring内部会做如下事情:
- 首先自动创建ConnectionFactoryBean实例对象
- 根据property给对象注入属性
- 调用实例对象的getObject方法
-
测试代码
@Test public void test() { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); //getBean("conn")表面上获取的是ConnectionFactoryBean,而实际获取的是getObject这个返回值。 System.out.println(ctx.getBean("conn")); //&开头代表创建的是FactoryBean类型的对象,这个获取的才是ConnectionFactoryBean实例。 System.out.println(ctx.getBean("&conn")); }
- 总结:
- 使用工厂方法创建对象,注入的时候需要指定factory-method的工厂方法
- 使用FactoryBean跟的话不需要指定factory-method,直接在配置文件中配置对应的FactoryBean,spring内部会自动调用FactoryBean的getObject方法,获取到真正的实例对象
- 这是FactoryBean的一个典型使用,即xml中注册的是一个FactoryBean类,但是该容器中创建的通常是factory这个对象
引入其他配置文件
-
上面看到如果想要数据库的引入可配置化,那就需要使用配置文件,在resources文件夹下创建db.properties
jdbc.driverClass=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/test-mybatis jdbc.username=root jdbc.password=root
-
applicationContext.xml的根标签beans添加:
//引入命名空间 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 http://www.springframework.org/schema/context/spring-context.xsd">
-
改造:
<!-- 引用db.properties文件的内容 --> <context:property-placeholder location="db.properties"/> <bean id="conn" class="com.zh.obj.ConnectionFactoryBean"> <property name="driverClass" value="${jdbc.driverClass}"/> <property name="url" value="${jdbc.url}"/> <!--${}里面不能使用username,是保留字--> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean>
SpEL表达式
- 使用方式
#{}
- 官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions
-
Dog、Person类
public class Dog { public String getTestName() {return "WangCai";} public int getTestAge() {return 99;} } public class Person { private String name; private int age; private Dog dog; public String getName() {return name;} public void setName(String name) { this.name = name;} public int getAge() { return age;} public void setAge(int age) { this.age = age;} public Dog getDog() {return dog;} public void setDog(Dog dog) { this.dog = dog;} @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + ", dog=" + dog + '}'; } }
-
applicationContext.xml使用
<bean id="dog" class="com.zh.domain.Dog"/> <bean id="person" class="com.zh.domain.Person"> <property name="dog" value="#{dog}"/> <property name="age" value="#{dog.testAge}"/> <!--<property name="name" value="#{dog.testName.bytes.length}"/>--> <property name="name" value="#{dog.getTestName()}"/> </bean>
-
ConnTest测试代码
@Test public void spEl() { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); System.out.println(ctx.getBean("person")); }