浅谈spring框架

文章目录

  • spring
    • 理解IOC和DI
    • IOC原理简述
    • 理解AOP
    • AOP原理简述
    • 事务传播行为
    • bean生命周期
    • 循环依赖问题
  • 注解的实现
    • 模拟框架识别注解
      • 模拟IOC加载流程
      • beanDefinition初始化
      • 创建Bean+@value字面量注入
      • @autowired依赖注入
  • springMVC
    • 理解MVC
    • 请求处理流程
    • 拦截器和过滤器
  • springBoot

spring

spring是面向Java的轻量级开发框架,核心模块主要是依赖注入(DI)、控制反转(IOC)和面向切面编程(AOP)

理解IOC和DI

IOC控制反转是一种设计思想。我们通常指的直接控制,是程序员设计类,然后创建对象去使用,对象往往是程序员先new出来再使用,因此这里的控制权在程序员手里。而反转的意思就是将对象创建的任务交给框架去完成,控制权也交给框架
spring框架的控制反转就是通过IOC容器去完成的,用户去设计一个实体类,在使用这个类的实例时,不需要自己去创建,只需要通过配置文件或注解声明需要使用哪个实例,实例创建的工作转移到了IOC容器
DI依赖注入是从另一个角度描述IOC容器,因为一个实例往往具有各种的成员,程序员创建对象时往往需要通过构造方法和set方法去为实例注入依赖,而IOC容器介入后,创建对象的工作以及注入依赖的工作都交给框架去做,我们需要做的仅是通过配置文件(property)或注解(autoWired/resource)去声明即可。

这种控制反转非常利用实例的管理(尤其是大型项目),它最大的特点就是解耦——编码时松散,运行时耦合

没有IOC的时候,程序员通过反射或new关键字创建对象,通常在构造函数中注入依赖,这样最明显的表现就是,源码中的代码耦合度就很高,而采用IOC容器管理对象创建和依赖注入的形式,编码时没有显示的依赖注入语句,我们所要做的就是修改配置文件,而程序运行时,实例创建和依赖注入则依赖框架反射完成。

IOC代替我们管理实例的生命周期,而且默认是单例的,每个实例都能够被很好的复用,程序员可以更好的精力投入到业务代码中,提升效率。

IOC原理简述

IOC本质上是一个map的集合,IOC启动的时候会将配置文件加载为resource实例,用于生成beanDefinition的组件会读取resource并解析配置项,将声明的bean组件封装为一个beanDefinition实例保存在一个map中,这个beanDefinition可以看作bean实例的元信息,其中key是beanName,value是beanDefinition实例,IOC容器为我们返回实例或依赖注入的时候,都是通过读取beanDefinition实例描述的元信息并反射创建一个Object对象。(遍历beanDefinition集合,根据beanDefinition中保存的全限定类名反射创建一个空对象,并作为Object类型返回)

一个Bean在被创建之前,会优先创建存在依赖关系的bean、存在depend-on声明的bean。如果bean声明了工厂方法则会调用工厂方法去创建bean实例。
beanFactory通常注册了一些**后置处理器(BeanPostProcessor)**或一些其他观察者组件,bean创建后会触发这些组件,依赖注入和AOP动态代理都是基于后置处理器实现的(这里用到了观察者设计模式)。(实例创建和依赖注入是独立的过程,其实都是类似的,前者反射创建对象,后者反射调用set方法)
最终创建完毕的bean实例被作为Object类型存放在IOC容器其中的一个map中

默认bean都是单例的,只有第一次getBean的时候才会执行创建和依赖注入的流程,之后都是直接从map中获取,线程不安全,但是一般声明的Bean都是无状态的偏工具类型的bean(某个服务接口、连接池、工具类等)。也可以通过protoType声明多例Bean,这样每次获取时都会返回一个新创建的Bean实例。

单例bean的生命周期由spring IOC容器管理,因为IOC容器总是有一个强引用指向它,而多例bean的生命周期和普通对象一样,由JVM管理,IOC仅为它保存的beanDefinition即bean的元信息。
单例bean的好处:减少创建实例的动作、由spring管理Bean生命周期,减轻GC的回收压力、能够实现单例缓存池的效果。

理解AOP

