Java Spring框架

科长
2023-07-03 / 0 评论 / 80 阅读 / 正在检测是否收录...

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的形式创建出来的,其有部分缺点:

  1. 实例化一个组件比较复杂,例如UserServiceUserRoleService 都需要创建HikariDataSource ,实际上需要读取配置,才能实例化HikariConfig,然后再去实例化HikariDataSource
  2. 没有必要让每个Service组件分别去创建DataSource的实例,完全可以共享一个DataSource 实例,但是由谁去创建,谁去获取创建好的实例,都不好处理。同理UserInfoServlet或者其他的xxxServlet 需要使用UserService实例或者UserRoleService实例时,也应当共享同一个实例,但是比较复杂不好处理
  3. 很多组件需要销毁以便释放资源,例如DataSource,但如果组件被对多个组件共享,怎样确保他的使用方已被全部销毁
  4. 随着组件越来越多,需要共享的组件写起来非常困难,依赖关系也更加复杂

从上面几个问题中,不难看出,随着系统越来越庞大,组件越来越多,其组件的生命周期和相互之间的依赖关系如果由组件自身来维护,则会大大增加系统的复杂度,也会导致组件之间耦合度越来越高,测试维护越来越困难

因此,核心问题是:

  1. 谁负责创建组件
  2. 谁负责根据依赖关系组装组件
  3. 销毁的时候,如何按照依赖顺序正确销毁

解决这些问题的核心方案就是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,这种方式带来了很多好处:

  1. UserService不需要关心如何创建的DataSource,因此不需要再去写读取数据库配置之类的代码
  2. 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);

BeanFactoryApplicationContext的区别在于,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

我们以UserServiceUserRoleService 为例,我们给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非常简单,一共需要三步:

  1. 定义执行方法,并在方法上通过AspectJ的注解告诉Spring应该在何处调用此方法
  2. 标记@Component@Aspect
  3. @Configuration类上标注@EnableAspectJAutoProxy

4-2、拦截器类型

顾名思义,拦截器有以下类型:

  • @Before:这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了
  • @After:这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行
  • @AfterReturning:和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码
  • @AfterThrowing:和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码
  • @Around:能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能
2

评论 (0)

取消