Spring:IOC(控制反转 )、DI(依赖注入 )、AOP(通知类型、事务、拦截器)

一、回顾JavaWeb项目结构以及存在的问题

我们所说的Spring是广义上的,springmvc就是spring下的一个框架。他有很多框架,是广义上的。

Spring对每一层提供的东西:

以前我们写的代码结构以及存在的问题:

我们希望的是不手动new对象,还能有对象------解决方案:IOC:控制反转

二、JavaWeb项目存在问题的解决方案

IOC---控制反转:解决new对象的问题

之前由开发人员new对象 ,造成了代码过度耦合,所以我们由spring的IOC容器负责对象的创建和管理。需要哪个对象,就从SpringIOC的容器中直接获取就可以了。

我们只管类的定义,不管对象的创建,不用写new对象这个部分了,降低代码的耦合性。

IOC只解决了new对象的问题(=右边的解决了),没有解决赋值的问题,赋值的问题解决采用的DI--依赖注入

DI:依赖注入--解决赋值符号问题

接下来系统的学习Spring:

三、Spring理论学习

1.Spring是什么

Spring是分层的 Java SE/EE应用full-stack轻量级开源框架,以IOC(Inverse Of Control:反转控制)和 AOP(Aspect Oriented Programming:面向切面编程)为内核。

提供了表现层SpringMVC和持久层Spring JDBCTemplate以及业务层事务管理等众多的企业级应用技术,还能整合开源世界众多著名的第三方框架和类库,逐渐成为使用最多的Java EE 企业应用开源框架。

2 、Spring的优势

  1. 方便解耦,简化开发

通过Spring提供的IOC容器 ,可以将对象间的依赖关系交由Spring进行控制,避免硬编码所造成的过度耦合。

用户也不必再为单例模式类、属性文件解析等这些很底层的需求编写代码,可以更专注于上层的应用。

  1. AOP 编程的支持

通过Spring的AOP功能 ,方便进行面向切面编程,许多不容易用传统OOP实现的功能可以通过AOP轻松实现。

  1. 声明式事务的支持

可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式方式灵活的进行事务管理,提高开发效率和质量

  1. 方便程序的测试

可以用非容器依赖的编程方式进行几乎所有的测试工作,测试不再是昂贵的操作,而是随手可做的事情。

3.总结 :

IOC---控制反转:解决new对象的问题

DI:依赖注入--解决赋值符号问题

AOP---面向切面编程---在不修改源码的情况下增强源码的功能

声明式事务

以前我们对事务的操作是 编程式处理 ,也就是通过写代码的方式来解决问题:

如上所示,但是实际我们希望方法中只有相关的多个操作不希望出现其他与业务无关的代码,希望不写事务处理代码还能实现事务处理 ,Spring提供了解决方案:在方法上面添加注解:

四、Spring具体的实现

首先创建项目

以前new对象的方式:

java 复制代码
package com.stedu;

import com.stedu.dao.impl.UserDaoImpl;
import com.stedu.service.impl.UserServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ApplicationTests {

    @Test
    void contextLoads() {
    }

    @Test
    public void test1(){
        //以前我们new对象的方式:带来的问题:过度耦合
        UserServiceImpl userService = new UserServiceImpl();
        userService.insert();
    }

    @Test
    public void test2(){
        UserDaoImpl userDao = new UserDaoImpl();
         userDao.insert();
    }
}

1.让IOC容器实例化(创建)对象:

方式:在对应的实现类上面添加注解 并且类所在的层次比启动类低的话,就会自动扫描创建实现类对象:哪个类上面有这个注解,哪个类就会创建对象 :

java 复制代码
1.实例化对象 :---让Spring的IOC容器创建对象
@Controller---在Controller类上
@Service---在Service类上
@Repository---在Dao类上(如果使用mybatis不用写)
@Component---不属于任何一层的类上
说明:
(1)这四个注解作用没有任何区别
(2)建议使用的位置不同
(3)其实前三个的本质其实都是@Component,作用就是让IOC容器创建对象

目前就只是创建对象,但是还不能使用,

2.关于使用:依赖注入:@Autowired
@Autowired根据类型注入:字段注入/构造方法注入/set方法注入。

当同一个类型有多个实现类的时候:@Qualifier---结合@Autowired实现根据名称注入(注意:类必须有名称)

/或者当同一个类型有多个实现类的时候可以使用`@Resource(name = "要注入的实现类 类名")`也是根据名称注入:通过name值


3.目前存在的问题:
@Controller
@Service
@Repository
@Component
这四个注解只能实例化自定义类的对象,无法实例化第三方类(不是自定义的类)的对象。