面向切面编程,切面的主要目的是为了复用和管理代码。将诸如记录日志、事务等与业务关联不是特别密切的系统性、功能性代码专门抽取出来,封装成一个切面。实现业务代码和功能性代码的解耦

AOP原理简述

AspectJ是静态代理,编译期将切面织入java字节码。Spring AOP是动态代理编译后不会修改字节码,每次运行时会临时生成代理对象,代理对象包含目标对象的全部方法,同时在指定的切入点织入切面方法。

静态代理的代理对象是在编译期就存在的,而动态代理的代理对象是运行期生成,但是二者的被代理对象都需要实现接口,而cglib也叫子类代理,从内存中构建出一个子类来扩展目标对象的功能。

AOP基于IOC提供的扩展点实现,IOC启动时AOP模块向beanFactory注册了后置处理器后置处理器就是一个listener,观察到事件发生后将会被回调,会对创建好的bean进行判断,如果bean所在类在切面表达式的切入范围,则会为创建代理对象,最终放入IOC容器的是代理bean。最终bean执行目标方法本质上是在执行invocationHandler实例的invoke方法或者methodInterceptor实例的intercept方法,方法执行时会获取方法上的拦截链递归执行

SpringAOP使用了策略模式,代理工厂Bean组件中同时保存了cglib代理工厂和jdk代理工厂两种策略的实现方式。

JDK动态代理只提供实现了接口的目标类的代理,代理对象将同时实现接口和继承proxy类,proxy持有invocationHandler的引用,最终代理对象将被看作接口类型,方法本质调用invocationHandler实例的invoke方法。

JDK动态代理只提供接口的代理,不支持类的代理。核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起。接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。

如果被代理类没有实现接口,那么springAOP将选择CGLIB动态代理,其基于子类进行代理,因此被代理类不可以是final修饰的,方法调用本质上也是执行methodInterceptor实例的intercept方法。(代理对象继承代理类并重写所有方法,本质上是拦截器调用intercept方法)

Cglib的原理——Enhancer生成一个原有类的子类,并且设置好callBack方法(指定实现了回调方法的接口实现类)则原有类的每个方法调用都会转为调用实现了methodInterceptor接口的代理对象的intercept方法。

使用cglib就是为了弥补jdk动态代理的不足——一定要实现接口

spring中大概用到的设计模式:工厂、单例、观察者、模板方法、代理、适配器、策略等,有机会再展开说

事务传播行为

spring还有一个比较特别的功能,就是设定传播行为。
传播行为大致就是:A是一个开启了事务的调用,而现在A被另一个方法B调用,那么A的事务行为如何在B中传播
大致分为三种类型:
【1】支持当前事务的(有B事务A就加入,否则要么新建,要么不建,要么抛异常)
(默认)required:如果B是一个事务,那么A和B共用一个事务,否则A单独开启一个事务执行。(如果发生错误,则一起回滚)
Supports:如果B是事务那么加入,否则A也作为非事务调用运行
Mandatory:如果B是事务则加入,如果调用事务方法A的方法B没有开始事务就抛异常

【2】不支持当前事务类型的(挂起、异常)
Requires_new:如果B是事务则挂起(二者事务是相互独立的、互不影响,先执行A,即使A回滚了页不影响B),否则A新建一个事务执行。
Never:不允许事务,不以事务允许,如果B是事务,则抛异常。(mandatory则是强制要求B是一个事务,否则抛异常)
not_support。B是事务就挂起,然后A以非事务运行
【3】一些其他类型
(嵌套)nested:如果B是事务则作为其子事务(嵌套事务),如果没有就新建。如果B不是事务方法同required。如果B是一个事务方法就会嵌套事务,即B回滚事务则A一定会回滚,但是A回滚不影响B的主事务和其他子事务

bean生命周期

【1】IOC容器根据beanDefinition的信息反射创建一个对象,并作为object类型存储在IOC容器中。此时这个bean的生命周期由IOC容器管理,因为始终存在一个强引用
【2】依赖注入,IOC容器通过扫描autowired注解或者解析配置,为bean对象基于构造器或set方法反射注入依赖。
【3】bean的初始化。主要是一些扩展点方法。

