Home Spring学习

0 81

转载自知乎bravo1988

Spring基础(2):放弃XML,走向注解 – 知乎 (zhihu.com)

上一篇并没有实际地带大家去看源码,而是介绍了两个概念:

  • BeanDefinition
  • BeanPostProcessor

当然,我介绍得非常笼统,不论是BeanDefinition还是BeanPostProcessor其实都有着较为复杂的继承体系,种类也很多。作为Spring系列第一篇,主要目的还是帮大家摆脱对Spring的刻板认知,刷新你们的三观,毕竟太多人对Spring的理解仅限于所谓的IOC和AOP。现在Spring5都出来了,好多人还停留在Spring2.5、Spring3的年代,还在使用XML。

今天我将会带大家复习Spring的基础,大致流程是:

  • 复习XML方式开发
  • 通过逐步暴露XML的弊端,引出Spring注解
  • 最终完全舍弃XML,采用Spring注解开发

之所以推荐注解开发,原因有两点:

  • XML配置太繁琐了
  • 掌握Spring注解开发有助于后期学习SpringBoot

只有熟练使用Spring后,看源码时才能把应用和原理联系起来。

文章篇幅较长,建议看的时候先把下方目录截图,放在一旁做引导,防止自己看着看着不知道看到哪了。

主要内容:

  • IOC与DI
  • Spring的3种编程风格与2种注入方式
  • 1️⃣XML配置开发:<bean>描述依赖关系
  • 自动装配:让<bean>职责单一化
  • 2️⃣XML+注解:XML+<context:component-scan>+@Component
  • @Autowired的小秘密
  • 2️⃣JavaConfig+注解:@Configuration+@ComponentScan+@Component
  • 3️⃣JavaConfig方式:@Configuration+@Bean
  • 大乱斗:@ImportResource、@Component、@Bean

IOC与DI

关于IOC的好处,推荐一篇文章,个人觉得写得很好:Spring IoC有什么好处呢?

大家不妨将IOC理解成一种思想,而DI是实现该思想的一种具体方式。Spring被称为IOC容器,它实现IOC的方式除了DI(Dependency Inject,依赖注入),其实还有DL(Dependency Look,依赖查找)。由于我们平时很少用到DL,所以这里只讨论DI(依赖注入)。

IOC与DI

Spring依赖注入的做法

首先,提供一些配置信息(比如XML)来描述类与类之间的关系,然后由IOC容器(Spring Context)去解析这些配置信息,继而维护好对象之间的关系。

<!-- 配置信息:在XML中定义Bean -->
<bean id="person" class="com.bravo.annotation.Person">
    <property name="car" ref="car"></property>
</bean>


<bean id="car" class="com.bravo.annotation.Car"></bean>

其次,还有一个很重要的前提是,除了配置信息,对象之间也要体现依赖关系。

public class Person {
    // Person类中声明了Car,表示Person依赖Car
    private Car car;
    // 由于上面XML使用了<property>标签,表示setter方法注入,所以必须提供setter方法
    public void setCar(Car car) {
        this.car = car;
    }
}

总结起来就是:

  • 编写配置信息描述类与类之间的关系(XML/注解/Configuration配置类均可)
  • 对象之间的依赖关系必须在类中定义好(一般是把依赖的对象作为成员变量)
  • Spring会按照配置信息的指示,通过构造方法或者setter方法完成依赖注入
XML中bean标签的职责:1.定义bean 2.维护bean依赖关系,指导Spring完成依赖注入

Spring的3种编程风格与2种注入方式

按照Spring官方文档的说法,Spring的容器配置方式可以分为3种:

  • Schema-based Container Configuration(XML配置)
  • Annotation-based Container Configuration(注解)
  • Java-based Container Configuration(@Configuration配置类)

Spring支持的2种注入方式:

  • 构造方法注入
  • setter方法注入

在Spring4之前,Spring还支持接口注入(很少用),这里不提及。

(这个分类还是有问题,后面分析源码时再解释)

大家必须要明确,所谓3种编程风格和2种注入方式到底指什么,之间又有什么联系?

我们从2种注入方式开始分析。

Q:Spring注入的是什么?
A:是Bean。
Q:这些Bean怎么来的?
A:IOC容器里的。

所以,所谓的3种编程风格其实指的是“将Bean交给Spring管理的3种方式”,可以理解为IOC,而2种注入方式即DI,是建立在IOC的基础上的。也就是说Spring的DI(依赖注入)其实是以IOC容器为前提。

3种编程风格其实指的是3种把Bean交给Spring管理的方式,而DI有2种方式:setter方法注入/构造方法注入

接下来,我们把3种编程风格分别用代码实验一下。

Spring系列文章我都会贴出完整、可运行的代码,所以建议大家一边看一边复制到本地调试,这样学得更快。


1️⃣XML配置开发:<bean>描述依赖关系

setter方法注入

pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.12.RELEASE</version>
        </dependency>
    </dependencies>

配置信息(setter方法注入)

<?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
       http://www.springframework.org/schema/beans/spring-beans.xsd">


    <!-- 在xml中描述类与类的配置信息 -->
    <bean id="person" class="com.bravo.xml.Person">
        <!-- property标签表示,让Spring通过setter方法注入-->
        <property name="car" ref="car"></property>
    </bean>


    <bean id="car" class="com.bravo.xml.Car"></bean>