解决方案:配置类+@Bean
@Configuration   在类上使用,表示这是一个配置类 

具体使用:

java 复制代码
package com.stedu.dao.impl;

import com.stedu.dao.UserDao;
import org.springframework.stereotype.Repository;

@Repository//依赖注入
public class UserDaoImpl implements UserDao {

    @Override
    public void insert() {
        System.out.println("userDao insert...");
    }
}
java 复制代码
package com.stedu.service.impl;

import com.stedu.service.UserService;
import org.springframework.stereotype.Service;


@Service//依赖注入
public class UserServiceImpl implements UserService {
    @Override
    public void insert() {
        System.out.println("userService insert....");
    }
}

2.DI---依赖注入

复制代码
关于使用:依赖注入:@Autowired根据类型注入

验证一下:在不手动创建对象的情况下还能自动创建对象调用方法:

2.1 什么是依赖注入?

依赖注入:Dependency Injection ,指容器负责创建和维护对象之间的依赖关系,而不是通过对象本身负责自己的创建和解决自己的依赖。在当前类需要用到其他类的对象,由Spring为我们提供,我们只需要在配置中(添加注解)说明。

业务层和持久层的依赖关系,在使用 Spring 之后,就让 Spring 来维护了。

简单的说,就是坐等框架把持久层对象传入业务层,而不用我们自己去获取。

2.2依赖注入方式

字段注入、set方法注入、构造 方法 注入 :

如果同一个类型,有多个 实现,怎么办

实现类起名字:在注入的时候额外添加注解 :


然后在注入的时候额外添加注解@Qualifier("要使用的实现类的名字")

或者另外一种解决方案:@Resource(name = "要注入的实现类 类名")

3.目前存在的问题(无法实例化第三方类)以及解决方案

@Controller

@Service

@Repository

@Component

这四个注解只能实例化自定义类的对象,无法实例化第三方类(不是自定义的类:比如ObjectMapper)的对象。

解决方案:配置类+@Bean
@Configuration 在 类上使用 ,表示这是一个配置类 (对项目进行配置)
@Bean使用在配置类的方法上 ,作用就是将方法的返回值放在Spring的 IOC容器中。

引入依赖:

xml 复制代码
   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

解决方案:配置类+@Bean:

java 复制代码
package com.stedu.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MainConfig {
    //假如我们项目中很多次用到这个ObjectMapper类,这个类不是自定义类 ,是第三方类,希望 IOC管理
    //但是这个不是自定义类,是第三方的类,无法使用之前的四个注解:@Service等等。
    //那怎么解决呢?在配置类中生成:想生成哪个第三方类的对象就要让这个方法返回这个第三方类对应的类型

    @Bean//2添加这个注解,放入Spring IOC容器中管理
    public ObjectMapper  objectMapper(){
        return new ObjectMapper();//1.完成了new对象
    }
}

这样就将第三方类创建对象交给了 Spring IOC进行处理了。

五 、Spring AOP

AOP:面向切面编程,在不修改Java类源码的情况下,对Java类代码进行增强

1.什么是AOP以及AOP的相关概念

什么是AOP

AOP为Aspect Oriented Programming的缩写,意思为面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。

AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP的作用及其优势

作用:在程序运行期间,在不修改源码的情况下对方法进行功能增强

优势:减少重复代码,提高开发效率,并且便于维护。

AOP的底层实现

实际上,AOP的底层是通过Spring提供的的动态代理技术实现的。

在运行期间,Spring通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,在去调用目标对象的方法,从而完成功能的增强。

AOP的相关概念

Spring的AOP实现底层就是对上面的动态代理的代码进行了封装,封装后我们只需要对需要关注的部分进行代码编写,并通过配置的方式完成指定目标的方法增强。

在正式学习之前,必须了解的几个概念

Target(目标对象):被增强的对象;有一个对象,想对代码增强,但是还不想修改源码 ,就可以将其设置成目标对象(没有被增强的对象 )

Proxy (代理):一个类被增强后,就产生一个结果代理类(代理=目标+增强);(增强之后的对象)

Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在Spring中,这些点指的是方法,因为Spring只支持方法类型的连接点;所以的方法都是连接点

Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义;实际增强的方法。切入点的本质还是连接点

Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint连接点之后所要做的事情就是通知;

Aspect(切面):是切入点和通知的结合;

Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。

目前先了解这些概念,特别注意加粗的几个概念。

现在就明确在Spring中有这么一种技术,能够在不修改方法代码的基础上对方法功能进行增强就行了。

总结