bean的初始化不等于实例初始化,因为实例初始化在创建对象那一刻就完成了,但是作为一个bean组件,还有很多工作没做,例如触发各种listener的回调方法。

如果实现了aware接口,那么就会执行aware接口中的方法。
如果实现了后置处理器接口就需要实现两个方法,beforeInitialization和afterInitialization。

处理器需要单独作为一个bean注册进IOC调用InitializationBean接口的方法比调用自定义init方法效率高,因为后者需要读取配置,反射获取方法对象,然后再反射执行

<bean class="com.sh.MyPostProcessor" />

两个方法执行中间还包括beanInitialization接口的方法和自定义的init方法。afterInitialization后bean就可以使用了。
IOC容器关闭时,会调用disposableBean接口的destroy方法,然后执行自定义的销毁方法。

为什么初始化前执行aware接口,因为aware接口中的方法提供了applicationContext、beanFactory、beanName等参数,初始化方法可能会使用/获取到这些,因此aware接口中的方法需要提前执行。
aware接口的方法可以用于向IOC容器(applicationContext/beanFactory对应实现类)中注册成员

public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.applicationContext=applicationContext;
}

补充:
Application和beanFactory
二者都是接口(规定了IOC容器的实现规范),application是beanFactory的子接口,同时还是继承了其他接口如资源访问、国际化消息访问。如果使用beanFactory接口,对应容器构造对象时是延迟加载的(第一次getBean才加载),而applicationContext容器构造对象是立即加载的(读取完配置就加载)
从面向对象方面,beanFactory是面向框架开发者的,而applicationContext面向框架使用者

Spring保存的和默认返回的都是单例bean,是线程不安全的,可以通过threadLocal实现线程隔离,或者直接声明多例bean。
多例bean的获取基于原型设计模式,spring在容器中保存一个原型,而每当用户向IOC申请bean时都会返回一个新的对象。
如果申请单例bean则先查询缓存,没有则创建。如果是多例bean直接根据beanDefinition反射创建对象并作为Object类型返回。

循环依赖问题

如果有两个Bean分别是A和B,其中A和B的依赖注入关系为A{B b}和B{A a},则存在循环依赖问题。
多例bean对于循环依赖的策略是抛出异常(否则递归造成栈溢出),默认单例属性set的注入场景支持循环依赖。
“循环依赖”问题的本质是“two sum”
简要描述:
IOC容器维护了一个“未完成Map”,当spring为一个Bean注入依赖时,会首先查询“未完成Map”是否存在所需要注入的类型,如果查到说明存在循环引用,这便是递归返回的核心,直接找到这个循环依赖bean对应的工厂Bean,调用getObject注入一个半成品Bean并返回。
否则将当前正在创建的Bean的引用放入Map中,然后进入递归,为“循环引用成员”去注入依赖,而当进入递归后能够通过“未完成Map”拿到所需类型的Bean,则完成注入工作并退出递归,退出递归后循环依赖的注入也完成了。

spring维护了一个临时的map缓存,存放未完成依赖注入的bean引用,和一个工厂bean的map缓存。当spring为某个bean的依赖(创建并)注入bean时,如果发现未完成bean缓存能够找到对应引用说明存在循环引用,则直接从工厂bean缓存拿到循环依赖bean的工厂bean,通过getObject注入依赖(此时还是半成品,全部返回后就是依赖关系完整的bean)后返回。当bean创建结束后,两个互相依赖的bean的字段中都保存一个完成的bean对象的引用。

注解的实现

spring开发中最常见的就是注解,注解可以看作框架提供给用户的一种特殊接口,用户使用注解对类型、方法、字段等进行标注。框架扫描到注解后,将会通过反射执行某种行为,例如读取参数创建对象。

模拟框架识别注解

我自定义3个注解@value、@autowired、@Qualifier和@component

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Value{
    String value() default "";
}

其中@target和@retention元注解是用于修饰注解的注解,他们由JVM去识别,而我们的注解使用自己的实现去识别。@target指定了我们的@value注解能够修饰到哪里,@Retention指定主键的存活时间,因为我们需要在运行时反射拿到其中的内容,因此这里必须是runtime

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired{}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Qualifer{
    String value() default "";
}
@Target(ElementType.TYPE) 
@Retention(RetentionPolicy.RUNTIME) 
 public @interface Component{
    String value()default "";
}

