1. 什么是框架
框架在项目中的表现就是一系列的jar包,例如Thymeleaf就是一个框架。
每种框架都会解决某种特定的问题,可能是开发效率的问题,或运行效率的问题,或代码管理维护的问题等等。
项目中使用框架就相当于得到了一个“毛坯房”,使用了框架之后,开发人员只需要关心后续的“装修”即可。
绝大部分的框架都有特定的使用方式,在使用时,必须遵循框架的使用规则!
每个框架都可能是若干个开发人员甚至开发团队多年的工作积累的作品,对于初学者来说,不要过于钻牛角尖,尝试理解框架的底层实现原理!
简单的说:使用框架,可以让编程变得更加简单!在学习框架时,主要学习框架的正确使用方式!
2. 依赖关系
假设在项目中需要开发一个用户注册的功能!在项目中可能存在:
代码语言:javascript复制public class UserRegServlet {
private UserDao userDao = new UserDao();
public void doPost() {
userDao.reg(); // 调用userDao对象实现存储用户数据
}
}
代码语言:javascript复制public class UserDao {
public void reg() {
// 通过JDBC技术将用户数据存储到数据库中
}
}
在以上代码中,UserRegServlet
就是依赖于UserDao
的!
3. 耦合度
如果某个类过于依赖于另外一个类,通常称之为了“耦合度较高”,是不利于代码的管理和维护的,简单的说,如果UserRegServlet
依赖于UserDao
,在未来的某一天,UserDao
已经不能满足项目的需求了(可能是因为代码有Bug,或者使用的技术比较落后等),如果需要把UserDao
替换掉,替换难度大就会影响项目的管理和维护,为了解决这样的问题采取的解决方案就称之为“解耦”,使得依赖关系不那么明确,甚至就是不明确!
就以上UserRegServlet
依赖UserDao
的问题,如果要解耦,可以先创建一个接口:
public interface IUserDao {
void reg();
}
然后,使得UserDao
是实现了以上接口的:
public class UserDao implements IUserDao {
public void reg() {
// 具体的实现了reg()方法应该实现的功能
}
}
经过以上调整以后,如果在UserRegServlet
中需要使用到UserDao
,以前的代码是这样的:
private UserDao userDao = new UserDao();
现在就可以改为:
代码语言:javascript复制private IUserDao userDao = new UserDao();
以上代码就相当于:
代码语言:javascript复制private List<String> strings = new ArrayList<>();
改成这样以后,在同一个项目中,无论多少个Servlet
组件需要使用到UserDao
,都可以使用以上“声明为接口,创建实现类的对象”的语法风格,如果以后UserDao
需要被替换掉,也只需要替换“赋值”的代码,声明部分是不需要替换的!例如需要把UserDao
替换为UserMybatisDao
时,原来的代码是:
private IUserDao userDao = new UserDao();
新的代码就可以是:
代码语言:javascript复制public class UserMybatisDao implements IUserDao {
public void reg() {
// 使用更好的方式实现reg()应该实现的功能
}
}
在后续的使用中,就可以是:
代码语言:javascript复制private IUserDao userDao = new UserMybatisDao();
也就是说,在UserDao
换成了UserMybatisDao
时,在各个Servlet
中,都只需要调整等于号右侧的内容,而不再需要修改等于号左侧的部分!
当然,关于以上代码的右侧部分,还可以使用“工厂设计模式”作进一步的处理:
代码语言:javascript复制public class UserDaoFactory {
// 返回接口类型的对象
public static IUserDao newInstance() {
return new UserDao(); // 也可以返回UserMybatisDao的对象
}
}
当有了工厂后,此前的代码就可以进一步调整为:
代码语言:javascript复制private IUserDao userDao = UserDaoFactory.newInstance();
可以发现,以上代码中不再出现任何一个实现类的名字了,无论是哪个Servlet
组件需要访问数据库,都声明为以上代码即可,以后,如果实现类需要被替换,也只需要替换工厂方法的返回值即可!
在实际项目开发时,项目中的组件的依赖更加复杂,为每个组件都创建对应的接口及工厂是非常麻烦的,而Spring框架就很好的解决了这个问题,可以简单的将Spring理解为一个“万能工厂”,当使用了Spring框架后,就不必自行开发工厂了!
4. Spring框架简介
Spring框架的主要作用:解决了创建对象和管理对象的问题。
5. 通过Spring创建对象
创建Maven Project,在创建过程中,勾选Create a simple project,Group Id填为cn.tedu
,Artifact Id填为spring01
,其它项保持默认即可。
使用Spring框架时,必须在项目的pom.xml中添加spring-context
的依赖:
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
首先,在项目中,创建cn.tedu.spring
包,并在这个包下创建BeanFactory
类:
package cn.tedu.spring;
public class BeanFactory {
}
当前,代码放在哪个包中并不重要,应该养成习惯,每个类都应该放在某个包中,不要让任何类不放在任何包中! 以上类的名称也不重要,是自定义的!
如果希望由Spring来创建并管理某个类的对象,必须在以上类中添加方法,关于这个方法:
- 应该使用
public
权限; - 返回值类型就是需要Spring创建并管理的类的对象的类型;
- 方法名称可以自定义;
- 参数列表暂时为空;
- 在方法体中,自行编写创建返回值对象的代码。
假设需要Spring来创建Date
类型的对象,则在类中添加方法:
public Date aaa() {
// 规范,规则
}
Spring框架要求:创建对象的方法必须添加@Bean
注解,并且,这样的方法必须在配置类中!任何一个类添加了@Configuration
注解都可以作为配置类!
package cn.tedu.spring;
@Configuration
public class BeanFactory {
@Bean
public Date aaa() {
return new Date();
}
}
完成后,应该使用一个可以运行的类,或通过单元测试来检验“是否可以通过Spring容器获取对象”。本次先创建一个可以运行的类:
代码语言:javascript复制package cn.tedu.spring;
public class SpringTests {
public static void main(String[] args) {
// 1. 加载配置类,得到Spring容器
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext(BeanFactory.class);
// 2. 从Spring容器中获取所需要的对象
Date date = (Date) ac.getBean("aaa"); // getBean()方法的参数就是创建对象的方法的名称
// 3. 测试获取到的对象
System.out.println(date);
// 4. 关闭
ac.close();
}
}
6. 关于@Bean注解
当方法的声明之前添加了@Bean
注解,就表示这个方法是需要由Spring框架所调用,并且,由Spring框架管理该方法返回的对象的!默认情况下,该方法的名称就是后续获取对象时,调用getBean()
方法的参数!
由于添加了@Bean
注解的方法是被Spring框架调用的,不需要自行编写代码来调用这个方法,所以,Spring的建议是“使用合理的属性名称作为方法名,并不需要使用动词或动词为前缀的方法名”,简单的说,如果方法是为了获取Date
类型的对象的,该方法的名称应该是date
,而不是getDate()
,则后续调用getBean()
时,参数就是date
这个名称!
当然,如果不遵循Spring的建议,还可以在@Bean
注解中配置注解参数来指定Bean的名称,例如:
@Bean("date")
public Date getDate() {
return new Date();
}
则后续就根据注解参数来获取对象:
代码语言:javascript复制Date date = (Date) ac.getBean("date");
其关系如下图:
其实,在开发项目时,真的不必关心这些问题,也就是说,例如是一个获取Date
对象的方法,其名称到底是date
还是getDate
都是正确的!毕竟这个方法最终就是由Spring框架来调用,开发人员不会自行调用该方法!
7. Spring管理对象的作用域
由Spring管理的对象,默认情况下,是单例的!所以,其作用域就非常久!
在Spring管理对象的情况下,讨论对象的作用域,其本质就是讨论其是否单例!
在创建对象的方法之前,添加@Scope
注解,并配置注解参数为prototype
,就可以使得该对象不是单例的:
@Scope("prototype")
@Bean
public User user() {
return new User();
}
由Spring管理的对象,如果是单例模式的,默认情况下,是饿汉式的!在创建对象的方法之前,添加@Lazy
注解,就可以调整为懒汉式的:
@Bean
@Lazy
public User user() {
return new User();
}
一般,在开发项目时,极少调整对象的作用域!
8. 当天小结:
- Spring的主要作用:创建对象,管理对象;
- 如果某个方法是用于给Spring框架创建对象的,这个方法就必须添加
@Bean
注解; - 所有添加了
@Bean
注解的方法,其所在的类应该添加@Configuration
注解,凡添加了@Configuration
注解的类称之为配置类; - 默认情况下,由Spring管理的对象是单例的,使用
@Scope
注解可以将Spring管理的对象调整为“非单例”的; - 默认情况下,由Spring管理的单例的对象是是“饿汉式”的,使用
@Lazy
可以将它们改为“懒汉式”的。
附1:设计模式之单例模式
单例模式的特点:在同一时期,某个类的对象一定最多只有1个!也许会尝试多次的获取对象,但是,获取到的一定是同一个对象!
假设项目中有King
类:
public class King {
}
很显然,目前它并不是单例的,因为,可以:
代码语言:javascript复制King k1 = new King();
King k2 = new King();
King k3 = new King();
以上代码就创建了3个King
类型的对象!如果要实现单例,首先,就必须限制构造方法的访问,例如:
public class King {
private King() {
}
}
每个类中都可以有若干个构造方法,如果某个类没有显式的声明任何构造方法,编译器就会自动添加1个公有的、无参数的构造方法!如果类中已经声明任何构造方法,则编译器不会自动添加构造方法!
由于将构造方法声明为私有的,则原有的King k1 = new King();
这类代码就不能用于创建对象了!
限制构造方法的访问,其目的是“不允许随意创建对象”,并不是“不允许创建对象”,在King
类的内部,还是可以创建对象的,可以添加方法,返回内部创建的对象:
public class King {
private King king = new King();
private King() {
}
public King getInstance() {
return king;
}
}
所以,当需要King
类型的对象时,可以通过getInstance()
方法来获取!
但是,以上代码是不可行的!因为,如果要调用getInstance()
方法,必须先获取King
的对象,而获取King
对象的唯一方式就是调用getInstance()
方法!为了解决这个问题,必须在getInstance()
方法的声明之前添加static
修饰符,最终,就可以通过类名.方法名()
的语法格式来调用方法了!同时,由于“被static
修饰的成员,不可以访问其它未被static
修饰的成员”,所以,全局属性king
也必须被static
修饰:
public class King {
private static King king = new King();
private King() {
}
public static King getInstance() {
return king;
}
}
至此,基本的单例模式的代码就设计完成了!
以上代码是“饿汉式”的单例模式,另外,还有“懒汉式”的单例模式!
基本的懒汉式单例模式的代码是:
代码语言:javascript复制public class King {
private static King king = null;
private King() {
}
public static King getInstance() {
if (king == null) {
king = new King();
}
return king;
}
}
注意:以上代码是多线程不安全的!
在开发领域中,只要数据的产生、变化不是开发人员预期的,就称之为“不安全”,也就是“数据安全问题”。
为了保障线程安全,应该为以上创建对象的代码片断“加锁”,例如:
代码语言:javascript复制public class King {
private static King king = null;
private King() {
}
public static King getInstance() {
synchronized ("hello") {
if (king == null) {
king = new King();
}
}
return king;
}
}
当然,无论是哪个线程在什么时候执行以上代码,都必须先“锁住”代码片断后才能开始执行,是没有必要的,“锁”的性能消耗是浪费的,所以,可以进一步调整为:
代码语言:javascript复制public class King {
private static King king = null;
private King() {
}
public static King getInstance() {
if (king == null) { // 判断有没有必要锁定接下来的代码
synchronized ("java") {
if (king == null) { // 判断有没有必要创建对象
king = new King();
}
}
}
return king;
}
}
至此,懒汉式的单例模式就完成了!