</bean

Person(这里偷懒,把后面要讲的构造器注入的准备工作也做了,对运行结果不影响)

public class Person {


    // Person依赖Car
    private Car car;


    // 无参构造
    public Person(){}


    // 有参构造
    public Person(Car car){
        this.car = car;
        System.out.println("通过构造方法注入...");
    }


    // setter方法
    public void setCar(Car car) {
        this.car = car;
        System.out.println("通过setter方法注入...");
    }


    @Override
    public String toString() {
        return "Person{" +
                "car=" + car +
                '}';
}

Car

public class Car {
}

Test

public class Test {
    public static void main(String[] args) {
        // 由于是XML配置方式,对应的Spring容器是ClassPathXmlApplicationContext,传入配置文件告知Spring去哪读取配置信息
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-context.xml");
        // 从容器中获取Person
        Person person = (Person) applicationContext.getBean("person");
        System.out.println(person);
    }
}

目录结构

测试结果

由于XML中配置依赖信息时,使用了property标签,所以Spring会调用setter方法注入

构造方法注入

接下来,我们试一下构造方法注入:

<?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
       http://www.springframework.org/schema/beans/spring-beans.xsd">


    <!-- 在xml中描述类与类的配置信息 -->
    <bean id="person" class="com.bravo.xml.Person">
        <!-- constructor-arg标签表示,让Spring通过构造方法注入-->
        <constructor-arg ref="car"></constructor-arg>
    </bean>
    <bean id="car" class="com.bravo.xml.Car"></bean>
    
</beans>

测试结果

其他代码都没变,仅仅改变配置信息,由property标签变为constructor-arg标签,Spring就知道要改为构造器注入了

至此,我们把XML配置下2种注入方式都实验过了,它们的区别是:

  • XML配置<property> + 对象提供对应的setter方法
  • XML配置<constructor-arg> + 对象提供对应的构造方法

改变XML配置的同时,需要对象提供对应的方法支持。如果你用了<property>,却没有在类中提供setter方法,则会报错。


自动装配:让<bean>职责单一化

我们会发现<bean>这个标签,其实承载着两个作用:

  • 定义bean,告诉Spring哪个Bean需要交给它管理(放入容器)
  • 维护bean与bean之间的依赖关系

接下来我们思考这样一个问题:

对于Person类

public class Person {
    // Person依赖Car
    private Car car;


    public void setCar(Car car) {
        this.car = car;
    }
}

上面代码其实已经很好地描述了Person和Car的依赖关系,此时在XML中继续用<property>或者<constructor-arg>反而成了累赘:

  • 既然类结构本身包含了依赖信息,<bean>再用<property>等去描述就显得多余了
  • 如果类结构变动,我们还需要额外维护<bean>的依赖信息,很麻烦。比如Person新增了一个shoes字段,那么<bean>又要写一个<property>表示shoes

所以,最好的做法是把让<bean>标签职责单一化,让它只负责定义bean,把bean与bean的依赖关系转交给类自身维护(有这个字段就说明有依赖)。

既然菜鸡的我们能想到,那么Spring肯定也想到了,于是它提出了“自动装配”的概念。很多人一听到自动装配,脑子里只有@Autowired。不算错,但其实XML也支持自动装配,而且真要论先来后到的话,肯定还是XML的自动装配在前。

XML实现自动装配可以分为两种:全局、局部。

全局自动装配(XML根标签<beans>末尾加default-autowire配置)

<?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
       http://www.springframework.org/schema/beans/spring-beans.xsd"
        default-autowire="byName">

    <!-- 在xml中只定义bean,无需配置依赖关系 -->
    <bean id="person" class="com.bravo.xml.Person"></bean>
    <bean id="car" class="com.bravo.xml.Car"></bean>
  
</beans>

所谓全局,就是在XML根标签末尾再加一个配置default-autowire=”byName”,那么在此XML中配置的每一个<bean>都遵守这个自动装配模式,可选值有4个:

  • byName
  • byType
  • constructor
  • no
default其实就是no

测试结果

我们会发现改用自动装配后,虽然没有了property标签,但是默认是调用setter方法

局部自动装配(每一个<bean>单独设置autowire)

<?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
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 在xml中只定义bean,无需配置依赖关系 -->
    <bean id="person" class="com.bravo.xml.Person" autowire="byName"></bean>
    <bean id="car" class="com.bravo.xml.Car"></bean>

</bean

测试结果

小结:

  • Spring支持自动装配(全局/局部),把原先<bean>标签的职责单一化,只定义bean,而依赖关系交给类本身维护
  • 自动装配共4种,除了no,其他3种各自对应两种注入方式:byName/byType对应setter方法注入,constructor对应构造方法注入 (请自己动手证明)

2️⃣XML+注解:XML+<context:component-scan>+@Component

原本<bean>标签有两个职责:

