1、什么是Spring?
Spring 是一个支持快速开发的Java EE应用程序框架。他提供了一系列底层容器和基础设施,并可以快速集成大量常用的开源框架。是开发Java EE项目的必备。
2、Spring Framework
Spring Framework 主要包括几个模块:
- 支持 IoC 和 AOP 的容器
- 支持 JDBC 和 ORM 的数据访问模块
- 支持申明式事物模块
- 支持基于 Servlet 的 MVC开发
- 支持基于 Reactive 的 WEB 开发
- 以及集成 JMS、JavaMail、JMX、缓存等其他模块
3、IoC 容器
3-1、初识IoC、 JavaBean
Spring 提供的容器又叫做 IoC容器。IoC全称Inversion of Control
,直译为控制反转。
为什么需要IoC?按照以往的开发的方式假设我们需要通过UserService
来获取指定用户的信息:
public class UserService {
private HikariConfig config = new HikariConfig();
private DataSource dataSource = new HikariDataSource(config);
public User getUserById(Long userId){
try(Connection conn = dataSource.getConnection()){
...
return user;
}
}
}
为了从数据库中查询出用户,UserService
持有一个DataSource
。为了实例化一个HikariDataSource
,又需要实例化一个HikariConfig
查询完用户,比如需要通过UserRoleService
再去查询用户的角色信息:
public class UserRoleService {
private HikariConfig config = new HikariConfig();
private DataSource dataSource = new HikariDataSource(config);
public User getUserRoleByUserId(Long userId){
try(Connection conn = dataSource.getConnection()){
...
return role;
}
}
}
因为UserRoleService
中也需要去访问数据库,所以不得不又实例化了一个HikariDataSource
现在我们处理用户发送过来的请求:
public class UserInfoServlet extends HttpServlet {
private UserService userService = new UserService();
private UserRoleService userRoleService = new UserRoleService();
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
long userId = getFromCookie(req);
User user = userService.getUserById(userId);
Role role = userRoleService.getUserRoleByUserId(userId);
....
}
}
上述的每一个Service组件
都是以new
的形式创建出来的,其有部分缺点:
- 实例化一个组件比较复杂,例如
UserService
和UserRoleService
都需要创建HikariDataSource
,实际上需要读取配置,才能实例化HikariConfig
,然后再去实例化HikariDataSource
- 没有必要让每个
Service组件
分别去创建DataSource
的实例,完全可以共享一个DataSource
实例,但是由谁去创建,谁去获取创建好的实例,都不好处理。同理UserInfoServlet
或者其他的xxxServlet
需要使用UserService
实例或者UserRoleService
实例时,也应当共享同一个实例,但是比较复杂不好处理 - 很多组件需要销毁以便释放资源,例如
DataSource
,但如果组件被对多个组件共享,怎样确保他的使用方已被全部销毁 - 随着组件越来越多,需要共享的组件写起来非常困难,依赖关系也更加复杂
从上面几个问题中,不难看出,随着系统越来越庞大,组件越来越多,其组件的生命周期和相互之间的依赖关系如果由组件自身来维护,则会大大增加系统的复杂度,也会导致组件之间耦合度越来越高,测试维护越来越困难
因此,核心问题是:
- 谁负责创建组件
- 谁负责根据依赖关系组装组件
- 销毁的时候,如何按照依赖顺序正确销毁
解决这些问题的核心方案就是IoC容器
在传统的项目中,控制权在程序本身,程序的控制流程完全由开发者控制,例如:
UserInfoServlet
创建了UserService
, 在创建UserService
的时候又创建了DataSource
组件。这种模式的缺点就是,一个组件如果要使用另一个组件,就必须要先知道如何正确的创建它,否则无法正常使用
在IoC模式下,控制权发生了反转,即从应用程序转移到了IoC容器,所以组件不再由应用程序自己创建和配置,而是由IoC容器负责。应用程序只需要直接使用已经创建好并且配置好的组件。为了能让组件在IoC容器中被“装配”出来,需要某种“注入”机制。例如UserService
自己并不会去创建DataSource
,而是等待外部通过setDataSource()
方法来注入一个配置好的DataSource
:
public class UserService {
private DataSource dataSource;
public void setDataSource(DataSource dataSource){
this.dataSource = dataSource;
}
}
不需要再通过new
的形式来实例化一个DataSource
组件,而是通过注入的方式注入一个DataSource
,这种方式带来了很多好处:
UserService
不需要关心如何创建的DataSource
,因此不需要再去写读取数据库配置之类的代码DataSource
被注入到UserService
同样也可以被注入到UserRoleService
中,共享一个组件变得非常简单
因此IoC又称为依赖注入,它解决了一个最主要的问题:将组件的创建+配置与组件的使用分离开,并且由IoC容器负责管理组件的生命周期
因为IoC容器要负责实例化所有的组件,因此需要告诉容器如何创建组件,以及之间的依赖关系。
通过配置XML
文件的来实现:
<beans>
<bean id="dataSource" class="HirkariDataSource" />
<bean id="userService" class="UserService">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="userRoleService" class="UserRoleService">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
上述XML
配置文件指示IoC容器创建三个JavaBean组件,并把id为dataSource
的组件通过属性dataSource
(调用setDataSource()
函数)注入到了另外两个组件中,在Spring IoC中,我们把所有的组件统称为JavaBean
,即配置一个组件就是配置一个JavaBean
依赖注入方式
从上面可以看出,依赖注入可以通过set()
方法实现。同时依赖注入也可以通过构造方法实现
通过有参构造注入依赖:
public class UserService {
private DataSource dataSource;
public UserService(DataSource dataSource) {
this.dataSource = dataSource;
}
}
无侵入容器
Spring的IoC容器是一个高度可拓展的无侵入容器。所谓无侵入就是指应用程序的组件无需实现Spring的特定接口,或者说组件根本不知道自己在Spring的容器中运行
3-2、配置JavaBean的方式
项目目录结构
spring-ioc-appcontext
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── bystart
│ └── learnjava
│ ├── Main.java
│ └── service
│ ├── User.java
│ ├── UserService.java
│ └── UserRoleService.java
└── resources
└── application.xml
3-2-1、使用XML
配置
application.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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userService" class="com.bystart.learnjava.service.UserService"/>
<bean id="userRoleService" class="com.bystart.learnjava.service.UserRoleService" />
<!-- 注入一些属性案例
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test" />
<property name="username" value="root" />
<property name="password" value="password" />
<property name="maximumPoolSize" value="10" />
<property name="autoCommit" value="true" />
</bean>
-->
<!-- 注入一个Bean 案例
<bean id="userService" class="com.bystart.learnjava.service.UserService">
<property name="dataSource" ref="dataSource" />
</bean>
-->
</beans>
Main.java
文件内容:
public class Main {
public static void main(String[] args){
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
UserService userService = context.getBean(UserService.class);
UserRoleService userRoleService = context.getBean(UserRoleService.class);
User loginUser = userService.login("username@qq.com","password");
loginUser.setRoleName(userRoleService.getUserRoleNameByUserId(loginUser.getUserId()));
}
}
我们从创建Spring的代码中可以看出,Spring容器就是ApplicationContext
,它是一个接口,有很多实现类, 这里我们选择ClassPathXmlApplicationContext
表示它会自动从classpath
中查找指定的xml配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
获得了ApplicationContext
的实例,就获得了IoC容器的引用。从ApplicationContext
中我们可以根据Bean的id获取Bean,但更多的时候一般根据Bean的类型来获取Bean的引用
UserService userService = context.getBean(UserService.class);
Spring还提供了另一种IoC容器叫BeanFactory
,使用方式和ApplicationContext
类似:
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("application.xml"));
UserService userService = factory.getBean(UserService.class);
BeanFactory
和ApplicationContext
的区别在于,BeanFactory
的实现是按需创建,即第一次获取Bean时才创建这个Bean,而ApplicationContext
会一次性创建所有的Bean。实际上,ApplicationContext
接口是从BeanFactory
接口继承而来的,并且,ApplicationContext
提供了一些额外的功能,包括国际化支持、事件和通知机制等。通常情况下,我们总是使用ApplicationContext
,很少会考虑使用BeanFactory
3-2-2、使用注解配置
我们可以使用更简单的方式来配置一个JavaBean,那就是使用注解@Component
:
@Component
public class UserRoleService {
}
@Component
注解就相当于定义了一个Bean,它有一个可选的名称,默认是userRoleService
,即小写开头的类名
想使用这个Bean也是非常的简单只需要加一个 @Autowired
,自动注入到属性,想要注入对应的依赖,则自己也需要被IoC托管,需要加上@Component
注解:
@Component
public class UserService {
@Autowired
private UserRoleService userRoleService;
}
@Autowired
还可以加入到构造方法中
@Component
public class UserService {
private UserRoleService userRoleService;
public UserService(@Autowired UserRoleService userRoleService){
this.userRoleService = userRoleService;
}
}
开启注解扫描,我们在Main.java
添加部分代码:
@Configuration
@ComponentScan
public class Main {
public static void main(String[] args){
ApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
UserService userService = context.getBean(UserService.class);
UserRoleService userRoleService = context.getBean(UserRoleService.class);
User loginUser = userService.login("username@qq.com","password");
loginUser.setRoleName(userRoleService.getUserRoleNameByUserId(loginUser.getUserId()));
}
}
Main.class
中加入一个注解@Configuration
,表示他是一个配置类,因为在创建ApplicationContext
时:
ApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
使用的实现类是AnnotationConfigApplicationContext
,他的构造方法必须传入一个标注了@Configuration
的类,此外Main.java
还标注了@ComponentScan
它表示告诉容器,自动搜索扫描当前类所在的包及子包下所有标注了 @Component
的类给创建到IoC容器中进行托管,并根据@Autowired
自动装配
使用Annotation配合自动扫描能大幅简化Spring的配置,我们只需要保证:
- 每个Bean被标注为
@Component
并正确使用@Autowired
注入 - 配置类被标注为
@Configuration
和@ComponentScan
- 所有Bean均在指定包以及子包内
4、Spring AOP
在AOP编程中,我们经常会遇到下面的概念:
- Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点
- Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行
- Pointcut:切入点,即一组连接点的集合
- Advice:增强,指特定连接点上执行的动作
- Introduction:引介,指为一个已有的Java对象动态地增加新的接口
- Weaving:织入,指将切面整合到程序的执行流程中
- Interceptor:拦截器,是一种实现增强的方式
- Target Object:目标对象,即真正执行业务的核心逻辑对象
- AOP Proxy:AOP代理,是客户端持有的增强后的对象引用
4-1、装配AOP
我们以UserService
和 UserRoleService
为例,我们给UserService
的每个业务的方法执行前添
加日志,给UserRoleService
每个方法执行前后
添加日志记录
我们定义一个LoggingAspect
:
@Aspect
@Component
public class LoggingAspect {
// 在 UserService 的每个方法执行前执行该部分代码
@Before("execution(public * com.bystart.learnjava.service.UserService.*(..))")
public void doAddUserServiceLog(){
System.out.println("开始添加日志");
...
}
// 在 UserRoleService 的每个方法 执行前/后 执行该部分代码
@Around("execution(public * com.bystart.learnjava.service.UserRoleService.*(..))")
public Object doAddUserRoleServiceLog(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("开始添加执行前日志");
// 获取目标方法的返回内容
Object retVal = pjp.proceed();
System.err.println("开始添加执行后的日志,目标方法返回内容:" + pjp.getSignature());
// 真正返回内容给调用方
return retVal;
}
}
观察doAddUserServiceLog()
方法,我们定义了一个@Before
注解,后面的字符串是告诉AspectJ应该在何处执行该方法,这里写的意思是:执行UserService
的每个public
方法前执行doAddUserServiceLog()
代码
再观察doAddUserRoleServiceLog()
方法,我们定义了一个@Around
注解,它和@Before
不同,@Around
可以决定是否执行目标方法,因此,我们在doAddUserRoleServiceLog()
内部先打印日志,再调用方法,最后打印日志后返回结果
在LoggingAspect
类的声明处,除了用@Component
表示它本身也是一个Bean外,我们再加上@Aspect
注解,表示它的@Before
标注的方法需要注入到UserService
的每个public
方法执行前,@Around
标注的方法需要注入到UserRoleService
的每个public
方法执行前后
紧接着,我们需要给@Configuration
类加上一个@EnableAspectJAutoProxy
注解:
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class Main {
public static void main(String[] args){
ApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
UserService userService = context.getBean(UserService.class);
UserRoleService userRoleService = context.getBean(UserRoleService.class);
User loginUser = userService.login("username@qq.com","password");
loginUser.setRoleName(userRoleService.getUserRoleNameByUserId(loginUser.getUserId()));
}
}
Spring的IoC容器看到这个注解,就会自动查找带有@Aspect
的Bean,然后根据每个方法的@Before
、@Around
等注解把AOP注入到特定的Bean中
那么LoggingAspect
是怎么实现将方法注入到其他Bean,并且在目标方法执行前后触发LoggingAspect
的方法的呢
其实原理非常的简单,它是创建了一个代理对象:
public class UserServiceAopProxy extends UserService {
private UserService target;
private LoggingAspect aspect;
public UserServiceAopProxy(UserService target, LoggingAspect aspect){
this.target = target;
this.aspect = aspect;
}
public User login(String email, String password){
// 先执行Aspect的代码
aspect.doAddUserServiceLog();
// 再执行原有的逻辑
return target.login(email, password);
}
}
这些都是Spring容器启动时为我们自动创建的注入了Aspect的子类,它取代了原始的UserService
(原始的UserService
实例作为内部变量隐藏在UserServiceAopProxy
中)。如果我们打印从Spring容器获取的UserService
实例类型,它类似UserService$$EnhancerBySpringCGLIB$$1f44e01c
,实际上是Spring使用CGLIB动态创建的子类,但对于调用方来说,感觉不到任何区别。
Spring对接口类型使用JDK动态代理,对普通类使用CGLIB创建子类。如果一个Bean的class是final,Spring将无法为其创建子类。
可见,虽然Spring容器内部实现AOP的逻辑比较复杂(需要使用AspectJ解析注解,并通过CGLIB实现代理类),但我们使用AOP非常简单,一共需要三步:
- 定义执行方法,并在方法上通过AspectJ的注解告诉Spring应该在何处调用此方法
- 标记
@Component
和@Aspect
- 在
@Configuration
类上标注@EnableAspectJAutoProxy
4-2、拦截器类型
顾名思义,拦截器有以下类型:
- @Before:这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了
- @After:这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行
- @AfterReturning:和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码
- @AfterThrowing:和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码
- @Around:能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能
评论 (0)