定义一个实体类,其中省略杂七杂八的方法。其中@component也是自定义的,。其实就是在IOC启动的时候,扫描(拿到)类路径下所有被@component修饰的类的全限定类名,为他们创建对应的beanDefinition对象,然后放入IOC容器中。

@Component
class Family extends DefaultAspect {
    @Value("10.5f")
    private Float money;

    @Autowired
    @Qualifer("myWife")
    private Wife wife;

    public Family(){}
    }

其中Family中,money是字面量类型,因此使用@value修饰,而Wife是引用类型,使用@autowired修饰,并且使用@Qualifer

模拟IOC加载流程

    protected Map<String,Object> ioc = new HashMap<>();// beanName-bean对象
    protected List<String> beanNames =new ArrayList<>();//IOC中所有的bean全限定类名
    protected Set<Class<?>> beanClasses =new HashSet<>();//bean类型集合
    protected Set<BeanDefinition> beanDefinitions;//beanDefinition集合
    private String root;//从root包及其子包的@component修饰类型扫描进IOC容器

这里我们简单模拟IOC容器,但是不模拟过多细节,重点涂成几个注解功能的实现

    public MyAnnotationConfigApplicationContext(String pack){
        //根路径的包名
        this.root=pack;
        //扫描类,封装成beanDefinition对象(bean的描述信息)
        this.beanDefinitions = findBeanDefinitions(this.root);
        //创建bean
        createBean(this.beanDefinitions);
        //装载
        autowireBean(this.beanDefinitions);
    }

以上在构造方法中定义了流程。其中主要为几个部分:
【1】扫描pack包及其子包的类型,并为@component修饰的类型创建beanDefinition对象,并保存进IOC容器。
【2】扫描IOC容器的beanDefinition集合,分别为其创建Object对象,并且通过反射进行进行字面量(@value)赋值
【3】扫描autowired注解,流程类似【2】,源码中两个注解的赋值时机不太清楚,这里为了防止代码过长,分开写了。

beanDefinition初始化

beanDefinition存放Bean的元信息,这里简单定位为:bean的类型bean的全限定类名

public class BeanDefinition{
    private String beanName;
    private Class beanClass;
}

获取包下所有的类型,将所有标记component注解的bean创建beanDefinition对象,初始化beanDefinition集合

    private Set<BeanDefinition> findBeanDefinitions(String pack){
        //获取包下所有类
        Set<Class<?>> classSet = Uitls.getClasses(pack);
        Iterator<Class<?>> iterator = classSet.iterator();
        //将类封装为beanDefinition对象
        HashSet<BeanDefinition> beanDefinitions = new HashSet<>();
        while (iterator.hasNext()){
            Class<?> curClass = iterator.next();
            Component componentAnno = curClass.getAnnotation(Component.class);
            if(componentAnno!=null){
                //获取注解中的值,如果有值就将其作为bean,否则使用类名作为默认bean
                String beanName = componentAnno.value();
                if("".equals(beanName)){
                  String className=curClass.getSimpleName();
                  //首字母小写
                  beanName=className.substring(0,1).toLowerCase()+className.substring(1);
                }
                beanDefinitions.add(new BeanDefinition(beanName,curClass));
                beanNames.add(beanName);
                beanClasses.add(curClass);
            }
        }
        return beanDefinitions;
    }

主要:其实识别注解就是通过反射注解拿到注解,如果为null说明没有被修饰,否则就进行相应逻辑的处理

Component componentAnno = curClass.getAnnotation(Component.class);

不过根据不同的注解,可能修饰的地方也不一样,一般框架在拿到所有声明的属性(如依赖注入、表字段映射等)、类型(如创建Bean、表映射等)、方法(如加事务、AOP等)等,都会进行相应的注解识别。

创建Bean+@value字面量注入

