一、IOC容器

1.1 IOC概念

控制反转:把对象创建和对象的调用过程交给spring进行管理。

  • 目的:降低耦合度。
  • 底层原理:xml,反射,工厂模式

Spring提供IOC容器两种实现方式(两个接口)

  • BeanFactory:Spring内部使用的接口,不提倡开发人员使用。特点:加载配置文件时不会创建对象,获取对象时才会创建对象。
  • **ApplicationContext:**BeanFactory的子接口,提供了更多更强大的功能,一般由开发人员使用。特点:加载配置文件时会把配置文件里的对象进行创建。

ApplicationContext两个常用实现类:

  • FileSystemXmlApplicationContext:绝对路径,从盘符开始算起
  • ClassPathXmlApplicationContext:相对路径,从src开始算起

什么是Bean管理?

  • Bean管理是指两个操作:Spring创建对象、Spring注入属性
  • Bean管理有两种操作方式:基于xml配置文件方式实现 和 基于注解方式实现

1.2 IOC底层原理

1.2.1 其他IOC方式

以前:

数据存放在mysql数据库中,业务层想要获取数据,就得new一个mysql对象获取数据。

如果来一个需求,另一个用户想从oracle存放数据,service想要从orcle获取数据,就得需要修改service上的代码。从哪个数据库获取数据,由service控制,这样的问题导致耦合性高。

Spring_动态代理

IOC方式:

定义一个数据库接口类,mysql类实现该接口,service 只声明数据库接口类,不负责new对象,使用set方法,交给用户来负责new哪个对象。

后面如果添加获取数据方式(比如oracle),就不需要修改service代码,只需要用户new一个实现类对象传给service就行了。

Spring_IOC_02

1.2.2 spring 的IOC

IOC只是一个思想,有很多种实现方式,上述讲解的实现只是其中一种,spring的IOC实现是DI(依赖注入),与上述的实现方式不一样。

DI:对象的创建,以前从代码new来控制,转到由spring容器来进行控制。

spring对象的创建,还可以使用单例模式,解决了大量new对象,导致资源消耗。从前的单例模式,需要写很多代码,导致高耦合。


1.3 Spring IOC容器加载过程(超重点!!!)

