一、 为什么是Spring?
在正式进入Spring内容前我们先看看我们以往经典的程序设计。
当我们去登录时,会调用后端的Controller,Controller接收到用户的请求后会调用业务层的Service进行登录的业务处理,Service业务处理过程中会调用Dao层向DB获取数进行判断。
接下来我们用代码来模拟实现这个逻辑
Controller中,我们需要有一个方法来接收用户发起的请求
public class UserController {
private UserService userService = new UserServiceImpl();
// 登录
public void login() {
String username = "admin";
String password = "123456";
boolean success = userService.login(username, password);
System.out.println(success ? "登录成功" : "登录失败");
}
}
Service中要进行用户校验的业务逻辑处理,定义Service接口以及实现类
public interface UserService {
boolean login(String username, String password);
}
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImplForMySQL();
@Override
public boolean login(String username, String password) {
User user = userDao.findUserByUsernameAndPassword(username, password);
return user != null;
}
}
Dao中我们要使用用户传过来的用户名和密码去数据库查询是否存在,定义Dao接口及实现类
public interface UserDao {
User findUserByUsernameAndPassword(String username, String password);
}
public class UserDaoImplMySQL implements UserDao {
@Override
public User findUserByUsernameAndPassword(String username, String password) {
// 省略具体的实现过程...
return null;
}
}
看上去这个模拟的实现没有什么问题,但是有一天项目组说我们现在数据从MySQL迁移到了Oracel,怎么办??你得考虑它用户数据未来还会不会迁移回来呢!
所以此时我们就会对于Dao的实现再加一个
// 使用Oracle数据库实现UserDao
public class UserDaoImplForOracle implements UserDao {
@Override
public User findUserByUsernameAndPassword(String username, String password) {
// 省略具体实现过程...
return null;
}
}
不同的数据库实现有不同的处理方式,感觉逻辑正常,但是这时我们还得要反回去修改Service的实现,让它引用的是新的Dao实现。
public class UserServiceImpl implements UserService {
// private UserDao userDao = new UserDaoImpl();
private UserDao userDao = new UserDaoImplForOracle();
@Override
public boolean login(String username, String password) {
User user = userDao.findUserByUsernameAndPassword(username, password);
return user != null;
}
}
以上的这个设计好吗?实际是不好的,因为每次底层的变化都会要求上层进行代码的变更,其实这里违背软件开发中的开闭原则!
什么是开闭原则?
在软件开发过程中应对扩展开放,对修改关闭。也就是说,如果进行功能扩展时,添加额外的类是没有问题的,但是如果因为功能扩展而修改之前运行正常的程序,这时忌讳,是不被允许的。
从上图可以看出,上层是依赖下层的。UserController依赖UserServiceImpl,则UserServiceImpl依赖UserDaoImplForMySQL,这样就会导致一旦下层的改动,上层必然受到影响也需要进行改动,这也同时违背了另一个开发原则:依赖倒置原则
什么是依赖倒置原则?
依赖倒置原则,简称DIP,主要倡导的是面向抽象编程、面向接口编程,不要面向具体编程,让上层不再依赖下层,这样的话下面改动了,上面代码也不会受到牵连。这可以大大地降低程序的耦合度,耦合度低了扩展性就强了,同时代码的复用性也会增强。
在前面的代码中设计上是使用了接口,但是具体上层引用下层的时候创建的还是具体的实现类,并没有完全面向接口编程。那如何才能做到完全的面向接口编程呢?
在Service实现类中我们不要去依赖Dao层的具体实现,只依赖它的接口
public class UserServiceImpl implements UserService {
// private UserDao userDao = new UserDaoImpl();
// private UserDao userDao = new UserDaoImplForOracle();
private UserDao userDao; // 现在只依赖Dao层的接口,具体Dao层如何实现上层这里不管了
@Override
public boolean login(String username, String password) {
User user = userDao.findUserByUsernameAndPassword(username, password);
return user != null;
}
}
看这上面的代码又有疑问了,这里userDao不就是null吗?使用的时候不就会报空指针异常吗?
要让程序不出现空指针异常则要解决下面的问题:
- userDao的具体实现对象谁来创建?
- 创建的到底是 new UserDaoImplForOracle() / new UserDaoImplMySQL()?
- 对象创建好了又是谁把这个对象赋值到这个地方?
要解决以上的问题Spring框架可以做到!!
Spring实际上可以帮我们创建对象,帮我们维护对象与对象之间的关系。
显然我们把创建对象以及管理对象的权限交由Spring框架来处理了,不再是我们自己硬编码,这种把对象的创建权交出去,把对象的管理权交出去,被称为控制反转。
控制反转是Spring中最为核心的部分,我们来先简单介绍一下!
控制反转(Inversion of Control,缩写是IoC),是面向对象编程中的一种设计思想,可以用来降低代码之间的耦合度,符合依赖倒置原则。
控制反转的核心是:把对象的创建权交出去,把对象和对象之间关系的管理权交出去,由第三方容器来负责创建和维护。
控制反转的常见实现方式是:依赖注入(Dependency Injection,简称DI)
依赖注入的常见方式:
- set方法注入
- 构造方法注入
我们可以理解Spring框架就是一个实现了IoC控制反转思想的框架,其实现的方式是使用依赖注入!所以IoC是一种思想,DI是具体的实现。
有了上面的介绍,接下来我们正式进入Spring
二、Spring概述
简介
Spring是一个开源框架,由Rod Johnson创建,它创建的原因是为了解决企业应用开发的复杂性,具体的体现在如下几点:
- 它是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架
- 最初是为了解决EJB臃肿的设计,以及难以测试等问题
- 它为简化开发而生,程序员只需要关注核心业务的实现,尽可能不再关注非业务逻辑代码(事务控制、安全日志等)
八大模块
Spring5以后Spring是8大模块,在Spring5中新增了WebFlux模块
八大模块简单介绍:
Spring Core:
它是Spring框架最为基础的部分,使用依赖注入来实现容器对Bean的管理。核心容器的主要组件是BeanFactory,它是工厂模式的一个的实现,是任何Spring应用的核心。使用IoC把应用配置和依赖从实际应用代码中分离出来。
Spring Context:
上下文模块是使用Spring成为框架的原因。它扩展了BeanFactory,增加了对国际化(I18N)消息、事件传播、验证的支持。另外还提供了很多企业服务,如:电子邮件,JNDI访问,EJB集成,远程以及时序调度(scheduing)服务。也包含了对模板框架如Velocity和FreeMarker集成的支持。
Spring AOP:
提供了对面向切面编程的丰富支持,基于Spring的应用程序中的对象提供了事务管理服务。通过使用Spring AOP,不用依赖组件,就可以把声明性事务管理集成到应用程序中,可以自定义拦截器、切点、日志等操作。
Spring DAO:
提供了一个JDBC的抽象层和异常层次结构,消除了繁琐的JDBC编码和数据库厂商特有的错误代码解析,用来简化JDBC。
Spring ORM:
它并不试图实现自己的ORM解决方案,而是为几种流行的ORM提供集成方案,包含Hibernate,JDO和iBATIS SQL映射,这些都遵从Spring的通用事务和DAO异常层次结构。
Spring Web MVC:
为构建Web应用提供了一个功能全面的MVC框架。
Spring WebFlux:
Spring Framework中包含的原始Web框架 Spring Web MVC是专门为Servlet API和Servlet容器构建的。反应式Web框架Spring WebFlux是在5.0版的后期添加的,它完全是非阻塞的,支持反应式流(Reactive Stream),并支持在Netty,Undertow和Serlvet 3.1+容器等服务器上运行。
Spring Web:
Web上下文模块建立在应用程序上下文模块之上,为基于Web的应用程序提供了上下文,提供了Spring与其它Web框架的集成,还提供了一些面向服务支持,如:实现文件上传的multipart请求。
Spring的特点
- 轻量
- 完整Spring框架可以在一个只有1MB多的jar文件里发布,并且Spring运行所需要的开销也很小
- Spring是非侵入式的,Spring应用中的对象不依赖于Spring的特定的类(侵入式:要依赖别人;非侵入式:不依赖别人提供的)
- 控制反转
- Spring的控制反转IoC,它促进了松耦合。当我们应用了IoC后,一个对象依赖其它对象会通过被动的方式传递进来,而不是这个对象自己去创建
- 面向切面
- Spring提供了对于面向切面编程的丰富支持,通过分离应用的业务逻辑与系统级服务(日志、权限、审计...)进行内聚性的开发。应用对象只需要实现业务逻辑,并不需要关注其它非业务逻辑的系统服务
- 容器
- Spring管理应用对象的配置和生命周期,它是一种容器,可以配置每个对象如何被创建以及各个对象之间是如何关联的
- 框架
- Spring可以把简单的组件配置、组合成为复杂的应用。在Spring中,应用对象被声明式地组合(典型的在xml中)。Spring也提供了很多基础功能(事务管理,持久化框架集成),把应用逻辑的开发留由应用开发人员。
三、入门程序
搭建入门程序
我们使用Maven来构建应用,当我们需要使用Spring IoC功能时需要引入spring-context依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.1.9</version>
</dependency>
一旦我们的模块中引入了spring-context依赖后,它会关联引入其它一些依赖
spring-aop:面向切面编程
spring-beans:IoC核心
spring-core:spring核心工具包
spring-expression:spring表达式
spring-jcl:spring日志包
mcrometer-observation:收集应用程序的度量数据
在我们的模块中添加junit依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
新增一个空的bean类:User
public class User {
}
在模块resources目录下新增Spring配置文件:beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userBean" class="com.xiaoxie.bean.User"></bean>
</beans>
注意配置文件中 <bean id="userBean" class="com.xiaoixe.bean.User"></bean>是用来定义一个bean的,其中当前bean标签中的两个属性说明如下:
id:代表对象的唯一标识
class:指定要创建对象的全限定类名
创建测试类:SpringTest
@Test
public void TestFirst() {
// 实始化容器(解析beans.xml文件,创建对应的bean对象)
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 要据id获取bean对象
Object userBean = context.getBean("userBean");
System.out.println(userBean); // com.xiaoxie.bean.User@1623b78d
}
}
剖析入门程序
下面的两部分我们对应起来看
<bean id="userBean" class="com.xiaoxie.bean.User"></bean>
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Object userBean = context.getBean("userBean");
简单的理解上面两块就是:在Spring的配置文件中配置了一个bean,然后在我们需要使用这个bean对象的地方去获取这个bean实例。
从上面两部分引申出来的问题
1、在配置文件的bean标签中有一个id属性,它的值可以重复吗?
id是不可以重复的,如果重复定义的话在运行时会报错:Configuration problem: Bean name 'userBean' is already used in this <beans> element
2、我们在使用bean实例的时候没有new,那Spring是如何给我们造出来的对象?
在Spring底层是通过反射机制调用无参构造方法来创建的对象。
3、如果我们定义User的时候不提供无参构造方法时会怎么样?
如果我们定义User类时不提供可用的无参构造方法,最终会在解析配置文件时报错:Error creating bean with name 'userBean' defined in class path resource [beans.xml]: Failed to instantiate [com.xiaoxie.bean.User]: No default constructor found
4、Spring创建对象的具体原理是怎么样的?
// 解析beans.xml,获得到bean对应的class
String className = "com.xiaoxie.bean.User";
Class<?> clazz = Class.forName(className);
// 通过反射机制创建对象
Constructor<?> constructor = clazz.getDeclaredConstructor();
Object obj = constructor.newInstance();
5、Spring把最终创建好的对象存储到哪里呢?
实际上创建好的对象会存储到一个Map<String,Object>的Map集合当中,其中key就是id,value就是我们实际构造出来的对象实例。
6、Spring的配置文件必须是beans.xml?
其实不是的,这个名称没有特别的要求,我们可以看看ClassPathXmlApplication的构造方法
从这里可以看到传入的配置文件的名称都可是多个,那多个肯定每个名称是不一样的,所以说对于配置文件的名称来说没有要求!
从这里我们也可以看出我们的配置文件可以存在多个,这样的话Spring会同时解析多个配置文件。
7、配置指定的Bean只能是我们自己定义的类吗?
不一定,比如像我们JDK中的类也是可以在配置文件中进行配置的,但是要求类不是抽象类并且要有可用的无参构造方法,比如:java.util.Date
<?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="userBean" class="com.xiaoxie.bean.User"></bean>
<!-- jdk中的类java.util.Date -->
<bean id="dateBean" class="java.util.Date"/>
</beans>
如上配置后我们可以像我们自定义的类一样直接拿来使用
@Test
public void TestFirst() {
// 实始化容器(解析beans.xml文件,创建对应的bean对象)
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 要据id获取bean对象
Object userBean = context.getBean("userBean");
System.out.println(userBean); // com.xiaoxie.bean.User@1623b78d
Object dateBean = context.getBean("dateBean");
System.out.println(dateBean); // Sat Aug 24 14:49:27 CST 2024
}
8、当我们调用getBean()方法时,指定的id不存在会怎么样?
如果指定的id不存在,调用getBean()时会报错:NoSuchBeanDefinitionException: No bean named 'userBean1' available,这个是Spring框架设计的方案,实际设计时可以是报异常或者返回null对象,但是返回null对象可能会产生一些歧义(是传id不对还是创建的就没有成功?),所以Spring框架在设计时使用抛出异常的方式。
9、getBean()方法返回的对象是Object类型,如果访问这个类特有的属性还要向下转型,如果明确类型不希望向下转型怎么处理?
这个时候可以在getBean()方法中传入第二个参数,用来指定具体返回实例的类型,如下所示
User userBean = context.getBean("userBean", User.class);
10、ClassPathXmlApplicationContext类从类路径读取配置文件,如果配置文件不在类路径下可以有什么方式读取?
从这个类的名称前缀(ClassPathXml)上可以看出来它是从类路径上找配置文件,如果我们的配置文件不在类路径上比如在指定的磁盘路径上则可以按下面的方式去读取配置文件
ApplicationContext context = new FileSystemXmlApplicationContext("d:/spring.xml");
注意:ApplicationContext是一个接口,它还有一个超级父接口BeanFactory
实际应用上来看这种方式使用是比较少的,一般都会把配置文件放在类路径中,这样的话移植性更好!
添加Log4j2日志框架
添加这个日志框架的步骤:
第一步:我们要引入Log4j2的依赖
<!--log4j2的依赖-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.19.0</version>
</dependency>
第二步:在类路径下提供配置文件:log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<loggers>
<!--
level指定日志级别,从低到高的优先级:
ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF
-->
<root level="DEBUG">
<appender-ref ref="springlog"/>
</root>
</loggers>
<appenders>
<!--输出日志信息到控制台-->
<console name="springlog" target="SYSTEM_OUT">
<!--控制日志输出的格式-->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/>
</console>
</appenders>
</configuration>
注意:这个配置文件就有要求了,必须要放在类路径的根路径下,文件名必须是log4j2.xml
第三步:使用日志框架打印日志
// 获取Logger实例
Logger logger = LoggerFactory.getLogger(SpringTest.class);
// 根据不同的日志级别要求输出日志信息
logger.info("Spring Test start...");
logger.info("{}", dateBean);