扫描beanDefinition集合,反射创建bean对象(空对象),读取value注解(字面量)进行属性注入

    private void createBean(Set<BeanDefinition> beanDefinitions) {
        Iterator<BeanDefinition> iterator = beanDefinitions.iterator();
        while (iterator.hasNext()){
            BeanDefinition curBeanDefinition = iterator.next();
            Class beanClass = curBeanDefinition.getBeanClass();
            String beanName = curBeanDefinition.getBeanName();
            //反射创建对象
            try {
                Object bean = beanClass.getConstructor().newInstance();//空对象
                //属性赋值
                Field[] fields = beanClass.getDeclaredFields();
                for(Field field:fields){
                    //读取value注解
                    Value valueAnno = field.getAnnotation(Value.class);
                    if(valueAnno!=null){
                        String value = valueAnno.value();//value注解的值
                        String fieldName =field.getName();//变量名
                        //生成方法名
                        String methodName = "set"+fieldName.substring(0,1).toUpperCase()+fieldName.substring(1);
                        Method method = beanClass.getMethod(methodName, field.getType());//反射获取set方法
                        Object val = null;//value经过类型转换后为val
                        //注解值value的类型转换
                        switch (field.getType().getName()){  //这里使用包装类
                            case "java.lang.Integer":
                                val=Integer.parseInt(value);
                                break;
                            case "java.lang.String":
                                val=value;
                                break;
                            case "java.lang.Float":
                                val=Float.parseFloat(value);
                                break;
                        }
                        //反射执行set方法,注入属性(字面量)
                        method.invoke(bean,val);
                    }
                }
                //存入缓存
                ioc.put(beanName,bean);
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                e.printStackTrace();
            }
        }

    }

其中最核心的逻辑:反射创建一个(空参)对象,通过class拿到该对象的所有属性,遍历每一个属性,反射读取@value的值,并且拿到@value修饰的属性名称,字符串拼接出set方法,并根据method+方法参数列表类型定位出当前class的某个method实例,填入对象和参数值(需要转换为相应方法参数类型),反射调用方法,最终对象@value修饰的属性就注入完毕了,将这个bean对象加入IOC容器

Object bean = beanClass.getConstructor().newInstance();
Field[] fields = beanClass.getDeclaredFields();
Value valueAnno = field.getAnnotation(Value.class);
Method method = beanClass.getMethod(methodName, field.getType());
method.invoke(bean,val);
ioc.put(beanName,bean);

@autowired依赖注入

扫描beanDefinition集合,将所有标记了autowired注解(引用类型)的字段,注入bean实例

    private void autowireBean(Set<BeanDefinition> beanDefinitions) {
        Iterator<BeanDefinition> iterator = beanDefinitions.iterator();
        while (iterator.hasNext()){
            BeanDefinition beanDefinition = iterator.next();
            Class beanClass = beanDefinition.getBeanClass();
            Field[] fields = beanClass.getDeclaredFields();
            for(Field field:fields){
                Autowired autowiredAnno = field.getAnnotation(Autowired.class);
                if(autowiredAnno!=null){
                    //标记了autowired注解,说明该字段需要注入bean
                    Qualifer qualiferAnno = field.getAnnotation(Qualifer.class);
                    //如果标记了qualifier注解,就通过“标志”注入bean。否则类型注入
                    if(qualiferAnno!=null){
                        String qualiferValue = qualiferAnno.value();
                        //从IOC中获取bean,这个bean是需要被注入的bean
                        Object autowiredBean = getBean(qualiferValue);
                        //通过该字段的set方法注入
                        String fieldName = field.getName();
                        String methodName ="set"+fieldName.substring(0,1).toUpperCase()+fieldName.substring(1);
                        try {
                            Method method = beanClass.getMethod(methodName,field.getType());
                            //要执行set方法的bean
                            Object targetBean = getBean(beanDefinition.getBeanName());
                            //反射执行set方法
                            method.invoke(targetBean,autowiredBean);
                        } catch (NoSuchMethodException e) {
                            System.err.println("没有这个set方法:"+methodName);
                            e.printStackTrace();
                        } catch (IllegalAccessException | InvocationTargetException e) {
                            e.printStackTrace();
                        }
                    }else {
                        //按类型注入,这里暂时不实现
                        System.out.println("按类型注入:"+field.getType());
                    }

                }
            }
        }

    }