  • 定义bean
  • 描述依赖信息

上面通过自动装配,把依赖信息交给类本身维护,从此<bean>只负责bean定义。

现在,我们想想办法,能不能干脆把bean定义也剥离出来?这样就不需要在XML中写任何<bean>标签了。我早就看<bean>标签不爽了,这么一大坨,要是bean多了,就很臃肿。

怎么做呢?

我们先来回顾一下手上的牌面:

至此,我们已经成功调教Spring帮我们做了自动装配,也就是说IOC和DI中,DI已经实现自动化。我们接下来要考虑的是如何减少IOC配置的工作量。

原先是把<bean>写在XML中,再把XML喂给Spring:

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-context.xml");

既然现在打算消灭XML中的<bean>,则说明即使把XML喂给Spring,它也吃不到bean定义了。所以,必须要告诉Spring去哪可以吃到bean。

我们来看一下,当Spring吃下<bean>时,到底吃了什么:

<!-- 在xml中只定义bean,无需配置依赖关系 -->
<bean id="person" class="com.bravo.xml.Person" autowire="byName"></bean>
<bean id="car" class="com.bravo.xml.Car"></bean>

是的,<bean>只指定了类名和自动装配的模式。也就是说,要定义一个bean,只需要最基本的两样东西:

  • 类名
  • 装配模式(其实这个也不是必须的,默认no,不自动装配)

类名其实很好得到,我们自己写的类不就有吗?至于自动装配的模式,也完全可以在类中通过注解指定。于是,我们找到了改造的方向:用带注解的类代替<bean>标签。

之前:XML中写好bean标签后,把XML喂给Spring,Spring就会把bean实例化加到容器
现在:消灭bean标签后,XML中已经没有bean,Spring必须自己去找bean定义

Spring2.5开始提供了一系列注解,比如@Component、@Service等,这些注解都是用来表示bean的。而@Service等注解底层其实还是@Component:

之所以做一层封装,是为了赋予它特殊的语义:定义Service层的bean。其余的这里不再赘述。总之我们暂时理解为,如果要使用注解表示bean定义,我们能用的只有@Component。

新建annotation包,把Car和Person移过去:

Person

@Component //带注解的类,我们希望用这种方式定义bean,并让Spring把它吃进去
public class Person {


    // Person依赖Car
    private Car car;


    @Override
    public String toString() {
        return "Person{" +
                "car=" + car +
                '}';
    }
}

Car

@Component
public class Car {
}

XML(什么都没有配置,连自动装配模式也没指定,因为不在这里定义bean了)

<?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
       http://www.springframework.org/schema/beans/spring-beans.xsd">


</beans>

Test(不变)

public class Test {
    public static void main(String[] args) {
        // 由于是XML配置方式,对应的Spring容器是ClassPathXmlApplicationContext,传入配置文件告知Spring去哪读取配置信息
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-context.xml");
        // 从容器中获取Person
        Person person = (Person) applicationContext.getBean("person");
        System.out.println(person);
    }
}

测试结果

在Spring容器中找不到person

其实很好理解,我们传入了spring-context.xml告诉Spring去哪读取bean定义,但是实际上XML却没有配置任何<bean>,它是不可能把类实例化加入到容器的。

然而我们新定义的bean(@Component)Spring也没吃,怎么回事?

其实主要是因为我们的改变太突然了,Spring以前吃惯了XML中的<bean>,现在突然换成@Component这种注解类,它吃不惯,甚至不知道它能吃!

所以,必须通知Spring:

老哥,我们改用注解了,有@Component注解的类就是bean,和以前<bean>一样一样的。

如何通知?只要在XML中配置:

 <context:component-scan base-package="com.bravo.annotation"/>

官方文档对这个标签的解释是:

The use of <context:component-scan> implicitly enables the functionality of <context:annotation-config>. There is usually no need to include the <context:annotation-config> element when using <context:component-scan>.

翻译过来就是:

使用<context:component-scan>隐式地启用了<context:annotation-config>的功能。<context:annotation-config>的作用是让Spring具备解析@Component等注解的功能。当使用<context:component-scan>时,通常不需要包含<context:annotation-config>元素。

这个标签的作用相当于什么呢?Spring一口吃下去,发现没有吃到<bean>,却吃出了一张小纸条,上面写着:赶紧去找标了@Component注解的类,那是新菜式!

所以,最终</context:component-scan>标签的作用有两个:

  • 扫描:原先我们把写有bean定义的XML文件喂给Spring,现在则让Spring自己去指定路径下扫描bean定义
  • 解析:让Spring具备解析注解的功能

所以,XML虽然不用配置<bean>标签,却要配置扫描(需要配置额外的名称空间):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-3.0.xsd">


    <context:component-scan base-package="com.bravo.annotation"/>


</beans

测试结果:

虽然能找到Person了,但是Car并没有被注入

又出幺蛾子了,怎么回事呢?我们回想一下XML的bean定义:

<!-- 在xml中只定义bean,无需配置依赖关系 -->
<bean id="person" class="com.bravo.xml.Person" autowire="byName"></bean>
<bean id="car" class="com.bravo.xml.Car"></bean>

我们设置了autowire属性,告诉Spring按什么方式自动装配。

现在我们改用注解了,@Component只是相当于<bean>标签,却没有指明自动装配的模式。如何在类中告诉Spring我们需要的装配方式呢?

方法有很多种:

  • @Autowired(Spring提供的)
  • @Resource(JSR-250定义)
  • @Inject(JSR-330定义)

这里我们以@Autowired为例:

@Component
public class Person {


    // 用@Autowired告知Spring:请把Car装配进来
    @Autowired
    private Car car;