(1)简单过程:

  1. beanFactory先去扫描.xml、.class文件,解析扫描出来的类信息(扫描所有.class类,看看类上面有没有@Component,有就注册为BeanDefinition  [ˌdefɪˈnɪʃn]
  2. 实例化一个BeanDefinition对象来存储解析出来的信息,把实例化好的beanDefinition对象put到beanDefinitionMap当中缓存起来。
  3. 循环第1、2步,直到所有的BeanDefinition对象放到beanDefinitionMap中
  4. 然后Spring通过BeanDefinition对象,使用反射的方式实例化一个对象并存入容器中。

(2)详细过程

从概念态到定义态的过程

1、实例化一个ApplicationContext的对象;

2:调用bean工厂后置处理器(invokeBeanFactoryPostProcessors)完成扫描;

3:循环解析扫描出来的类信息(扫描所有.class类,看看类上面有没有@Component,有就注册为BeanDefinition

4、实例化一个BeanDefinition对象来存储解析出来的信息;

5、把实例化好的beanDefinition对象put到beanDefinitionMap当中缓存起来, 以便后面实例化bean;

6、再次调用其他bean工厂后置处理器;

从定义态到纯净态(还没有依赖注入)

7:当然spring还会干很多事情,比如国际化,比如注册BeanPostProcessor等等,如果我们只关心如何实例化一个bean的话那么这一步就是spring调用 finishBeanFactoryInitialization方法来实例化单例的bean,实例化之前spring要做验证, 需要遍历所有扫描出来的类,依次判断这个bean是否Lazy,是否prototype,是否 abstract等等;(单例、不是懒加载、不是抽象才会加载)

8:如果验证完成spring在实例化一个bean之前需要推断构造方法,因为spring实例化对象是通过构造方法反射,故而需要知道用哪个构造方法;

9:推断完构造方法之后spring调用构造方法反射实例化一个对象;注意这个时候对象已经实例化出来了,但是并不是一个完整的bean, 最简单的体现是这个时候实例化出来的对象属性是没有注入,所以不是一个完整的bean;

从纯净态到成熟态

10:spring处理合并后的beanDefinition

11:判断是否需要完成属性注入

12:如果需要完成属性注入,则开始注入属性初始化

13、判断bean的类型回调Aware接口

14、调用生命周期回调方法

15、如果需要代理则完成代理

创建完成

16、put到单例池——bean完成——存在spring容器当中  




二、IOC 的 Bean管理

2.1 基于xml配置方式 注册Bean

依赖注入:

  • 构造器注入:有参、无参方式。
  • set注入
  • p命名空间注入
  • c命名空间注入


2.2 基于注解方式 注册Bean

spring针对Bean管理中创建对象提供注解:

  • @Component: 游戏中普通的注解
  • @Service :业务逻辑层以及Service层
  • @Controller: 外部层
  • @Repository :dao层即持久层


功能是一样的,都可以用来创建对象,只不过把每个对象用在不同地方,以便查看

注解方式注册bean

  • @Autowired:根据属性类型自动装配
@Autowired  //根据类型进行注入
private UserDao userDao;
  • @Qualifier(value=" "):根据属性名称自动注入
@Autowired  //根据类型进行注入
@Qualifier(value = "userDaoImpl1") //根据名称进行注入
private UserDao userDao;
  • @Resource:可根据属性类型或者名称注入
@Resource(name = "userDaoImpl1")  //根据名称进行注入
private UserDao userDao;
  • @Value:注入普通类型的注入

       注解不是对象类型的定义,可以是字符串等其他


@Value(value = "abc")
private String name;


2.3 bean作用域

spring本身默认的时候是单实例。

作用域:

  • singleton 单实例对象,默认是这个。加载spring配置的时候就会创建对象
  • prototype 多实例对象。调用getBean方法的时候就会创建对象。
  • request:在一次Http请求中,容器会返回该Bean的同一实例。而对不同的Http请求则会产生新的Bean,而且该bean仅在当前Http Request内有效。
  • session:在一次Http Session中,容器会返回该Bean的同一实例。而对不同的Session请求则会创建新的实例,该bean实例仅在当前Session内有效。


2.4 xml自动装配


  • byName:根据属性名称注入 ,注入值bean的id值和类属性名称一样
  • byType:根据属性类型注入


三、AOP

3.1 静态动态代理

静态代理:

对于一个类,给类添加功能就得动类上的代码。因此添加一个代理类,代理这些其功能,只需要调用代理类就可以了。

例子:代理相当于中介。房东出租房子,但只提供房子服务,又不想负责看房子、找人等等其他业务,则交给中间,中介负责除了房子以外的业务。

动态代理:

如果存在多个类需要添加相同的功能,就得写多个代理类,很麻烦。动态代理则动态的生成代理类,写一个代理代码,动态生成多个代理类。

3.2 动态代理2种方式

3.2.1 JDK的动态代理

JDK的动态代理是基于反射实现。JDK通过反射方式,获取需求代理的类信息,生成一个代理类。

JDK代理的是接口,不是类。类只有实现了该接口才能被代理。

当我们通过代理对象执行原来那个类的方法时,代理类底层会通过反射机制,获取实现类的信息。

优点:

  1. JDK动态代理是JDK原生的,不需要任何依赖即可使用;
  2. 通过反射机制生成代理类的速度要比CGLib操作字节码生成代理类的速度更快

3.2.2 CGLib动态代理

采用了ASM字节码生成框架,直接对需要代理的类的字节码进行操作。

生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将我们定义的额外的逻辑(简单理解为Spring中的切面)织入到方法中,对方法进行了增强。

3.2.3 区别

JDK动态代理:

  1. JDK动态代理是JDK原生的;
  2. 使用的是反射,
  3. 代理的必须是接口。
  4. 通过反射机制生成代理类的速度要比CGLib操作字节码生成代理类的速度更快;

CGLib动态代理:

  1. 代理的不是接口,是普通类。
  2. 使用字节码文件,速度比JDK动态代理慢。

3.3 AOP介绍

AOP:不通过修改源代码方式,在主干功能里面添加新功能。

使用登录例子说明 AOP:

Spring_IOC_03

aop动态代理实现:默认使用JDK动态代理实现切面。当目标类实现了接口时,Spring会使用JDK动态代理;当目标类没有实现接口时,Spring会使用CGLIB动态代理

连接点:类里面哪些方法可以被增强,这些方法称为连接点

切入点:实际被真正增强的方法称为切入点

通知(增强):实际增强的逻辑部分称为通知,且分为以下五种类型:

  • 前置通知 :在连接点前面执行,前置通知不会影响连接点的执行,除非此 处抛出异常
  • 后置通知 :在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行
  • 异常通知 :在连接点抛出异常后执行
  • 环绕通知:环绕通知围绕在连接点前后,能在方法调用前后自定义一些操作
  • 最终通知 :在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容

切面:应用到切入点过程


3.4 使用注解实现

第一步:编写一个注解实现的增强类

package com.kuang.config;
 
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
 
@Aspect
public class AnnotationPointcut {
    @Before("execution(* com.kuang.service.UserServiceImpl.*(..))")
    public void before(){
        System.out.println("---------方法执行前---------");
    }
 
    @After("execution(* com.kuang.service.UserServiceImpl.*(..))")
    public void after(){
        System.out.println("---------方法执行后---------");
    }
 
    @Around("execution(* com.kuang.service.UserServiceImpl.*(..))")
    public void around(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("环绕前");
        System.out.println("签名:"+jp.getSignature());
        //执行目标方法proceed
        Object proceed = jp.proceed();
        System.out.println("环绕后");
        System.out.println(proceed);
    }
}

第二步:在Spring配置文件中,注册bean,并增加支持注解的配置

<!--第三种方式:注解实现-->
<bean id="annotationPointcut" class="com.kuang.config.AnnotationPointcut"/>
<aop:aspectj-autoproxy/>

aop:aspectj-autoproxy:说明


通过aop命名空间的<aop:aspectj-autoproxy />声明自动为spring容器中那些配置@aspectJ切面的bean创建代理,织入切面。当然,spring 在内部依旧采用AnnotationAwareAspectJAutoProxyCreator进行自动代理的创建工作,但具体实现的细节已经被<aop:aspectj-autoproxy />隐藏起来了
 
<aop:aspectj-autoproxy />有一个proxy-target-class属性,默认为false,表示使用jdk动态代理织入增强,当配为<aop:aspectj-autoproxy  poxy-target-class="true"/>时,表示使用CGLib动态代理技术织入增强。不过即使proxy-target-class设置为false,如果目标类没有声明接口,则spring将自动使用CGLib动态代理。

四、自定义注解与AOP

4.1 介绍

JDK动态代理与自定义注解结合使用,注解在方法、类上使用,可以实现功能代理。

定义注解:使用 @interface

public @interface LogInfo {
}

元注解介绍:

  1. @Retention(修饰注解,一般为RetentionPolicy.RUNTIME):
  • RetentionPolicy.SOURCE:注解仅存在于源代码中,在编译时会被丢弃。这种类型的注解通常用于提供编译时的辅助信息,不会对运行时产生影响。
  • RetentionPolicy.CLASS:注解存在于编译后的字节码文件中,但在运行时会被丢弃。这种类型的注解可以在编译时对代码进行一些处理,但不会影响程序运行时的行为。
  • RetentionPolicy.RUNTIME:注解在运行时可以通过反射获取到。这种类型的注解可以在运行时对程序的行为进行动态调整,例如在 AOP(面向切面编程)中经常使用。
  1. @Target(定义注解的使用范围):
  • ElementType.METHOD:指定注解可以应用于方法。
  • ElementType.FIELD:指定注解可以应用于字段、枚举常量。
  • ElementType.TYPE:类、接口、枚举、注解。
  • ElementType.PARAMETER:方法的参数。
  • ElementType.CONSTRUCTOR:构造函数。
  • @Target(ElementType.LOCAL_VARIABLE):局部变量
  • @Target(ElementType.ANNOTATION_TYPE) :注解
  • @Target(ElementType.PACKAGE)
  1. @Documented(文档化):
  • 当一个注解被 @Documented 修饰时,这个注解将会包含在 Javadoc 生成的文档中,使得注解的信息可以被文档化展示。
  1. @Inherited(继承性):
  • 如果一个注解被 @Inherited 修饰,那么子类会继承父类的该注解。这对于一些需要在继承关系中传递注解的情况非常有用。
  1. @Repeatable(可重复性):
  • 允许一个注解在同一个目标上被多次应用,而不需要使用容器注解来包裹多个相同的注解实例。这样可以使代码更加简洁和易读。

4.2 实例

(1)导入aop依赖

<!--AOP-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

(2)定义注解:

@Target(ElementType.METHOD)  //作用在方法上
@Retention(RetentionPolicy.RUNTIME) //注解在运行时可以通过反射获取到
public @interface MyLog {
    /**
     * 接口名称
     */
    String name() default "";
}

(3)实现aop

@Aspect // 1.表明这是一个切面类
@Component
public class MyLogAspect {

    // 2. PointCut表示这是一个切点,@annotation表示这个切点切到一个注解上,后面带该注解的全类名
    // 切面最主要的就是切点,所有的故事都围绕切点发生
    // logPointCut()代表切点名称
    @Pointcut("@annotation(me.zebin.demo.annotationdemo.aoplog.MyLog)")
    public void logPointCut(){};

    // 3. 环绕通知
    @Around("logPointCut()")
    public void logAround(ProceedingJoinPoint joinPoint){
        // 获取方法名称
        String methodName = joinPoint.getSignature().getName();
        // 获取入参
        Object[] param = joinPoint.getArgs();

        StringBuilder sb = new StringBuilder();
        for(Object o : param){
            sb.append(o + "; ");
        }
        System.out.println("进入[" + methodName + "]方法,参数为:" + sb.toString());

        // 继续执行方法
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println(methodName + "方法执行结束");

    }
}

(4)测试

@MyLog("访问资源")
    @GetMapping("/sourceC/{source_name}")
    public String sourceC(@PathVariable("source_name") String sourceName){
        return "你正在访问sourceC资源";
    }

4.3 获取注解等信息

// 获取方法名称
String methodName = joinPoint.getSignature().getName();

// 获取入参
Object[] param = joinPoint.getArgs();

//获取注解类信息
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
MyLog myLog = method.getAnnotation(MyLog.class);
myLog.name(); //获取name值


五、拦截器

(1)定义拦截器:

三个方法不用都是实现。

public class MyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        System.out.println("preHandle: " + request.getRequestURI());
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        System.out.println("postHandle: " + request.getRequestURI());
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        System.out.println("afterCompletion: " + request.getRequestURI());
    }
}
  • preHandle:在请求到达处理器之前执行,可以用于权限验证、数据校验等操作。如果返回true,则继续执行后续操作;如果返回false,则中断请求处理。
  • postHandle:在处理器处理请求之后执行,可以用于日志记录、缓存处理等操作。
  • afterCompletion:在视图渲染之后执行,可以用于资源清理等操作。