复制代码
Joinpoint(连接点):有可能被增强的方法
Pointcut(切入点):实际被增强的方法
Advice(通知/ 增强):封装增强业务逻辑的方法
Aspect(切面):切点+通知
Weaving(织入):将切点与通知结合的过程

AOP开发需要明确的事项

  1. 需要编写的内容

    编写核心业务代码(目标类的目标方法)
    编写切面类,切面类中有通知(增强功能方法)
    在配置文件中,配置织入关系,即将哪些通知与哪些连接点进行结合

换一种说法:

复制代码
谁是切点(切点表达式配置)
谁是增强(切面类中的增强方法)
将切点和通知进行织入配置
  1. AOP技术实现的内容

    Spring框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,
    动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,
    将通知对应的功能织入,完成完整的代码逻辑运行。

  2. AOP 底层使用哪种代理方式

在Spring中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。

2.通知的类型

环绕通知一个能代替四个其他的通知

就是相当于:

3.使用注解实现AOP_定义切面_切点表达式------通知

创建项目 :


没有提供aop,所以我们手动导入依赖 :

xml 复制代码
        <!--AOP:-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

service层:

java 复制代码
package com.study.service;

public interface UserService {
    void selectAll();

    void selectById();
}
java 复制代码
package com.study.service;
@Service
public class UserServiceImpl implements UserService{
    @Override
    public void selectAll() {
        System.out.println("selectAll...");
    }

    @Override
    public void selectById() {
        System.out.println("selectById...");
    }
}

我们想对service层里面的方法进行增强:还不想修改源码,(目前只是输出selectAll...,比如我们还想输出别的内容)可以使用aop增强:方式:创建切面类:

切面类要做两部分内容:AOP的使用

复制代码
1.通过切点表达式指定谁是切入点 
2.定义通知

做了这两个部分,就能整合到一块了。

示例:

java 复制代码
package com.study.aspect;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;

@Component//这个类不属于任何一层,所以使用@Component注解实例化类的对象,放在spring容器中
@Aspect//指定这是一个切面类
public class ServiceAspect {

    //1.定义切入点:指定全类名+方法
    @Pointcut("execution(void com.study.service.UserServiceImpl.selectAll()" +
            ")")
    public void  pt1(){

    }
    
    //2.前置通知:
    @Before("pt1()")
    public void before(){
        System.out.println("进入方法。。。。"+ LocalDateTime.now());
    }

    //2.后置通知:
    @AfterReturning("pt1()")
    public void after(){
        System.out.println("离开方法。。。。"+ LocalDateTime.now());
    }
}

输出结果:

其他通知类型示例;

java 复制代码
package com.study.service;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService{
    @Override
    public void selectAll() {
        System.out.println("selectAll...");
        //让代码出问题测试通知类型:
        String s = null;
        System.out.println(s.length());
    }

    @Override
    public void selectById() {
        System.out.println("selectById...");
    }
}
java 复制代码
package com.study.aspect;


import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component//这个类不属于任何一层,所以使用@Component注解实例化类的对象,放在spring容器中
@Aspect//指定这是一个切面类
public class ServiceAspect {

    //1.定义切入点:指定全类名+方法
    @Pointcut("execution(void com.study.service.UserServiceImpl.selectAll()" +
            ")")
    public void  pt1(){

    }

    //2.前置通知:
    @Before("pt1()")
    public void before(){
        System.out.println("进入方法。。。。"+ LocalDateTime.now());
    }

    //2.后置通知:
    @AfterReturning("pt1()")
    public void afterReturning(){
        System.out.println("离开方法。。。。"+ LocalDateTime.now());
    }

    //异常的:不出异常不会触发 :
    @AfterThrowing("pt1()")
    public void afterThrowing(){
        System.out.println("!!!!!!!!!");
    }

    //after:在程序的最后一定会执行的:
    @After("pt1()")
    public void after(){
        System.out.println("after");
    }
}

环绕通知

环绕通知就相当于其他四种通知,与其他四种通知不要一起并存

java 复制代码
package com.study.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component//这个类不属于任何一层,所以使用@Component注解实例化类的对象,放在spring容器中
@Aspect//指定这是一个切面类
public class ServiceAspect {

    //1.定义切入点:指定全类名+方法
    @Pointcut("execution(void com.study.service.UserServiceImpl.selectAll()" +
            ")")
    public void  pt1(){

    }

    //环绕通知:
    @Around("pt1()")
    public void around(ProceedingJoinPoint pt){
        try{
            //前置通知
            System.out.println("进入方法。。。。"+ LocalDateTime.now());

            //切入点:
            pt.proceed();

            //后置通知:
            System.out.println("离开方法。。。。"+ LocalDateTime.now());
        } catch (Throwable e) {
            //异常通知;
            System.out.println("!!!!");
            throw new RuntimeException(e);
        } finally {
            //最终通知:
            System.out.println("after");
        }

    }

}