    @Override
    public String toString() {
        return "Person{" +
                "car=" + car +
                '}';
    }
}

测试结果

自动装配成功!

@Autowired的小秘密

上面我们有惊无险地从用@Component替换了<bean>,并且结识了@Autowired这个超棒的注解,用来完成自动装配。即:

  • <context:component-scan>+@Component彻底解放IOC配置
  • @Autowired完成自动装配

但是细心的小伙伴会发现,相较于<bean>中的autowire=”byName”,@Autowired虽然装配成功了,却没有显式地指定自动装配的模式。

只有一种解释:它有默认的装配方式。

在探究@Autowire默认的装配模式之前,关于bean的名称,要和大家先交代一下:

<!-- 在xml中只定义bean,无需配置依赖关系 -->
<bean id="person" class="com.bravo.xml.Person" autowire="byName"></bean>
<bean id="car" class="com.bravo.xml.Car"></bean>

在<bean>中,id即为最终bean在Spring容器的名字。

同样的,@Component也提供了给bean命名的方法:

@Component("bravo")
public class Person {


    // 用@Autowired告知Spring:请把Car装配进来
    @Autowired
    private Car car;


    @Override
    public String toString() {
        return "Person{" +
                "car=" + car +
                '}';
    }
}

如果不指定,则默认会把类名首字母小写后作为beanName。

铺垫结束,我们开始探究@Autowired到底默认是哪种装配模式:

  • byName
  • byType
  • constructor
  • no(已经装配成功,排除)

先来看看是不是byName

@Component
public class Person {


    // 用@Autowired告知Spring:请把Car装配进来
    @Autowired
    private Car myCar;


    @Override
    public String toString() {
        return "Person{" +
                "car=" + myCar +
                '}';
    }
}

测试结果

Car在Spring中bean的名字应该是car,而我把Person中的Car变量名改为myCar,仍旧注入成功,说明不是byName。

再来看看是不是byType。

这个稍微有点麻烦,因为我需要弄出至少两个同类型的bean。所以我打算把Car变成接口,然后创建Bmw和Benz两个实现类。这个接口只是为了试验,没有实际意义:

Car

//接口
public interface Car {
}


//实现类Bmw
@Component
public class Bmw implements Car {
}


//实现类Benz
@Component
public class Benz implements Car {
}

Person

@Component
public class Person {


    // 用@Autowired告知Spring:请把Car装配进来
    @Autowired
    private Car car;


    @Override
    public String toString() {
        return "Person{" +
                "car=" + car +
                '}';
    }
}

测试结果

熟悉的配方、熟悉的味道:expected single matching bean but found 2: BMW,benz

很明显,@Autowired默认采用byType的方式注入,由于当前Spring容器中存在两个Car类型的bean,所以注入时报错了,因为Spring无法替我们决定注入哪一个。

但是,有个神奇的现象是,你如果把变量名改为bmw或者benz,就会注入对应的bean:

@Component
public class Person {

    // 把变量名改为bmw
    @Autowired
    private Car bmw;

    @Override
    public String toString() {
        return "Person{" +
                "car=" + bmw +
                '}';
    }
}

也就是说,@Autowired默认采用byType模式自动装配,如果找到多个同类型的,会根据名字匹配。都不匹配,则会报错。

当然,有些人可能有强迫症,觉得我Car类型的变量必须叫car,但又想指定注入bmw,怎么办?我们先看看@Autowired能不能指定名字吧:

不能指定名字,因为Autowired只有一个属性:required,表示当前bean是否必须被注入

为了弥补@Autowired不能指定名字的缺憾,Spring提供了@Qualifier注解

@Qualifier("benz")
@Autowired
private Car car;

即使Spring容器中有两个Car类型的bean,也只会按名字注入benz。

其他的我就不测了,给个结论就好:

  • @Autowired:默认byType,type相同则byName
  • @Resource:和@Autowired几乎一样,但不能配合@Qualifier,因为它本身就可以指定beanName。但没有required属性
@Resource(name = "benz")
private Car car;
  • @Inject:用的很少,不做讨论

2️⃣JavaConfig+注解:@Configuration+@ComponentScan+@Component

有没有发现,上面标题还是2️⃣?因为接下来要介绍的,还是注解开发。

先复习一下前面两种方式:

  • 纯XML(<bean>负责定义bean,Java类负责定义依赖,Spring完成自动装配)
<!-- 在xml中只定义bean,无需配置依赖关系 -->
<bean id="person" class="com.bravo.xml.Person" autowire="byName"></bean>
<bean id="car" class="com.bravo.xml.Car"></bean>
  • 注解+XML(@Component+@Autowired,但我们发现注解并不能单独使用,必须要XML中配置开启注解扫描才能生效)
 <context:component-scan base-package="com.bravo.annotation"/>

之前我在注解(上)讲过,注解的使用必须包含三步:定义注解、使用注解、解析注解。@Component是Spring定义、我们使用,也肯定是由Spring解析。但是这个解析必须由我们手动开启。这就是<context:component-scan>标签的意义。

到了这一步我们已经把<bean>标签完全消灭了。但是这种模式有点不伦不类。

你说它叫XML配置开发吧,它又有@Component注解。你说它是注解开发吧,XML中还有一个<context:component-scan>在那嘚瑟呢。所以如何才能完全消灭XML呢?

究其根本,我们发现无法消灭XML的原因在于:注解的读取和解析必须依赖于<context:component-scan>标签!因为我们要帮Spring开启注解扫描,不然他不知道去哪读取bean。

既然<bean>标签可以被@Component代替,那么<context:component-scan>标签应该也能找到对应的注解。

不错!这个注解就是@ComponentScan!如此一来我们就再也不需要spring-context.xml了。

但是转念一想,脊背发凉…ClassPathXmlApplicationContext这个类要求我们必须传一个XML,怎么办?别担心,Spring同样提供了一个注解@Configuration,目的是让我们可以把一个普通的Java类等同于一个XML文件,而这个Java类就是JavaConfig,我们习惯称之为配置类。

新建一个javaconfig包,把annotation包下的所有类移过来,并且新建AppConfig配置类

@Configuration //表示这个Java类充当XML配置文件
@ComponentScan(basePackages = "com.bravo.javaconfig") //相当于XML中的<context:component-scan>标签
public class AppConfig {


}

这样,我们就可以把XML删除,用@ComponentScan来开启注解扫描。

目录结构

准备测试时,发现了大麻烦:

ClassPathXmlApplicationContext无法接受AppConfig配置类,它只认XML

所以,用AppConfig配置类替代XML只是我们的一厢情愿吗?

其实是我们选错了实现类。ApplicationContext的子类除了ClassPathXmlApplicationContext,还有一个专门针对注解开发的:AnnotationConfigApplicationContext。

新的Test

public class Test {
    public static void main(String[] args) {
        // AnnotationConfigApplicationContext是Spring用来专门针对注解开发的ApplicationContext子类
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        // 从容器中获取Person
        Person person = (Person) applicationContext.getBean("person");
        System.out.println(person);
    }
}

测试结果

稳得一批

至此,XML已经被我们完全消灭了。


3️⃣JavaConfig方式:@Configuration+@Bean

严格来说,上面的做法并不是所谓的Java-based Container Configuration(@Configuration配置类)风格。我们虽然用到了@Configuration,但只是为了让Java配置类替代XML,最终消灭XML。这也太大材小用了…本质上,这还是@Component+@Autowired注解开发,只是开启注解扫描的方式从<context:component-scan>标签变为@ComponentScan。

实际上,真正的Java-based Container Configuration编程风格是这样的:

AppConfig(如果你不扫描@Component,则不需要@ComponentScan)

@Configuration
public class AppConfig {

    //new一个Benz对象,通过@Bean注解告知Spring把这个bean加到容器
    @Bean
    public Car benz(){
       return new Benz();
    }
    
    //new一个Bmw对象,通过@Bean注解告知Spring把这个bean加到容器
    @Bean
    public Car bmw(){
        return new Bmw();
    }
    
    //new一个Person对象,通过@Bean注解告知Spring把这个bean加到容器
    @Bean
    public Person person(){
        Person p = new Person();
        p.setCar(new Benz());
        return p;
    }

}

Benz(去除@Component,那是注解开发方式)

public class Benz implements Car {
}

Bmw(去除@Component,那是注解开发方式)

public class Bmw implements Car {
}

Person(去除@Component,那是注解开发方式)

public class Person {

    private Car car;

    // setter方法。在@Bean场景下,手动调用setter方法设置成员变量
    public void setCar(Car car) {
        this.car = car;
    }
 
    @Override
    public String toString() {
        return "Person{" +
                "car=" + car +
                '}';
    }
}

测试结果

小结

Java-based Container Configuration编程风格指的是:

  • 用@Configuration把一个普通Java类变成配置类,充当XML
  • 在配置类中写多个方法,加上@Bean把返回值对象加到Spring容器中
  • 把配置类AppConfig喂给AnnotationConfigApplicationContext,让它像解析XML一样解析配置类
  • 无需加@Component注解,因为我们可以手动new之后通过@Bean加入容器

大乱斗:@ImportResource、@Component、@Bean

其实XML、注解、JavaConfig三种方式相互兼容,并不冲突。

  • XML的<bean>
  • @Component注解和扫描(不论是<context:component-scan>还是@ComponentScan)
  • @Configuration与@Bean

为了证实它们确实不冲突,我搞了很变态的,一个项目里三种编程方式混用:

  • 两辆车子,bmw和benz交给@Bean(JavaConfig)
  • Person交给@Component和@ComponentScan(注解)
  • Student交给XML和@ImportResource(XML)

目录结构

AppConfig

@Configuration //JavaConfig方式,把当前Java类作为配置类
@ComponentScan(basePackages = "com.bravo.all")//注解方式,开启扫描
@ImportResource("spring-context.xml")//XML方式,导入bean定义
public class AppConfig {


    @Bean
    public Car benz(){
       return new Benz();
    }


    @Bean
    public Car bmw(){
        return new Bmw();
    }

}

Car

public interface Car {
}

Benz(JavaConfig方式:@Bean加入Spring)

public class Benz implements Car {
}

Bmw(JavaConfig方式:@Bean加入Spring)

public class Bmw implements Car {
}

Person(注解方式:@ComponentScan扫描@Component加入Spring)

@Component
public class Person {