(2)配置拦截器的拦截规则

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //配置拦截器MyInterceptor
        registry.addInterceptor(new MyInterceptor())
                .addPathPatterns("/**")   // 拦截的路径请求,/**为全部拦截
                .excludePathPatterns("/login", "/register")  //排除的路径请求
                .order(1);             // 拦截器顺序
        
        //配置拦截器UploadFileInterceptor
        registry.addInterceptor(new UploadFileInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/login", "/register")
                .order(2);
    }
}

六、自定义注解与拦截器

我们使用springboot拦截器实现这样一个功能,如果方法上加了@LoginRequired,则提示用户该接口需要登录才能访问,否则不需要登录。 首先定义一个LoginRequired注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
    
}

实现spring的HandlerInterceptor 类先实现拦截器。

public class SourceAccessInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("进入拦截器了");

        // 反射获取方法上的LoginRequred注解
        HandlerMethod handlerMethod = (HandlerMethod)handler;
        LoginRequired loginRequired = handlerMethod.getMethod().getAnnotation(LoginRequired.class);
        if(loginRequired == null){
            return true;
        }

        // 有LoginRequired注解说明需要登录,提示用户登录
        response.setContentType("application/json; charset=utf-8");
        response.getWriter().print("你访问的资源需要登录");
        return false;
    }
}

添加拦截器配置。

@Configuration
public class InterceptorTrainConfigurer implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SourceAccessInterceptor()).addPathPatterns("/**");
    }
}

测试:然后写两个简单的接口,访问sourceA,sourceB资源,给sourceB加上注解。只有sourceB才能请求成功。

@RestController
public class IndexController {

    @GetMapping("/sourceA")
    public String sourceA(){
        return "你正在访问sourceA资源";
    }

    @LoginRequired
    @GetMapping("/sourceB")
    public String sourceB(){
        return "你正在访问sourceB资源";
    }

}

七、spring声明性事务

所谓声明式事务,就是通过配置的方式,比如通过配置文件(xml)或者注解的方式,告诉spring,哪些方法需要spring帮忙管理事务,然后开发者只用关注业务代码,而事务的事情spring自动帮我们控制。

比如注解的方式,只需在方法上面加一个@Transaction注解,那么方法执行之前spring会自动开启一个事务,方法执行完毕之后,会自动提交或者回滚事务,而方法内部没有任何事务相关代码,用起来特别的方法。

@Transaction
public void insert(String userName){
    this.jdbcTemplate.update("insert into t_user (name) values (?)", userName);
}