获取方法名称

如何获取方法名称,知道打印的是哪个方法:

环绕通知里面:

复制代码
ProceedingJoinPoint pt
pt.getSignature().getName()

其他四种通知里面获取名字:

复制代码
JoinPoint p
p.getSignature().getName()

环绕通知注意事项及完善

返回值类型要对应

*能匹配全部的

之前我们写的环绕通知并不完善:

完善后的;

java 复制代码
package com.study.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component//这个类不属于任何一层,所以使用@Component注解实例化类的对象,放在spring容器中
@Aspect//指定这是一个切面类
public class ServiceAspect {

    //1.定义切入点:指定全类名+方法
    @Pointcut("execution(* com.study.service.UserServiceImpl.*(..)" +
            ")")
    public void  pt1(){

    }
    //环绕通知:
    @Around("pt1()")
    public Object around(ProceedingJoinPoint pt){
        try{
            //前置通知
            System.out.println("进入方法。。。。"+ LocalDateTime.now());

            //切入点:

            Object result = pt.proceed(pt.getArgs());

            //后置通知:
            System.out.println("离开方法。。。。"+ LocalDateTime.now());

            return result;
        } catch (Throwable e) {
            //异常通知;
            System.out.println("!!!!");
            throw new RuntimeException(e);
        } finally {
            //最终通知:
            System.out.println("after");
        }

    }
}

4.事务处理---声明式事务处理---AOP技术:@Transactional注解

在需要进行事务处理的部分上面标注上@Transactional注解就能进行事务处理了。

项目搭建:Mybatis、mysql、配置xml文件 ,写service层Mapper接口,标注解,转账

Mapper层:

转账的业务,转入和转出:


在启动类 上面 添加注解:扫描mapper层的全部类,要写全类名。

业务类:

实现类上面添加 @Transactional注解:运行时异常可以正常回滚

失效的情景:抛出的是编译时异常(不是运行时异常)事务无法回滚

解决方案:

复制代码
  @Transactional(rollbackFor = Exception.class)

5.拦截器

拦截器--拦截请求的

1、拦截器

拦截器Interceptor和前面学习的过滤器Filter类似,但是他们还是有些区别的,区别如下:

过滤器Filter在任何基于Servlet的框架都可以使用,而拦截器Interceptor是Spring MVC独有的;

过滤器Filter配置/*拦截所有资源,所有静态资源都会被拦截,拦截器也是。

拦截器可以用在权限验证,比如在访问后台资源的时候,经过拦截器看请求有没有进行身份验证,身份验证通过后放行,否则跳转会后台登陆页面。

拦截器的使用步骤 :如何使用拦截器

(1)定义拦截器,实现HandlerInterceptor接口

(2)配置拦截器,在自定义的配置类中配置

复制代码
   1-这个配置类要实现WebMvcConfigurer接口

   2-重写addInterceptors方法,添加拦截器并配置拦截器拦截和排除的路径
1.1、自定义拦截器

如果我们需要定义拦截器必须实现HandlerInterceptor接口,并重写接口中的3个方法。3个方法描述如下:

preHandle方法:该方法在控制器的处理请求方法前执行,其返回值表示是否中断后续操作,返回true表示继续向下执行,返回false表示中断后续操作;(最常用的)

postHandle方法:该方法在控制器的处理请求方法调用之后、解析视图之前执行,可以通过此方法对请求域中的模型和视图做进一步的修改。

afterCompletion方法:该方法在控制器的处理请求方法执行完成后执行,即视图渲染结束后执行,可以通过此方法实现一些资源清理、记录日志信息等工作。

(1)定义拦截器,实现HandlerInterceptor接口

java 复制代码
/**
 * 定义拦截器:会在控制器的处理请求方法前执行
 * true:放行
 * false:拦截
 */
 @Compoent
public class TestInterceptor implements HandlerInterceptor {
    //主要逻辑:在Controller之前执行:抽取Controller中的冗余代码
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
        System.out.println("pre...");
        return true;//放行,后续的拦截器或Controller就会执行
    }
    //在Controller之后执行:进一步的响应定制
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("post...");
    }
    //在页面渲染完毕之后,执行:资源回收
    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        System.out.println("after...");
    }
}
1.2 拦截器的配置

(2)配置拦截器,在自定义的配置类中配置

复制代码
   1-这个配置类要实现WebMvcConfigurer接口

   2-重写addInterceptors方法,添加拦截器并配置拦截器拦截和排除的路径