    // 用@Autowired告知Spring:请把Car装配进来
    @Qualifier("benz")
    @Autowired
    private Car car;


    @Override
    public String toString() {
        return "Person{" +
                "car=" + car +
                '}';
    }
}

Student(XML方式:使用<bean>定义)

public class Student {
    private Car bmw;


    //由于在下方XML配置中,我选用了byName自动装配,而byName/byType都要提供setter方法
    public void setBmw(Car bmw) {
        this.bmw = bmw;
    }


    @Override
    public String toString() {
        return "Student{" +
                "car=" + bmw +
                '}';
    }
}

spring-context.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
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 在xml中描述类与类的配置信息 -->
    <bean id="student" class="com.bravo.all.Student" autowire="byName">
    </bean>

</beans>

Test

public class Test {
    public static void main(String[] args) {
        // AnnotationConfigApplicationContext是Spring用来专门针对注解开发的ApplicationContext子类
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        // 从容器中获取Person
        Person person = (Person) applicationContext.getBean("person");
        System.out.println(person);
        // 从容器中获取Student
        Student student = (Student) applicationContext.getBean("student");
        System.out.println(student);
    }
}

测试结果

通常来说,我们日常开发一般是注解+JavaConfig混用。也就是

  • @ComponentScan+@Configuration+@Component+@Bean

Spring基础(2):放弃XML,走向注解 – 知乎 (zhihu.com)

之前接触过加密算法,粗略了解到对称算法,非对称算法,不可逆算法等等

正好本次项目中用到了 MD5加密,特此详解MD5加密算法

 

MD5加密算法

基本介绍

MD5是一个安全的散列算法,输入两个不同的明文不会得到相同的输出值根据输出值,不能得到原始的明文,即其过程不可逆;所以要解密MD5没有现成的算法,只能用穷举法,把可能出现的明文,用MD5算法散列之后,把得到的散列值和原始的数据形成一个一对一的映射表,通过比在表中比破解密码的MD5算法散列值,通过匹配从映射表中找出破解密码所对应的原始明文

 

破解方法

对信息系统或者网站系统来说,MD5算法主要用在用户注册口令的加密,对于普通强度的口令加密,可以通过以下三种方式进行破解:

(1)在线查询密码。一些在线的MD5值查询网站提供MD5密码值的查询,输入MD5密码值后,如果在数据库中存在,那么可以很快获取其密码值。

(2)使用MD5破解工具。网络上有许多针对MD5破解的专用软件,通过设置字典来进行破解。

(3)通过社会工程学来获取或者重新设置用户的口令。

因此简单的MD5加密是没有办法达到绝对的安全,因为普通的MD5加密有多种暴力破解方式,因此如果想要保证信息系统或者网站的安全,需要对MD5进行改造,增强其安全性,本文就是在MD5加密算法的基础上进行改进!

 

加密原理

MD5以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值。

在MD5算法中,首先需要对信息进行填充,使其字节长度对512求余数的结果等于448。因此,信息的字节长度(Bits Length)将被扩展至N*512+448,即N*64+56个字节(Bytes),N为一个正整数。填充的方法如下,在信息的后面填充一个1和无数个0,直到满足上面的条件时才停止用0对信息的填充。然后再在这个结果后面附加一个以64位二进制表示的填充前的信息长度。经过这两步的处理,现在的信息字节长度=N*512+448+64=(N+1)*512,即长度恰好是512的整数倍数。这样做的原因是为满足后面处理中对信息长度的要求。MD5中有四个32位被称作链接变量(Chaining Variable)的整数参数,他们分别为:A=0x01234567,B=0x89abcdef,C=0xfedcba98,D=0x76543210。当设置好这四个链接变量后,就开始进入算法的四轮循环运算,循环的次数是信息中512位信息分组的数目。

将上面四个链接变量复制到另外四个变量中:A到a,B到b,C到c,D到d。主循环有四轮(MD4只有三轮),每轮循环都很相似。第一轮进行16次操作。每次操作对a、b、c和d中的其中三个作一次非线性函数运算,然后将所得结果加上第四个变量(文本中的一个子分组和一个常数)。

再将所得结果向右环移一个不定的数,并加上a、b、c或d中之一。最后用该结果取代a、b、c或d中之一。以一下是每次操作中用到的四个非线性函数(每轮一个)。

其中,?是异或,∧是与,∨是或,是反符号。

如果X、Y和Z的对应位是独立和均匀的,那么结果的每一位也应是独立和均匀的。F是一个逐位运算的函数。即,如果X,那么Y,否则Z。函数H是逐位奇偶操作符。所有这些完成之后,将A,B,C,D分别加上a,b,c,d。然后用下一分组数据继续运行算法,最后的输出是A,B,C和D的级联。最后得到的A,B,C,D就是输出结果,A是低位,D为高位,DCBA组成128位输出结果。

安全性

从安全的角度讲,MD5的输出为128位,若采用纯强力攻击寻找一个消息具有给定Hash值的计算困难性为2128,用每秒可试验1000000000个消息的计算机需时1.07×1022年。若采用生日攻击法,寻找有相同Hash值的两个消息需要试验264个消息,用每秒可试验1000000000个消息的计算机需时585年。

 

算法应用

MD5加密算法由于其具有较好的安全性,加之商业也可以免费使用该算法,因此该加密算法被广泛使用,md5算法主要运用在数字签名、文件完整性验证以及口令加密等方面。

 

算法缺陷

在目前的信息系统中,对md5加密方法的利用主要通过在脚本页面中引用包含md5加密函数代码的文件,以asp脚本为例,在需要调用的页面中加入,md5.asp为md5加密函数代码文件,然后直接调用函数MD5(sMessage)即可,md5加密后的值有16位和32位之分,如果在md5加密函数中使用的是MD5 = LCase(WordToHex(a) &WordToHex(b) & WordToHex(c) & WordToHex(d)),则表示是32位,如果使用的是MD5=LCase(WordToHex(b) & WordToHex(c)),则表示是16位。例如对明文为“123456”的值进行加密,其md5值有两个,如下所示:

 

A=123456password=md5(A)= 49ba59abbe56e057 password=md5(A)=10adc3949ba59abbe56e057f20f883e如果将加密的md5值直接保存在数据库,当网站存在注入或者其它漏洞时,入侵者极有可能获取用户的密码值,通过md5在如果将加密的md5值直接保存在数据库,当网站存在注入或者其它漏洞时,入侵者极有可能获取用户的密码值,通过md5在线查询或者暴力破解可以得到密码。

 

算法改进

本文提到的方法是在使用md5加密算法对明文(口令)加密的基础上,对密文进行了改变,在密文中截取一段数据并丢弃,然后使用随机函数填充被丢弃的数据,且整个过程不改变md5加密后的位数。其加密过程用算法描述如下:

(1)对明文password进行md5加密,获得密文md5(password)。

(2)使用截取函数截取加密后的密文,从第beginnumber位置开始截取number位数值,得到密码A,其中A=left(md5(password),beginnumber-1)。

(3)使用截取函数截取加密后的明文的number位数后的值B,其中 B=right(md5(password),md5-digit -(beginnumber+number-1))。

(4)使用随机函数gen_key(number)填充被截取的number的值。

(5)变换后的密码值为encrypt_password =A&get_key(number)&B

变量说明:

解密过程跟加密过程有些类似,先对输入的明文进行加密,接着从beginnumber处截取前半部分得到A′,后半部分得到B′,然后从数据库中读出密码中的A和B部分,最后如果A=A′并且B=B′,则认为用户输入的密码跟数据库中的密码是匹配的。

当然,这只是改进 MD5 算法的一种方法,实际可行的思路有很多

 

结束

有人也曾经提出对md5加密算法中的函数或者变量进行修改,从而加强在使用原md5算法的安全,但是这种方法修改了md5原函数或者变量后,无法验证修改后md5算法在强度上是否跟原算法一致。本文提出的方法是在原有md5加密的基础上,通过对密文截取一定位数的字符串,并使用随机数进行填充,最后得到的密文虽然是经过md5加密,但是其值已经大不一样,因此通过md5常规破解方法是永远也不能破解其原始密码值,从而保证了数据的安全。虽然目前有很多攻击方法,诸如SQL注入、跨站攻击等,可以较容易的获取数据库中的值,通过本方法进行加密,在网站或者系统代码泄露前,其数据是相对安全的,因此具有一定参考加值。

 

</span>
    <span class="fr">
    <span class="fl" th:if="${session.user == null}">你好,请<a href="/login.html" style="color:#ff4e00;">登录</a>&nbsp;<a href="/register" style="color:#ff4e00;">免费注册</a>&nbsp;&nbsp;</span>
    <span class="fl" th:if="${session.user != null}"><a href="/user/userInfo" >欢迎回来,<span th:text="${session.user.userName}"></span></a>&nbsp;|&nbsp;<a href="/orders/list">我的订单</a>&nbsp;</span>
    <span class="fl" th:if="${session.user != null} and ${session.user.role != 0}">|&nbsp;<a href="/admin/adminIndex">后台管理&nbsp;</a></span>
    <span class="fl" th:if="${session.user != null}">|&nbsp;<a href="/admin/adminIndex">秒杀商品&nbsp;</a></span>
    <span class="fl" th:if="${session.user != null}">|&nbsp;<a href="/user/logout">注销</a></span>
</span>

注意:多条件下使用 th:if 格式为
 <span class="fl" th:if="${session.user != null} and ${session.user.role != 0}">|&nbsp;<a href="/admin/adminIndex">后台管理&nbsp;</a></span>

前面的学习中,我们完成了防止超卖商品和抢购接口的限流,已经能够防止大流量把我们的服务器直接搞炸,这篇文章中,我们要开始关心一些细节问题,我们现在设计的系统中还有一些问题

1.我们应该在一定时间内执行秒杀处理,不能在任意时间都接收秒杀请求,如何加入时间验证?

2.对于现有的接口,暴露了我们的接口地址,然后通过脚本抢购怎么办?

3.秒杀开始之后如何限制单个用户的请求频率,即单位时间内的访问次数?

 

此节内容解决:

  • 限时抢购
  • 抢购接口隐藏
  • 单用户限制频率(单位时间内限制访问次数)

 

限时抢购的实现

使用Redis来记录秒杀商品的时间,对秒杀过期的请求进行拒绝处理

 

秒杀请求被拦截:

数据库无改变,即未卖出

Redis 抢购时间可以自己设置,通过传参

通过乐观锁防止超卖+令牌桶限流

 

//开发一个秒杀方法 乐观锁防止超卖,令牌桶限流
@GetMapping("/killtoken")
public  String killtoken(Integer id){
    LOGGER.info("秒杀商品的 ID = " + id);
    //加入令牌桶的限流措施
    //注意:限流之后商品不能百分百的卖掉,有些请求被抛弃,保留一小部分的商品
    if(!rateLimiter.tryAcquire(2,TimeUnit.SECONDS)){
        return "抢购失败,当前秒杀活动过于火爆,请重试!";
    }
    try {//根据秒杀商品的 ID 调用秒杀业务
        int orderId = orderService.kill(id);
        return "秒杀成功!订单ID为:" + orderId;
    }catch (Exception e){
        e.printStackTrace();
        return e.getMessage();
    }
}

会出现商品剩余的情况,因为在接口限流时有一部分请求被抛弃


查看数据库卖出的商品数量:


如果想多卖一点怎么办呢?
1.并发请求加多(Jmeter测试)
2.尝试获取令牌桶的时间+1s
3.增加令牌桶初始大小

  

引入依赖 guava

<!-- Google接口限流 guava  RateLimter 令牌桶实现-->
<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>30.1.1-jre</version>
</dependency>

既然是接口限流,那么一般将限流放在控制器Controller

@GetMapping("/sale")
public String sale(Integer id){
    //1.没有获取到 token 请求 直到获取到 token 令牌
    LOGGER.info("等待的时间:" + rateLimiter.acquire());//试图拿到令牌
    //2.设置一个等待的时间,如果在等待的时间内获取到了 token 令牌,则处理业务,如果在等待时间内没有获取到相应的 token 则抛弃请求
    if(!rateLimiter.tryAcquire(5, TimeUnit.SECONDS)){
        System.out.println("当前请求被限流,直接抛弃,无法调用后续秒杀逻辑......");
        return "抢购失败!";
    }
    System.out.println("处理业务..................");
    return "测试令牌桶";
}

令牌桶原理:先获取令牌,再执行业务逻辑

使用同步代码块,效率低下,因此改为乐观锁

 

StockMapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.southwind.mmall002.mapper.StockMapper">