和@value注入的逻辑很像,只不过这次注入的不是字面量,而是bean实例(Object对象),而且这次扫描字段数组时,同时对两个注解进行判断。之后的构造set方法也如出一辙,只不过这次set的类型是bean相应的类型(最终按照Object注入,但是识别set方法时是按照元类型识别的,其实就是field的类型)

通过@autoWired注解默认注入单例,如果想要注入多例,需要为声明的bean类型上面加上@scope注解,并且指定为protoType。
@autowired默认按照类型注入,搭配@qualifier可以按照名称注入。@resource默认按照名称,找不到时按照类型

其实,本质上还是那一套:框架在迭代到某个类型、方法、字段的时候会判断一下是否存在某个注解,如果存在则执行某些行为
一般都是反射拿到注解类型的实例(注解本质也是一个接口类型),这个实例主要保存了两个信息:注解的值被修饰一方的信息。之后就是通过各种反射操作到达相应注解的目的(真正做事情的还是识别注解的框架本身)

springMVC

springMVC是基于MVC设计思想的一种处理web前后端交互的框架,最常用的就是使用@requestMapping注解处理请求的映射。SpringMVC是一个spring基于MVC设计思想实现的开发框架,可以简化web的开发。

理解MVC

模型-视图-控制器。其中模型是关键,控制器和视图可以看作模型的输入与输出。对需要解决的问题建立模型,通过模型传递数据
控制器一般不写任何业务逻辑,业务写在service层。controller要做的就是接收请求,创建模型,通过业务层接口拿到输出结果,通过模型将业务封装起来,并将模型输出到前端,并渲染到视图上。

请求处理流程

前端控制器DispatcherServlet会拦截客户端的请求,然后遍历在handlerMapping(处理器映射器)数组,找到可以处理请求的handler(是一个处理链对象),由于handler可以多种多样(如原生servlet、实现controller接口的、实现HTTPRequestHandler接口的),因此每一种handler都有一个对应的适配器,dispatcherServlet会遍历adapters数组找到可以处理当前handler的适配器,通过适配器反射执行handler的对应方法得到一个modelAndView对象视图解析器viewResolver会解析modelAndView对象得到view对象,最终使用view的render方法传入model进行渲染,对response对象进行填充。

拦截器和过滤器

Filter会注册在applicationFilterChain的数组中,当所有filter执行完毕(doFilter全部放行)后才会执行servlet,基于责任链模式,基于函数回调。Filter接口是servlet规范下的接口,因此filter需要依赖servlet容器,导致它只能在web程序中使用

而spring的拦截器基于动态代理,属于spring的一个组件,并不局限于web程序。

Interceptor的执行时机:请求进入servlet后,进入controller前,当view的render方法返回后处理完毕。
Filter的执行时机:请求进入servlet前进行拦截,全部放行后才允许servlet执行。

Filter对所有进入servlet容器的请求进行拦截,包括静态资源。而拦截器只会拦截进入controller的请求。

springBoot

自动装配原理简述:

@SpringBootConfiguration
@EnableAutoConfiguration

Springboot的主配置类标注了springBootApplication注解,其主要由两个作用。
一个是标注这是一个应用的中心配置类,框架识别该注解后将其作为一个配置组件放入IOC容器。另一个是启动自动配置功能,框架将springBootApplication及其子包下的组件扫描进入IOC容器(元信息存入beanDefinition容器),同时还会扫描META-INF/spring.factories文件,并将其中每一项都加载进一个properties对象,其中key是自动配置(enableAutoConfiguration)类的全限定类名。最后将properties的值存入IOC容器。

【1】springBoot启动时,通过**@enableAutoConfiguration注解找到jar包中spring.factories配置文件中所有的自动配置类**,并对其加载。因此springBoot启动时,会加载大量自动配置类。
【2】自动配置类以autoConfiguration结尾来命名,XXXautoConfiguration配置类中通过EnableConfigurationProperties注解取得XXXProperties类在全局配置文件中配置的属性。
【3】XXXProperties类通过@configurationProperties注解与全局配置文件中对应的属性绑定。给容器中自动配置类添加组件的时候,会从XXXproperties类中获取某些属性。我们就可以在配置文件中指定这些属性的值

东西好不少,以后再补充

你可能感兴趣的:(java基础,spring,java,面试,ioc,aop)