1. Spring框架简介
Spring框架主要是用于创建对象和管理对象的!
2. 通过Spring框架创建对象,并从中获取所需的对象
新建maven工程项目
打开项目的pom.xml文件,在其中添加Spring的依赖!在使用Spring框架时,需要添加的依赖是spring-context
,具体代码是:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
</dependencies>
当需要查询某个依赖时,在浏览器中搜索“mvn”关键字,打开 http://www.mvnrepository.com 网站,并搜索依赖的名称,在查询结果中找到匹配度最高的依赖,选择版本,就可以查询到依赖的代码。
然后,将Spring配置文件,applicationContext.xml,将它复制到项目的src/main/resources文件夹下!
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"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.2.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd">
</beans>
如果该文件提示错误,可以不处理,并不影响开发和运行!
可以在Spring的配置文件中配置由Spring创建并管理的对象!
假设需要由Spring创建一个Date
类的对象,则在该文件中添加:
<!-- id属性:自定义名称,后续将根据这个名称来获取对象,推荐使用类名将首字母改为小写 -->
<!-- class属性:需要Spring框架创建哪个类的对象,取值是类的全名,即包名与类名 -->
<bean id="date" class="java.util.Date"></bean>
至此,已经配置好了需要由Spring管理的对象,后续,当该配置文件被加载时,Spring框架就会创建java.util.Date
类的对象,开发人员也可以通过id属性值date
获取该对象!
获取对象
- 加载spring的配置文件,获取spring容器
ClassPathXmlApplicationContext ac
= new ClassPathXmlApplicationContext("applicationContext.xml");
- 从以上对象中获取由spring创建的对象,调用getBean()方法,参数就是配置文件中bean节点的id属性
Date date = (Date) ac.getBean("date");
- 测试
System.out.println(date);
- 关闭,释放资源
ac.close();
示例:创建SpringDemo
类
package cn.demo.spring;
import java.util.Date;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SpringDemo {
public static void main(String[] args) {
// 加载Spring的配置文件,获取Spring容器
ClassPathXmlApplicationContext ac
= new ClassPathXmlApplicationContext("applicationContext.xml");
// 从Spring容器中获取由Spring管理的对象
// 调用getBean()时的参数就是配置文件中<bean>节点的id值
Date date = (Date) ac.getBean("date");
// 测试
System.out.println(date);
// 关闭,释放资源
ac.close();
}
}
如果需要Spring框架创建自定义的某个类,可以先创建例如cn.demo.spring.User
类,在Spring的配置文件中添加配置:
<bean id="user" class="cn.demo.spring.User"></bean>
在执行程序中,补充获取对象的代码:
// getBean方法得到的是object类型,需要强转
User user = (User) ac.getBean("user");
// 或者(第二个参数就是得到的类型,加上后就不需要强转)
User user1 = ac.getBean("user",User.class);
以上创建对象的方式仅限于对应的类存在无参数构造方法!如果某个类并不存在无参数构造方法,则不适用于以上做法!
如果某个类确实不存在无参数构造方法,但是,有静态工厂方法,也是可以的!静态工厂方法指的是该类存在某个被static
修饰的方法,并且该方法的返回值类型就是当前类的类型!例如Calendar
类就是有静态工厂方法的:
public abstract class Calendar {
// 以下getInstance()方法就是静态工厂方法
public static Calendar getInstance() {
// ... 方法的具体实现
}
}
然后配置为:
<!-- factory-method:工厂方法的名称,不需要添加参数的括号 -->
<bean id="calendar" class="java.util.Calendar"
factory-method="getInstance">
</bean>
静态工厂方法
如果自定义的某个类也是没有无参数构造方法的,也可以通过这样的方式进行处理,例如:
package cn.demo.spring;
public class Person {
// 添加带某参数的构造方法,使之不能通过默认构造方法创建对象
public Person(Object object) {
}
// 添加静态工厂方法
public static Person newInstance() {
return new Person(null);
}
}
然后,在Spring的配置文件中配置为:
<bean id="person" class="cn.demo.spring.Person"
factory-method="newInstance">
</bean>
实例工厂方法
如果某个类既没有无参数构造方法,也没有静态工厂方法,如果这个类存在实例工厂方法,也是可以的!例如存在:
package cn.demo.spring;
public class Student {
// 添加带某参数的构造方法,使之不能通过默认构造方法创建对象
public Student(Object object) {
}
}
另外,还需要有一个工厂类,后续,创建工厂类的对象后,调用工厂中的某个方法,就可以创建以上类的对象:
package cn.demo.spring;
public class StudentFactory {
// 能生产Student类的对象的工厂方法
// 该方法不需要使用static来修饰
// 应该先创建工厂的实例(对象),再调用方法得到Student类的对象
// 所以,这样的方法叫做“实例工厂方法”
public Student newInstance() {
return new Student(null);
}
}
接下来,在Spring的配置文件中进行配置:
<!-- 先配置工厂 -->
<bean id="studentFactory" class="cn.demo.spring.StudentFactory"></bean>
<!-- 再配置需要创建对象的类 -->
<!-- factory-bean与factory-method属性:调用谁的哪个方法可以创建对象 -->
<bean id="student" class="cn.demo.spring.Student"
factory-bean="studentFactory" factory-method="newInstance">
</bean>
以上,介绍了3种使用Spring创建对象的方式:
- 通过无参数构造方法来创建;
- 通过类中的静态工厂方法来创建;
- 通过其它类的实例的工厂方法来创建。
在实际使用时,优先选取最简单的做法即可!
另外,如果某个类完全不符合以上所有条件,还可能通过构造方法 注入值 来实现创建对象.
3. Spring管理对象的作用域与生命周期
3.1. Spring管理对象的作用域
对象的作用域就表示这个对象是何时创建出来的,到何时被销毁,在这个过程中,是可以使用这个对象的,超出这个时间范围就无法使用这个对象了!
由Spring所管理的对象,默认都是单例(单一实例:在同一个时间节点,多次尝试获取对象,最终获取到的都是同一个对象,并不会因为反复获取而得到不同的对象)的!
在Spring的配置文件中,可以为<bean>
节点添加scope
属性,用于配置该<bean>
节点对应的类是否通过单例模式进行管理,该属性的常用取值可以是singleton
或prototype
,其中,singleton
是默认值,表示单例,而prototype
表示非单例!
所以,如果希望某个类的对象不是单例的,可以配置为:
<bean id="user" class="cn.tedu.spring.User" scope="prototype">
</bean>
另外,还可以在<bean>
节点添加lazy-init
属性,以配置单例模式的情况下,是否为懒汉式加载!该属性的值可以是true
或false
,其中,false
是默认值,表示“饿汉式加载”,当取值为true
时表示“懒汉式加载”!例如:
<bean id="user" class="cn.tedu.spring.User" scope="singleton" lazy-init="false">
</bean>
附:设计模式之单例模式
单例模式是设计模式中,关于生产对象类型的设计模式的一种!
单例模式的具体表现是不可以获取同一个类的多个对象,反复获取也只会得到同一个对象!
假设需要将King
类设计为单例的
public class King {
private static King king = new King();
private King() {
// 将构造方法私有,使之不可以在类的外部来创建对象
}
public static King getInstance() {
return king; // 直接返回私有的全局属性
}
}
以上单例的代码是“饿汉式单例模式”,另外,还有“懒汉式单例模式”,其特征是“不到逼不得已,不创建对象”!例如:
public class King {
private static King king;
private King() {
// 将构造方法私有,使之不可以在类的外部来创建对象
}
public static King getInstance() {
// 判断全局的king是否被创建了对象,如果没有,则创建
if (king == null) {
king = new King();
}
return king;
}
}
但是,以上代码是存在线程安全风险的!当存在多个“同时”运行的线程时,是可能创建出多个对象的!为了解决线程安全问题,需要:
public class King {
private static King king;
private static final Object lock = new Object();
private King() {
// 将构造方法私有,使之不可以在类的外部来创建对象
}
public static King getInstance() {
// 为了解决线程安全问题,需要锁上以下代码
synchronized(lock) {
// 判断全局的king是否被创建了对象,如果没有,则创建
if (king == null) {
king = new King();
}
}
return king;
}
}
但是,一旦加了锁,效率又会变得比较低下!因为每次调用以上方法都需要锁上代码才可以继续向后执行,在多线程的应用场景中,多个“同时”运行的线程在执行这段代码是互斥的,为了解决效率偏低的问题,还会:
public class King {
private static King king;
private static final Object lock = new Object();
private King() {
// 将构造方法私有,使之不可以在类的外部来创建对象
}
public static King getInstance() {
// 判断是否有必要锁住代码
if (king == null) {
// 为了解决线程安全问题,需要锁上以下代码
synchronized(lock) {
// 判断全局的king是否被创建了对象,如果没有,则创建
if (king == null) {
king = new King();
}
}
}
return king;
}
}
至此,懒汉式的单例模式也就完成了!
总的来说,饿汉式的特点是:刚刚加载时就创建了对象,而懒汉式的特点是:当第1次尝试获取对象时才创建对象!
3.2. Spring管理对象的生命周期
生命周期:某个对象从创建到最终销毁的整个历程!在整个生命周期历程中,会设计一些生命周期的“阶段”,约定这个“阶段”应该做哪些事情,Servlet中,就将Servlet的生命周期划分为init()
、service()
、destroy()
这3大“阶段”,具体的表现为3个方法!与Servlet相似的这类的组件的特点是:开发人员只需要在编写代码时创建出对应的类,并重写其中指定的方法即可,并不需要自行创建类的对象,更不需要调用方法,最终,程序在运行起来后,方法会被自动的调用,例如Servlet组件就是由Tomcat容器创建的对象,并由Tomcat调用的其中的方法!由于类的创建、方法的调用都是由Tomcat来完成的,所以,开发人员不必关心Servlet中的方法在几点几分几秒被调用,只知道某些会在满足什么条件时被调用,并决定被调用时应该如何处理数据,所以,在开发Servlet时,开发人员只需要重写对应的方法即可!总的来说,在讨论生命周期问题时,特定的组件(某些类)的创建过程、销毁过程,及其中某些方法的调用的主动权都不在开发人员手里,而是由某些容器进行管理的,开发人员就不必关心方法的调用时间,只需要关心特定的方法在哪种情景下会被调用,从而决定被调用时应该做哪些事情!生命周期在管理对象中的表现就是一个个的方法,在学习生命周期时,一定要了解这些方法在什么情景下被调用、调用多少次!
在学习Spring时,可以发现,所编写的类如果交给Spring来管理,由何时创建对象、何时销毁对象也不由开发人员来管理了!如果开发人员需要在整个对象的管理过程中添加一些管理方法,例如“在销毁之前释放某些资源”,则可以在类中添加创建和销毁时的生命周期方法,例如:
package cn.demo.spring;
public class User extends Object {
public User() {
System.out.println("创建了User类的对象!!!");
}
// 生命周期方法:初始化时执行的方法
// 方法的声明:自定义名称,空参数列表
public void init() {
System.out.println("User.init()");
}
// 生命周期方法:销毁之前执行的方法
public void destroy() {
System.out.println("User.destroy()");
}
}
然后,还需要在Spring的配置文件中进行配置:
<!-- init-method:初始化方法的名称 -->
<!-- destroy-method:销毁方法的名称 -->
<bean id="user" class="cn.tedu.spring.User"
init-method="init" destroy-method="destroy">
</bean>
当运行时,可以发现:在执行了构造方法之后,就会自动调用初始化方法,在最终释放资源之前,就会执行销毁方法!
当编写了某个类由交Spring管理后,并不是必须声明并配置生命周期方法,仅在有需要的时候添加方法并配置即可,也不一定需要同时添加初始化和销毁这2个方法,完全可以只添加并配置其中的某1个,按需使用即可!
4. Spring IoC
4.1. Spring IoC简介
IoC:控制反转(Inversion of Control),在传统开发模式下,都是由开发人员自行创建对象,并管理对象,当使用了Spring框架后,这些事情都交给框架来完成,可以理解“将开发人员对对象的管理权交给了框架”,所以,就出现了“管理权”的移交,则称之为“控制反转”。
DI:依赖注入(Dependency Injection),具体表现为通过框架为对象的属性赋值!
IoC与DI:Spring框架通过DI实现了IoC,DI是实现手段,而IoC是希望实现的目标!
关于Spring IoC的学习,就是学习如何通过Spring框架确实某个类的对象的属性值!
4.2 通过SET方式注入属性的值
假设存在User
类,其中有String password
属性,希望Spring在创建User
类的对象时,还能为password
属性注入值(赋值),则先准备好User
类,例如:
package cn.demo.spring;
public class User {
private String password;
public String getPassword() {
return password;
}
public void setPassword(String password) {
System.out.println("User.setPassword()");
this.password = password;
}
}
然后,在Spring的配置文件中进行配置:
<bean id="user" class="cn.demo.spring.User">
<!-- property节点:用于配置属性的值 -->
<!-- name属性:需要配置值的属性名称,实际是set方法对应的名称 -->
<!-- value属性:需要配置值的属性的值 -->
<property name="password" value="000111"/>
</bean>
最后,运行时,当获取对象后就可以直接输出属性值,可以看到,该属性已经存在所配置的值!
注意:在<property>
属性中,name
属性其实是SET方法对应的名称,例如以上配置为name="password"
,则Spring框架会基于这个配置值,将首字母大写,并在左侧拼上set
,得到setPassword
,以此为方法名称,结合value
属性值,调用了setPassword("000111")
,从而使得password
属性被赋值!所以,本质是要求name
属性的值与类的SET方法是对应的!但是,规范的SET方法名称与Spring组织出SET方法的方式是完全相同的,所以,也可以简单的理解为name
属性指的就是属性名称!
如果需要注入List
集合类型的值,例如存在:
// David, Lucy, Tom, Lily, Alex
private List<String> names;
则需要配置为:
<bean id="sampleBean" class="cn.demo.spring.SampleBean">
<property name="names">
<list>
<value>David</value>
<value>Lucy</value>
<value>Tom</value>
<value>Lily</value>
<value>Alex</value>
</list>
</property>
</bean>
如果需要注入的是数组类型的,例如存在:
// 9, 5, 2, 7
private int[] numbers;
则配置为:
<property name="numbers">
<array>
<value>9</value>
<value>5</value>
<value>2</value>
<value>7</value>
</array>
</property>
如果需要注入的是Set
集合类型的数据,假如存在:
// Beijing, Shanghai, Guangzhou, Shenzhen
private Set<String> cities;
则需要配置为:
<property name="cities">
<set>
<value>Beijing</value>
<value>Shanghai</value>
<value>Guangzhou</value>
<value>Shenzhen</value>
</set>
</property>
如果需要注入的是Map
类型的数据,假如存在:
// {username:root, password:1234, from:Beijing}
private Map<String, String> session;
则需要配置为:
<property name="session">
<map>
<entry key="username" value="root"/>
<entry key="password" value="1234"/>
<entry key="from" value="Beijing"/>
</map>
</property>
如果需要注入的是java.util.Properties
类型的数据,假如存在:
// name:Jack, age:25, gender:Male
private Properties profile;
则配置为:
<property name="profile">
<props>
<prop key="name">Jack</prop>
<prop key="age">25</prop>
<prop key="gender">Male</prop>
</props>
</property>
当然,Properties
类型的数据也可以从.properties
类型的文件中读取!可以先在src/main/resources下创建jdbc.properties文件,并在其中进行属性与值的配置:
url=jdbc:mysql://localhost:3306/database_name
driver=com.mysql.jdbc.Driver
username=root
password=root
initialSize=2
maxActive=10
在Spring的配置文件中,需要使用<util:properties>
节点来读取这个文件,这个节点与<bean>
节点是同一个级别的节点,不要将这个节点写在<bean>
的子级!在配置时:
<!-- util:properties节点:用于读取.properties文件中的配置信息 -->
<!-- location属性:需要读取的文件的位置 -->
<!-- classpath:指的就是resources文件夹 -->
<util:properties id="jdbc" location="classpath:jdbc.properties"></util:properties>
如果这些数据需要被读取到SampleBean
的Properties jdbc
属性中,先在SampleBean
中添加:
// 数据来自于jdbc.properties文件
private Properties jdbc;
并且,在Spring的配置文件中,在配置SampleBean
的<bean>
节点子级添加:
<property name="jdbc" ref="jdbc"/>
在为属性注入值时,如果该属性值是另一个Bean,则应该通过
ref
属性引用到另一个Bean的id。使用的
<util:properties>
也是一种<bean>
。
4.3. 通过构造方法注入属性的值(仅了解)
如果某个类没有无参数的构造方法,并且,希望通过构造方法为某些属性注入值,例如:
package cn.demo.spring;
// 使用构造方法注入属性的值
// 使用构造方法注入时,并不要求每个属性都有SET/GET方法
public class Person {
// Zoe
private String username;
// 26
private int age;
public Person(String username, int age) {
super();
this.username = username;
this.age = age;
}
@Override
public String toString() {
return "Person [username=" + username + ", age=" + age + "]";
}
}
在Spring的配置文件中,需要配置为:
<bean id="person" class="cn.demo.spring.Person">
<!-- constructor-arg节点:配置构造方法的参数 -->
<!-- index属性:参数的位置,即第几个参数,从0开始 -->
<constructor-arg index="0" value="Zoe"/>
<constructor-arg index="1" value="26"/>
</bean>
5. Spring表达式
在使用Spring框架时,可以通过Spring表达式,获取另一个Bean中的某个属性的值!
假设存在ValueBean
的类,会在这个类中声明一些属性,这些属性的值都来自Person
类、SampleBean
类中的某个的值,例如:
// 当前类中的属性都将来自Person或SampleBean中的某个属性
public class ValueBean {
// 值是Person中的username
private String username;
public String getUsername() {
return username;
}
// 由于username属性的值还是会通过SET方式注入进来,所以,需要有对应的SET方法
public void setUsername(String username) {
this.username = username;
}
}
在Spring的配置文件中,需要配置为:
<bean id="valueBean" class="cn.tedu.spring.ValueBean">
<!-- 属性值是Person对象的username的属性值 -->
<property name="username" value="#{person.username}"></property>
</bean>
其实,Spring表达式的基本格式就是使用#{}
框住某个式子,如果需要获取另一个Bean中指定的属性的值,格式为:
#{Bean的id.属性的名称}
需要注意,Spring框架会根据以上表达式中的“属性名称”拼接出GET方法,并调用该GET方法以获取值!例如以上配置的是#{person.username}
,则Spring就会拼出getUsername
这个名称,并调用getUsername()
方法,所以,对应的Person
类必须有getUsername()
方法!
使用Spring表达式时,还可以获取另一个类的集合属性中的某一个元素!例如:
// 值是SampleBean的names中的第3个
private String name;
则配置为:
<!-- 属性值是SampleBean对象中的names(List类型)中的第3个值 -->
<property name="name" value="#{sampleBean.names[2]}"/>
所以,如果要访问的是List
集合中的某个值,Spring表达式的语法是:
#{Bean的id.List集合的属性名[索引位置]}
如果要获取数组中的某个元素,也可以使用以上语法 ,由于Spring在管理Set
时使用的实现类是LinkedHashSet
,所以,也可以使用以上语法获取Set
集合中的某个元素!
如果需要获取Map
或Properties
类型的数据中的某个元素,例如:
// 值是SampleBean的profile中的gender
private String gender;
// 值是SampleBean的session中的password
private String password;
则需要配置为:
<!-- 属性值是SampleBean对象中的profile(Properties类型)中的gender的值 -->
<property name="gender" value="#{sampleBean.profile.gender}"/>
<!-- 属性值是SampleBean对象中的session(Map类型)中的password的值 -->
<property name="password" value="#{sampleBean.session.password}"/>
也就是说,获取Map
或Properties
类型中的值时,Spring表达式的语法是:
#{Bean的id.Map或Properties类型数据的名称.Map的Key或Properties的属性名}
另外,还可以写成:
#{Bean的id.Map或Properties类型数据的名称['Map的Key或Properties的属性名']}
例如:
<!-- 属性值是SampleBean对象中的session(Map类型)中的password的值 -->
<property name="password" value="#{sampleBean.session['password']}"/>
6. 自动装配(仅了解)
在配置某个类的<bean>
时,如果需要为其中的某些属性注入值,不必添加子级的<property>
节点来注入,只需要在<bean>
节点配置autowire
属性即可,Spring框架就会完成“自动装配”!
自动装配的机制是:Spring框架会在容器中查询匹配的对象,为需要自动装配的Bean的属性自动赋值!
例如:
<bean id="userDao" class="cn.demo.spring.UserDao"></bean>
<bean id="userLoginServlet" class="cn.demo.spring.UserLoginServlet" autowire="byName">
</bean>
在运行时,Spring会创建UserDao
类的对象,然后,创建UserLoginServlet
的对象时,由于对应的<bean>
节点配置了autowire
属性,则Spring还会查找UserLoginServlet
中有哪些属性,就找到了private UserDao userDao;
这个属性,同时,在Spring容器已经存在UserDao
的对象,与private UserDao userDao;
属性是匹配的,就会调用该属性的SET方法为这个属性注入值!整个过程是Spring框架自动完成的,开发人员只需要在配置时添加autowire
属性的配置即可!这个就是Spring框架的自动装配机制!
在配置时,autowire
属性的常用取值可以是byName
,表示“按照名称实现自动装配”,也可以是byType
,表示“按照类型实现自动装配”,还有其它取值,一般不使用!
当取值为byName
时,要求<bean>
的id
与类中的SET方法名称是一致的,例如在XML配置中存在:
<bean id="userDao" class="xx.xx.xx.xx">
则在需要自动装配的类中,应该存在名为setUserDao
的方法!
当取值为byType
时,对各个名称都没有要求(包括SET方法,也只需要是以set
作为前缀即可,后缀没有任何要求),只要类型能够匹配,就可以实现自动装配的效果!但是,能够匹配类型的对象必须最多只有1个,如果有2个或更多个,则在加载Spring配置文件时就会报错!
无论是使用byName
还是byType
,Spring框架都是尝试在Spring容器中查找匹配的对象进行注入,如果完全没有匹配的对象,就会放弃装配,并不会导致装配过程出现错误!
在<bean>
节点中添加autowire
实现自动装配是比较方便的,但是,在实际开发时,永远不会这样来处理!因为当某个类中有若干个属性时,这个又被配置了autowire
,则哪些属性已经成功的装配了值,而哪些却没有被装配值,从代码中的表现是非常不直观的!
此时,重点理解byName
和byType
的特征即可!
7. Spring注解
7.1. 组件扫描与通用注解
在Spring的配置文件中添加:
<!-- 组件扫描 -->
<!-- base-package属性:被扫描的根包 -->
<context:component-scan base-package="cn.demo.spring"/>
当Spring的配置文件被加载时,Spring框架就会扫描以上配置的cn.demo.spring
包及其子包下的所有内容,检查这些包中是否存在组件,如果存在,就会自动创建这些组件的对象!
所以,在cn.demo.spring
包中,还可以创建组件类,例如:
package cn.demo.spring;
import org.springframework.stereotype.Component;
@Component
public class User {
}
只有添加了组件注解的类才是组件类!也就是说,如果User
类在组件扫描的包中,却没有添加组件注解,Spring并不会创建这个类的对象!
Spring默认是根据类名将首字母改为小写作为Bean的id,所以,如果要获取以上User
类的对象,则应该使用user
作为Bean的id,例如:
User user = ac.getBean("user", User.class);
如果需要自行指定Bean的id,可以在@Compontent
注解中添加参数!例如:
@Compontent("uuu")
还可以使用@Controller
、@Service
、@Repository
这几种注解,在Spring框架的作用范围之内,它们的作用与@Compontent
是完全等效的,使用方式也完全相同,在需要添加注解时,添加以上任意一个即可!
这些注解的语义是不同的,在项目中添加其它有特殊定位的组件,例如控制器类型的组件就应该添加@Controller
注解,业务类型的组件就应该添加@Service
注解,处理数据持久化的组件就应该添加@Repository
注解,其它类型的组件就应该添加@Compontent
注解!
7.2. 管理对象作用域与生命周期的注解
在类的声明之前添加@Scope
注解可以配置该类被Spring框架管理时是否单例!该注解中可以添加参数"singleton"
或"prototype"
,当设置为@Scope("singleton")
时,表示单例,这是默认的管理方法,还可以设置为@Scope("prototype")
,表示非单例,例如:
@Scope("prototype")
@Component
public class User {
public User() {
System.out.println("User.User()");
}
}
使用@Lazy
注解可以配置该类是否为懒加载模式,可以在这个注解中配置的值是布尔值!一般,如果该类不需要是懒汉式加载,则不添加该注解,如果需要是懒汉式加载,直接添加@Lazy
注解即可,并不需要设置注解参数!同时,需要先保证该类是单例模式的,才讨论是否懒加载的问题:
@Scope("singleton")
@Component
@Lazy
public class User {
public User() {
System.out.println("User.User()");
}
}
在自定义的生命周期方法之前添加@PostConstruct
可以表示该方法是生命周期方法中的初始化方法,添加@PreDestroy
注解就表示销毁方法,例如:
@PostConstruct
public void onCreate() {
System.out.println("User.onCreate()");
}
@PreDestroy
public void onDestroy() {
System.out.println("User.onDestroy()");
}
7.3. 自动装配的注解
在需要被自动装配的属性之前添加@Autowired
注解,就可以表示接下来的这1个属性将会被自动装配,例如:
@Component
public class UserLoginServlet {
@Autowired
private UserDao userDao;
public void doPost() {
System.out.println("UserLoginServlet.doPost()");
userDao.login();
}
}
在使用自动装配时,也可以使用@Resource
注解!也就是说,@Autowired
和@Resource
这2个注解选取其中的1个来使用就可以了!
附:关于@Autowired和@Resource的区别
使用这2种注解都可以实现自动装配!
@Resource
注解是javax
包中的注解,它是优先byName
来装配的,如果byName
无法装配,则会自动尝试byType
装配,在byType
装配时,要求匹配类型的对象必须有且仅有1个,如果无法装配,则会报告错误;
@Autowired
注解是Spring框架中的注解,它是优先byType
来装配的,但是,这个过程中,只会检索匹配类型的对象的数量,并不直接装配,如果找到的对象的数量是0个,则直接报错,如果找到的对象的数量是1个,则直接装配,如果找到的对象的数量超过1个(2个或更多个),则会尝试byName
来装配,如果byName
装配失败,则报错。
在实际开发项目时,绝大部分情况下,需要装配的对象都是有且仅有1个的,并且命名都是规范的,所以,无论byType
或byName
都是可以装配成功,就不必在乎装配方式和做法,在以上2个注解的选取方面,通常也没有明确的要求!
附:(面向切面编程)
Spring AOP
AOP:面向切面(Aspect)的编程。
AOP并不是Spring框架独有的,只不过Spring框架很好的支持了AOP。
在当前项目中,关于数据的处理流程大致是:
- 注册:页面 -----> 控制器 -----> 业务层 -----> 持久层
- 登录:页面 -----> 控制器 -----> 业务层 -----> 持久层
- 改密:页面 -----> 控制器 -----> 业务层 -----> 持久层
- 头像:页面 -----> 控制器 -----> 业务层 -----> 持久层
假设需要在整个处理流程中添加某个功能,例如“统计每个业务方法的执行耗时”,在传统模式下,可能会将统计耗时的功能封装为一个方法,然后,在处理每个业务的过程中,都调用该方法!
而AOP的做法就是假想在整个处理流程中,可以增加切面,自行确定切面的连接点(连接在整个处理流程中的哪个位置),当数据的处理过程执行到切面时,就会自动执行切面中的方法,然后,再执行后续的流程!
所以,在实现AOP编程时,需要做的事情主要就是:确定切面方法,确定连接点位置。在整个开发过程中,对原有的处理流程涉及的代码并不产生修改!
在实现AOP编程之前,需要先添加相关依赖:
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjtools -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
然后,(结合博客中springboot项目)在cn.demo.store.aop
包下创建TimerAspect
切面类,并在类中编写切面方法及配置连接点位置:
@Component
@Aspect
public class TimerAspect {
@Around("execution(* cn.tedu.store.service.impl.*.*(..))")
public Object testServicesTime(ProceedingJoinPoint pjp) throws Throwable {
// 记录起始时间
long start = System.currentTimeMillis();
// 执行Service中的方法,例如用户注册,或用户登录等
Object result = pjp.proceed();
// 记录结束时间并对比
long end = System.currentTimeMillis();
System.err.println("执行耗时:" + (end - start) + "毫秒。");
// 返回
return result;
}
}
在创建切面类时,由于切面类是由交Spring框架管理的,所以,该类必须在组件扫描的cn.demo.store
包下,并且,需要添加@Component
注解;由于是一个切面类,还必须添加@Aspect
注解;切面类的名称可以自定义,通常以Aspect
作为名称的后缀。
关于切面类中的切面方法的声明:
- 应该使用
public
权限; - 可以使用
Object
作为返回值类型,如果执行的业务是可能有返回值的,则应该将切面方法的返回值类型声明为Object
,并且,在切面方法中,调用ProceedingJoinPoint
对象的proceed()
方法时,获取返回结果,作为切面方法的返回值; - 方法名称可以自定义;
- 可以添加
ProceedingJoinPoint
接口类型的参数,它表示处理连接点的对象,用于调用连接点对应的方法,如果切面被配置在Service层,则表示调用业务方法的对象,在切面方法内部,应该调用该参数对象的proceed()
方法,表示执行业务方法,并且,在调用业务方法时,会出现Throwable
类型的异常,此时,需要继续将异常抛出,不可以使用try...catch
直接处理!
在切面方法之前的@Around
注解,表示在之前与之后都需要执行切面,用于配置连接点信息,其中的参数表达式配置了切面将应用于哪里,以上配置的* cn.demo.store.service.impl.*.*(..)
就表示在cn.demo.store.service.impl
包下的所有类的所有方法!
其实,还可以使用@Before
和@After
注解,如果切面方法之前添加的只是@Before
注解,则表示仅在执行连接点方法之前需要执行某些任务,反之,如果切面方法之前添加的只是@After
注解,则表示仅在执行连接点方法之后需要执行某些任务。当使用@Before
或@After
注解时,切面方法对应的连接点方法是自动执行的,不需要通过切面方法中的代码进行调用,所以,切面方法的返回值类型使用void
即可,并且,参数列表中并不需要添加ProceedingJoinPoint
对象,更加不需要在切面方法内容调用执行,不需要抛出异常!
通常,建议掌握使用@Around
注解的做法,可以不关心@Before
和@After
注解的使用方法,例如:
@Around("execution(* cn.tedu.store.service.impl.*.*(..))")
public Object aaaaa(ProceedingJoinPoint pjp) throws Throwable {
// 记录起始时间
long start = System.currentTimeMillis();
// 执行Service中的方法,例如用户注册,或用户登录等
Object result = pjp.proceed();
// 返回
return result;
}
在不考虑return
语句的情况下,如果调用业务方法已经是切面方法中的最后一条语句了,其实,就等同于@Before
的效果,反之:
@Around("execution(* cn.tedu.store.service.impl.*.*(..))")
public Object aaaaa(ProceedingJoinPoint pjp) throws Throwable {
// 执行Service中的方法,例如用户注册,或用户登录等
Object result = pjp.proceed();
// 执行某些代码
// 返回
return result;
}
将调用业务方法作为整个切面方法中的第1条有效语句,等同于@After
的效果!
所以,通过@Around
注解,可以解决@Before
想要解决的问题,也可以解决@After
想要解决的问题。
基于AOP的效果,它的应用领域的特点应该是:(假设在业务层添加AOP)许多业务甚至所有业务都需要做相同的事情,且希望统一管理!具体的应用可以是:统计耗时、记录日志、权限验证……
什么是非侵入式设计?
从框架的角度可以理解为:无需继承框架提供的任何类
这样我们在更换框架时,之前写过的代码几乎可以继续使用。
Spring 有什么优势?
- 低侵入 / 低耦合 (降低组件之间的耦合度,实现软件各层之间的解耦)
- 声明式事务管理(基于切面和惯例)
- 方便集成其他框架(如MyBatis、Hibernate)
- 降低 Java 开发难度
- Spring 框架中包括了 J2EE 三层的每一层的解决方案(一站式)
BeanFactory 和 ApplicationContext 的区别
1.BeanFactory:
是Spring中最底层的接口,只提供了最简单的IoC功能,负责配置,创建和管理bean。在应用中,一般不使用 BeanFactory,而推荐使ApplicationContext(应用上下文),原因如下。
2.ApplicationContext:
- 继承了 BeanFactory,拥有了基本的 IoC 功能;
- 除此之外,ApplicationContext 还提供了以下功能:
① 支持国际化;② 支持消息机制;③ 支持统一的资源加载;④ 支持AOP功能;
注意: ApplicationContext 和 BeanFactory 相比,最主要的区别在于 BeanFactory 是延迟加载,举个例子:如果 Bean 没有完全注入,BeanFactory 加载后,会在你第一次调用 getBean 方法才会抛出异常;而 ApplicationContext 会在初始化的时候就加载并且检查,这样的好处是可以及时检查依赖是否完全注入;所以通常我们会选择 ApplicationContext。