    <!--根据商品 ID 扣除库存-->
    <update id="updateSale" parameterType="com.southwind.mmall002.entity.kill.Stock">
        update stock
        set
            saled=saled + 1,
            version = version + 1
        where
            id = #{id}
            and
            version = #{version}
    </update>
</mapper>

Service层代码
    public  Integer kill(Integer id, HttpSession session) {
        //根据 商品 ID 校验库
        Stock stock = checkStock(id);
        LOGGER.info("商品的名称为:" + stock.getName());
        LOGGER.info("商品的已售为:" + stock.getSaled());
        LOGGER.info("商品的库存为:" + stock.getCount());
        //更新库存
        updateSale(stock);
        //创建订单
        return createOrder(stock,session);
    }

    //校验库存
    private Stock checkStock(Integer id){
        Stock stock = stockMapper.selectById(id);
        if(stock.getSaled().equals(stock.getCount())){
            throw new RuntimeException("库存不足!!!");
        }
        return stock;
    }

    //扣除库存
    private void updateSale(Stock stock){
        LOGGER.info("准备更新库存...");
        //在 SQL 层面完成销量的+1 和 版本号的+1 并且根据商品 ID 和版本号同时查询更新的商品
        stock.setSaled(stock.getSaled() + 1);
        int updateRows = stockMapper.updateSale(stock);//返回更新的条数
        if(updateRows == 0 ){
            throw new RuntimeException("抢购失败,请重试!!!");
        }
    }

    //创建订单
    private Integer createOrder(Stock stock,HttpSession session){
        User user= (User) session.getAttribute("user");
        Orders orders = new Orders();
        orders.setUserId(user.getId());
        orders.setLoginName(user.getLoginName());
        orders.setSerialnumber(stock.getName());
        orderMapper.insert(orders);
        return orders.getId();
    }

}

结果:实现了线程同步

public synchronized Integer kill(Integer id, HttpSession session) {
    //根据 商品 ID 校验库
    Stock stock = checkStock(id);
    LOGGER.info("商品的名称为:" + stock.getName());
    LOGGER.info("商品的已售为:" + stock.getSaled());
    LOGGER.info("商品的库存为:" + stock.getCount());
    //更新库存
    updateSale(stock);
    //创建订单
    return createOrder(stock,session);
}

尝试给 kill 方法加悲观锁解决超卖

坑点:synchronized 加在业务层,而业务层加了事务控制,加入事务控制会导致线程同步(事务的)
而synchronized 也有一个线程同步,事务的线程同步的范围比synchronized 的大

当我们的synchronized 代码块确实能保证线程排队走,但是synchronized 代码块结束之后,事务还没结束,这个线程已经把锁释放了
但事务还未结束,事务还没提交,此时下一个线程已经来了,来了之后,此时事务开始提交,下一个线程执行了,执行的时候数据库也跟着提交
这样就有可能出现多次提交的问题,所以即使在业务层加上synchronized 也会出现超卖问题

解决方案:
1. 去掉事务
2. synchronized 放在我们的Controller  调用处,保证synchronized 的线程同步范围比事务的要大