0
点赞
收藏
分享

微信扫一扫

暴撸三万字,Spring注解驱动开发,SpringBoot基石,你还在为大量的Spring配置而烦恼?

楠蛮鬼影 2022-04-08 阅读 37
SpringSSM

文章目录

建议先学

这已经算是高级部分了,如果你还没学习SSM这最基本的三大框架,还是赶紧如学上吧,并且做个增删改查的小项目

  1. 五万字的Spring5学习笔记,带你熟悉运用Spring5
  2. 四万多字的SpringMVC学习总结,带你领略不一样的SpringMVC
  3. 怎么请求数据库的数据?这套四万多字的Mybatis学习笔记给你答案,只做入门,不做深层次分析
  4. 基于SSM框架的CRUD小项目,外加JSR303,使用Maven搭建工程,前端使用Jquery对Ajax的封装进行异步请求

介绍

在这里插入图片描述

Spring注解驱动开发是个什么东西?
如果你学过原生的Spring,就会发现,配置bean很困难,但在Spring高级当中,你可以使用注解的方式进行,省去了一大堆配置,直接以@bean的方式注入,这便是Spring注解驱动开发的高明之处,当然,功能远不止那么多.

为什么要学习?

SpringBoot底层还是Spring,但肯定不是使用原生的去做的,而是使用大量的Spring注解去简化配置,所以学习Spring注解驱动开发,对学习Springboot有很大帮助,说实话,学得好的话Spring注解驱动开发的话,你可以随便看看SpringBoot就可以了,SpringBoot底层这些东西可以以后面试再学.

组件注册

原始配置

  • 在Spring中,原始的注册一个bean需要在类路径下配置一个xml文件,然后在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 ">
        <bean id="person" class="com.hyb.Person">
            <property name="id" value="10"></property>
            <property name="name" value="hyb"></property>
        </bean>
    </beans>
    
  • 然后通过ApplicationContext进行获取该bean

    @Test
    public void t1(){
        //获取类路径下的的xml文件
        ApplicationContext applicationContext=new ClassPathXmlApplicationContext("bean.xml");
        Person person =(Person) applicationContext.getBean("person");
        System.out.println(person.toString());
    }
    

Configration

  • 该注解可简化原始配置,不用再配置xml文件的方式就可以注册一个bean

    //标记该类是一个配置类
    @Configuration
    public class MainConfig {
    
        @Bean("person")
        public Person person(){
            return new Person(10,"hyb");
        }
    }
    
  • 测试用的实现类不一样,是基于注解的ApplicationContext

    ApplicationContext applicationContext1=new AnnotationConfigApplicationContext(MainConfig.class);
    Person bean = applicationContext1.getBean(Person.class);
    System.out.println(bean.toString());
    

ComponentScan

  • 使用该注解需要在需要扫描的类中加上@Component 注解。

  • 该注解主要是为了解决xml配置文件中扫描包的问题

    <!--只要标注了@Controller,@Service,@Component的注解都能被扫描-->
    <context:component-scan base-package="com.hyb"></context:component-scan>
    
  • 而我们用了注解,便可以更加方便快捷低进行扫描。

    //标记该类是一个配置类
    @Configuration
    @ComponentScans({
    //        value指定要扫描的包
            @ComponentScan(value = "com.hyb",excludeFilters = {
    //                指定过滤类型,此处按照注解过滤,所以传入注解,表示有这个注解的类都要排除
                    @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})
    
            }),
        //use..=false 代表使用默认的扫描器,才能让includeFilter里的注解只被扫描到
            @ComponentScan(value = "com.hyb",includeFilters = {
                    @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})
            },useDefaultFilters = false)
    })
    @ComponentScan(value = "com.hyb",includeFilters = {
            @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Service.class})
    },useDefaultFilters = false)
    
  • 从上面的例子可以看到,我们不仅用了@ComponentScan这个注解,还用了@ComponentScans注解,后者代表多个@ComponentScan注解的集中写法而已,作用都一样。

  • 注意:既然最原始的写法都要在配置文件中,那这个注解也要在配置文件中,即要在@Configuration注解中。

Component 和Bean

  • 两者所完成的功能是一样的,但是@Bean更加灵活一些,后者提供了在配置类手动添加bean,在添加bean之前可以做一些操作。
  • 而前者要配置ComponentScan使用,并且一次只能扫描一个对象。
  • 在我们不知道源码而要使用一些类的时候,就可以使用后者,直接注解一个bean。而如果是我们自己写的类,只需要注册一次,便可以使用前者。

自定义规则

  • 在前面我们扫描组件的时候,可以指定过滤规则,可以翻开源码,会发现,官方给了几种规则

    ANNOTATION, 注解规则
    ASSIGNABLE_TYPE, 类型规则,这里一般传入类.class
    ASPECTJ,ASPECTJ规则,很少用到
    REGEX,正则表达式规则
    CUSTOM;自定义规则
    
  • 下面我们便演示自定义规则,规定自定义规则必须实现

    package com.hyb;
    
    import org.springframework.core.io.Resource;
    import org.springframework.core.type.AnnotationMetadata;
    import org.springframework.core.type.ClassMetadata;
    import org.springframework.core.type.classreading.MetadataReader;
    import org.springframework.core.type.classreading.MetadataReaderFactory;
    import org.springframework.core.type.filter.TypeFilter;
    
    import java.io.IOException;
    
    public class MyType implements TypeFilter {
        @Override
        public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
    //        metadataReader 读取当前类信息
    //        获取当前类注解信息
            AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
    //        获取当前类信息
            ClassMetadata classMetadata = metadataReader.getClassMetadata();
    //        获取当前类资源信息,比如类路径
            Resource resource = metadataReader.getResource();
    
    //        获取当前类类名
            String name = classMetadata.getClassName();
    //        如果类名包含er的就扫描
            return name.contains("er");
    //        metadataReaderFactory 读取其他类信息
    //        自定义匹配失败 return false
        }
    }
    

Scope

  • 指定bean作用域,返回一个单实例还是多实例

    @Configuration
    public class ScopeConfig {
    
    //    默认单实例
    //    String SCOPE_SINGLETON = "singleton"; 单实例
    //    String SCOPE_PROTOTYPE = "prototype"; 多实例
        @Scope("prototype")
        @Bean("person")
        public Person getPerson(){
            return new Person(2,"zyl");
        }
    }
    
        @Test
        public void t2(){
            AnnotationConfigApplicationContext scopeConfig = new AnnotationConfigApplicationContext(ScopeConfig.class);
            Person person1=(Person)scopeConfig.getBean("person");
            Person person2=(Person)scopeConfig.getBean("person");
    //        若是单实例,当new AnnotationConfigApplicationContext(ScopeConfig.class);
    //        IOC容器便会加载完成,创建出一个对象,以后每次getbean一个对象的时候,都会IOC容器里拿,所以person1和person2是相等的
    //        如果是多实例,每次getBean一个对象的时候并创建一个对象,所以每次的对象是不相等的
            System.out.println(person1==person2);
        }
    

Lazy

  • 懒加载,针对于单实例,延迟单实例创建时间,若有懒加载,单实例会在第一次获取bean的时候才创造对象,然后放入IOC容器中,相反没有懒加载,单实例会在创建IOC容器的时候帮你创建好对象。

Conditional

  • 该注解提供了自定义条件注册bean。

  • 我们先实现两个条件,要求在只有在Windows环境下才能注册成功

    public class MyCondition implements Condition {
        @Override
        public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
            Environment environment = conditionContext.getEnvironment();
            String property = environment.getProperty("os.name");
            return property.contains("Windows");
        }
    }
    
    public class LinuxCondition implements Condition{
        @Override
        public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
            Environment environment = conditionContext.getEnvironment();
            String property = environment.getProperty("os.name");
            return property.contains("linux");
        }
    }
    
  • 之后,在注册bean的时候加上注解

    @Conditional({MyCondition.class})
    @Bean("person1")
    public Person getPerson1(){
        return new Person(3,"hyb");
    }
    
    @Conditional({LinuxCondition.class})
    @Bean("person2")
    public Person getPerson2(){
        return new Person(4,"hyb1");
    }
    
        @Test
        public void t3(){
            AnnotationConfigApplicationContext an = new AnnotationConfigApplicationContext(ScopeConfig.class);
            String[] beanNamesForType = an.getBeanNamesForType(Person.class);
            for (String s : beanNamesForType) {
                System.out.println(s);
            }
    //        获取当前环境
            ConfigurableEnvironment environment = an.getEnvironment();
    //        获取当前环境名字
            String property = environment.getProperty("os.name");
            System.out.println(property);
    
    //        找到所有的Person类型的对象
            Map<String, Person> beansOfType = an.getBeansOfType(Person.class);
            System.out.println(beansOfType);
    
        }
    
  • 之后你便会发现,当我们加上此注解后,只有在Windows环境下才能注册bean成功

  • 注意:该注解也可以加在类上,表示该配置类中的所有bean都得复合Windows操作系统才能注册

Import

  • 代替当需要注册多个bean的写法

  • 直接在配置文件上加上这个注解,并传入类.class,便可以将该类注册成bean,id默认为全类名

    @Import({Color.class})
    

    便可以代替@bean的方式注册Color组件。

  • 你还可以导入一个包含类多个注册类的类,来代替直接写多个类

    @Import({ImportClass.class})
    

    比如这里,我们让ImportClass 这个类注册多个类,就省去了写多个注册类的麻烦

    public class ImportClass implements ImportSelector {
        @Override
        public String[] selectImports(AnnotationMetadata annotationMetadata) {
    //        annotationMetadata 可以获取所有注解信息
    //        不能返回null,当为空的时候返回空数组
    //        将需要注册的bean的全类名注册在数组里
            return new String[]{"config.Red"};
        }
    }
    
  • 最后一种方式,你可以指定bean注册的名称

    public class SecondImportClass implements ImportBeanDefinitionRegistrar {
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    //        importingClassMetadata 当前bean一些注解信息
    //        指定bean名称,注册bean
            RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Blue.class);
            registry.registerBeanDefinition("Blue",rootBeanDefinition);
        }
    
    }
    
    @Import({SecondImportClass.class})
    

FactoryBean接口

  • 使用Spring内置的工厂接口也可以注册bean

    public class YellowFactory implements FactoryBean<Yellow> {
    
    //    注册一个对象
        @Override
        public Yellow getObject() throws Exception {
            return new Yellow();
        }
    
    //    返回类型
        @Override
        public Class<?> getObjectType() {
            return Yellow.class;
        }
    
    //    是否单例
        @Override
        public boolean isSingleton() {
            return true;
        }
    }
    
  • 实现接口后,我们可以在配置文件中注册这个工厂bean

    @Bean("factory")
    public YellowFactory yellowFactory(){
        return new YellowFactory();
    }
    
  • 但如果我们获取起类型,你会发现,该工厂bean的类型是其注册的bean的类型

    @Test
    public void t4(){
        Object yellowFactory = an.getBean("factory");
        System.out.println(yellowFactory.getClass());
    
    }
    

    输出class config.Yellow

    如果你实在想获取工厂bean类型:

    Object yellowFactory = an.getBean("&factory");
    System.out.println(yellowFactory.getClass());
    

指定初始化方法和销毁方法

  • 在bean注解里,可以指定一个对象的初始化和销毁方法

    package config;
    
    public class Life {
        public Life(){
            System.out.println("构造Life");
        }
        public void init(){
            System.out.println("初始化Life");
        }
        public void destory(){
            System.out.println("销毁Life");
        }
    }
    
    @Bean(value = "Life",initMethod = "init",destroyMethod = "destory")
    public Life getLife(){
        return new Life();
    }
    
        @Test
        public void t5(){
    //        单实例对象,在初始化IOC的时候会创造对象,所以调用Life的构造方法
        AnnotationConfigApplicationContext an = new AnnotationConfigApplicationContext(ScopeConfig.class);
    //         获取bean的时候,调用指定的初始化方法
            Life life=(Life) an.getBean("Life");
    //        关闭IOC的时候,调用指定的销毁方法,但如果是多实例对象,就算关闭IOC,也不会调用指定的销毁方法
            an.close();
        }
    
  • 实现接口也可以指定初始化方法和销毁方法

    public class Life implements InitializingBean, DisposableBean {
        public Life(){
            System.out.println("构造Life");
        }
    
        @Override
        public void destroy() throws Exception {
            System.out.println("销毁方法");
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
            System.out.println("初始化方法");
        }
    }
    
  • 也可以使用jsr250的两个注解进行指定初始化和销毁方法

    public class Life {
        public Life(){
            System.out.println("构造Life");
        }
    //    在bean创建完成并属性赋值完成调用
        @PostConstruct
        public void init(){
            System.out.println("初始化Life");
        }
    //    在对象销毁前调用
        @PreDestroy
        public void destory(){
            System.out.println("销毁Life");
        }
    }
    

后置处理器

  • Spring提供了一个后置处理器接口,可以在初始化前后的方法中被调用

    public class MyPostProcesser implements BeanPostProcessor {
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            System.out.println("初始化之前被调用了");
            return bean;
        }
    
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            System.out.println("初始化之后被调用了");
            return bean;
        }
    }
    
    public class Life {
        public Life(){
            System.out.println("构造Life");
        }
    //    在bean创建完成并属性赋值完成调用
        @PostConstruct
        public void init(){
            System.out.println("初始化Life");
        }
    //    在对象销毁前调用
        @PreDestroy
        public void destory(){
            System.out.println("销毁Life");
        }
    }
    

    我们定义的Life类指定初始化和销毁函数,上面的接口便可以在该类的初始化方法前后工作。

        @Test
        public void t5(){
    //        单实例对象,在初始化IOC的时候会创造对象,所以调用Life的构造方法
    
    //         获取bean的时候,调用指定的初始化方法
            Life life=(Life) an.getBean("Life");
    //        关闭IOC的时候,调用指定的销毁方法,但如果是多实例对象,就算关闭IOC,也不会调用指定的销毁方法
            an.close();
        }
    

    输入:

    初始化之前被调用了
    初始化Life
    初始化之后被调用了

    销毁Life

Value

  • 在注册bean的时候,spring提供了该注解让我们直接在原始类中进行赋值。有三种常用赋值方式,配置文件最常见,但会有乱码问题

    //    直接赋值,或者可以编写表达式
        @Value("1")
    //    表达式赋值
    //    @Value("#{2-1}")
        private Integer id;
    
        @Value("${person.name}")
        private String name;
    

    ${person.name}表示读取外部的properties结尾的配置文件,在注册Person的bean时,在配置类上加上引导properties配置文件的注解

    @PropertySource(value = {"classpath:/person.yml"})
    @Configuration
    

    之后,当你获取该bean的时候便可以得到带有属性值的对象

AutoWried

  • 该注解可以自动注入一个对象

    @Repository
    public class BookDao {
    
        private int label;
    
        public void setLabel(int label) {
            this.label = label;
        }
    
        @Override
        public String toString() {
            return "BookDao{" +
                    "label=" + label +
                    '}';
        }
    }
    
    @Service
    public class BookService {
    
        @Autowired
        BookDao bookDao2;
    
        @Override
        public String toString() {
            return "BookService{" +
                    "bookDao=" + bookDao2.toString() +
                    '}';
        }
    }
    
    @Configuration
    @ComponentScan({"com.hyb"})
    public class DaoConfig {
    
        @Bean("bookDao2")
        public BookDao getBookDao(){
            BookDao bookDao = new BookDao();
            bookDao.setLabel(2);
            return bookDao;
        }
    }
    
    @Test
    public void t7(){
        AnnotationConfigApplicationContext an = new AnnotationConfigApplicationContext(DaoConfig.class);
        BookService bookService=(BookService) an.getBean("bookService");
        System.out.println(bookService);
    }
    
  • 答案是输入的label=2的对象。这是因为:我们在IOC容器里创建了类型一致的对象,而如果IOC里有两个或两个以上的对象时候,getbean方法就会通过@Autowired注解注入的对象名来寻找,因为在DaoConfig中,我们配置了一个对象叫bookDao2,所以这个时候会注入bookDao2。而如果IOC容器里只有一个对象的时候,getbean方法就会通过类型寻找,从而默认找到了注入的对象。

  • 当然,如果你注入的时候写的名字就不对,而想用其他名字的对象,可以用一个注解来解决

    @Qualifier("bookDao")
    //    @Qualifier("bookDao")
        @Autowired(required = false)
        BookDao bookDao2;
    

    该注解用来指定注入getbean会在IOC容器里找到哪个对象进行注入

  • 注意:如果我们@Repository和@Bean(“bookDao2”)注解注释掉,IOC容器里就不会有对象了,这个时候注入是会出错的,但是我们可以要求自动注入的属性。

    @Autowired(required = false)
    

    设置为false表示IOC容器该类型对象为空的时候,不进行注入。

primary

  • 该注解可以设置首选哪个对象进行装配

        @Primary
        @Bean("bookDao2")
        public BookDao getBookDao(){
            BookDao bookDao = new BookDao();
            bookDao.setLabel(2);
            return bookDao;
        }
    
  • 而如果我们就是要装配指定的对象,就在 @Autowired注解之上用@Qualifier指定。

AutoWired位置

  • 该注解可以标注在属性上,也可以标注在方法,构造器,参数上,都是从IOC容器拿到的值,同时,当标注在构造器中时,如果该构造器是该类唯一的构造器,注解可以省略。不仅如此,当我们通过标注有@bean注解的方法来获取对象时,如果该注解含有参数带有,且该参数是对象,那么该参数也是直接从IOC容器中获取。

Resource

  • 该注解主持jsr250,但不支持@primary和@Autowired的功能。

        @Resource //按照名称来装配,该注解有name属性可以指定名称装配,没有支持@Primary的功能,也不支持AutoWired的属性
    

Inject

  • 该注解需要导入依赖,支持@Primary但不支持@Autowired的功能。

    <!-- https://mvnrepository.com/artifact/javax.inject/javax.inject -->
    <dependency>
      <groupId>javax.inject</groupId>
      <artifactId>javax.inject</artifactId>
      <version>1</version>
    </dependency>
    
    @Inject //支持primary,但不支持AutoWired的属性
    

profile

  • 该注解提供了环境切换的功能,所谓环境切换就是在开发中,有时候需要用到测试环境,生产环境不同的配置对象。

  • 接下来,我将模拟出两个数据库连接数据源。在这之前,我们得导入Durid连接池,和数据库连接jar

    <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.2.5</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.25</version>
    </dependency>
    
  • 然后我们事先编写一个配置文件,将数据库连接的配置信息写好,然后使用一个配置类创建数据连接源的时候,适合从外部导入该文件的配置。

    package config;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.EmbeddedValueResolverAware;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Profile;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.util.StringValueResolver;
    
    import javax.sql.DataSource;
    
    @PropertySource("classpath:/resource.properties")
    @Configuration
    public class DataResourceConfig implements EmbeddedValueResolverAware {
    
        private final DruidDataSource  druidDataSource = new DruidDataSource();
    // 通过value解析,该解析也可以放在形参位置
        @Value("${jdbc.username}")
        private String user;
    
        @Value("${jdbc.pwd}")
        private String pwd;
    
    // 通过之解析器的方式解析${}
        private String driverClass;
    
        private String jdbcUrl;
    

    // 测试环境

    @Profile("test")
    @Bean
    public DataSource dataSource(){
        druidDataSource.setUsername(user);
        druidDataSource.setPassword(pwd);
        druidDataSource.setDriverClassName(driverClass);
        druidDataSource.setUrl(jdbcUrl);
        return druidDataSource;
    }
    

    //生产环境

    @Profile("product")
    @Bean
    public DataSource proDataSource(){
        druidDataSource.setUsername(user);
        druidDataSource.setPassword(pwd);
        druidDataSource.setDriverClassName(driverClass);
        druidDataSource.setUrl(jdbcUrl);
        return druidDataSource;
    }
    
    @Override
    public void setEmbeddedValueResolver(StringValueResolver stringValueResolver) {
        this.driverClass=stringValueResolver.resolveStringValue("${jdbc.driver}");
        this.jdbcUrl=stringValueResolver.resolveStringValue("$jdbc.url}");
    }
    

    }

从以上的数据源可以看出,我们创建了两个数据源,这在开发中极为常见,所以我们需要给每个方法上标明该注解,然后给上环境名字,这样我们在调用的时候就可以规定使用哪个环境。

值得注意的是,该注解不仅可以用在返回一个对象的方法上,还可以用在整个配置类上,表明该类全部属于该环境,无论该配置类的方法上有任何其他环境都将不起作用。

- 下面我们进行测试,在测试中有两种使用某个环境的方式,但是这里推荐使用代码的方式,有更好的移植性。

​```java
    @Test
    public void t8(){
//        只是初始化,先不传入配置类
        AnnotationConfigApplicationContext an = new AnnotationConfigApplicationContext();
//       获取环境,设置环境
        an.getEnvironment().setActiveProfiles("test","product");
//        注册配置类
        an.register(DataResourceConfig.class);
//        刷新容器
        an.refresh();
//        获取IOC容器中含有的类型为DataSource的对象
        String[] beanNamesForType = an.getBeanNamesForType(DataSource.class);
        for (String s :
                beanNamesForType) {
            System.out.println(s);
        }
    }

我们可以看到,我们可以同时使用一个或多个环境,如果没有设置该环境的bean,默认不受任何限制,仍然可以使用。一旦设置了该环境,在IOC容器里便可以使用。

AOP

  • aop:在代码的执行过程中指定某个位置动态插入断码执行,底层原理运用了动态代理的模式。

  • 下面对一个方法的执行前后进行模拟通知,首先我们先创造一个类,并创造一个方法

    public class Count {
    
        public int countXY(int x,int y){
            System.out.println("方法运行中。。。。");
            return x+y;
        }
    }
    
  • 然后创建一个AOP类,并标明countXY方法前后执行的方法

    //告诉Spring容器当前类是一个切面类
    @Aspect
    public class Log {
    
    //    抽取共同的切入点表达式
        @Pointcut("execution(public int com.hyb.Count.*(..))")
        public void pointCut(){}
    
    //    @Before("public int com.hyb.Count.countXY(int,int)")
    //    前置通知 ,joinPoint 必须写在参数第一位
        @Before("pointCut()")
        public void before(JoinPoint joinPoint){
    //        获取截取方法名
            String name = joinPoint.getSignature().getName();
    //        获取参数列表
            Object[] args = joinPoint.getArgs();
    
            System.out.println("除法运行之前,传入方法"+name+",传入参数"+ Arrays.asList(args));
        }
    
    //后置通知
        @After("pointCut()")
        public void end(){
            System.out.println("方法结束。。。。");
        }
    
    //    返回通知,result 为自定义返回结果参数
        @AfterReturning(value = "pointCut()",returning = "result")
        public void methodReturn(Object result){
    //        result 为返回结果
            System.out.println("方法返回。。。。。"+result);
        }
    
    //    异常通知,ex 为自定义抛出异常参数
        @AfterThrowing(value = "pointCut()",throwing = "ex")
        public void methodExep(Exception ex){
            System.out.println("方法异常。。。。");
        }
    
    //    环绕通知 @Round //动态代理,手动切换目标方法运行
    }
    
  • 然后编写配置类,将以上两个类都加入到IOC容器中

    //开启基于注解的AOP模式
    @EnableAspectJAutoProxy
    @Configuration
    public class AopConfig {
    
    //    业务逻辑类
        @Bean
        public Count count(){
            return new Count();
        }
    
    //    切面类
        @Bean
        public Log log(){
            return new Log();
        }
    }
    
  • 之后测试

    @Test
    public void t9(){
        AnnotationConfigApplicationContext an = new AnnotationConfigApplicationContext(AopConfig.class);
        Count count= (Count) an.getBean("count");
        int i = count.countXY(1, 2);
        System.out.println(i);
    }
    

    之后你便会发现,当我们调用countXY的时候,AOP类中的方法都被执行了。

事务

  • 在非Spring的注解驱动开发中,我们需要在配置文件允许事务注解驱动,才能使用事务,所以在注解驱动开发中,也需要类型的行为。

  • 首先得在配置文件上,加上允许注解驱动事务开发的注解

    @EnableTransactionManagement
    @Configuration //配置文件
    
  • 然后在该文件中配置数据源

         private final DruidDataSource  druidDataSource = new DruidDataSource();
        @Bean
        public DataSource dataSource(){
            druidDataSource.setUsername(user);
            druidDataSource.setPassword(pwd);
            druidDataSource.setDriverClassName(driverClass);
            druidDataSource.setUrl(jdbcUrl);
            return druidDataSource;
        }
    
  • 然后在该配置文件中,配置事务管理器对象

    @Bean
    public PlatformTransactionManager platformTransactionManager(){
        return new DataSourceTransactionManager(dataSource());
    }
    
  • 之后便可以在需要加上事务的方法中加上注解@Transactional便可以声明事务了。

  • 需要注意的是,事务发生在于数据库交互的时候,所以要导入数据库依赖和连接池依赖,最重要的是导入spring-jdbc的依赖。

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.3.13</version>
    </dependency>
    

拓展接口

BeanFactoryPostProcessor

  • 该接口提供了在IOC容器加载完bean定义而未创建bean之间可以操作的接口

  • 实现该接口

    //该注解也可以声明主键
    @Component
    public class MyFactoryProcessor implements BeanFactoryPostProcessor {
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
    //        后置处理器,可以在IOC容器加载bean定义而未创建bean对象之间来进行操作
    
    //        拿到有几个bean定义了
            int count = configurableListableBeanFactory.getBeanDefinitionCount();
    //        每个bean定义的名字
            String[] beanDefinitionNames = configurableListableBeanFactory.getBeanDefinitionNames();
            System.out.println("有《"+count+"》个bean");
            for (String s :
                    beanDefinitionNames) {
                System.out.println("bean-->"+s);
            }
    
        }
    }
    
  • 写配置类,将该接口扫描

    @ComponentScan("config")
    @Configuration
    public class ExtentConfig {
    
        @Bean
        public Count count(){
            return new Count();
        }
    }
    
  • public class Count {
    
        public Count() {
            System.out.println("count创建了对象");
        }
    }
    
  • @Test
    public void t10(){
        AnnotationConfigApplicationContext an = new AnnotationConfigApplicationContext(ExtentConfig.class);
        Count count=(Count) an.getBean("count");
    }
    //有《1》个bean
    //bean-->count
    //count创建了对象
    

BeanDefinitionRegistryPostProcessor

  • 接口提供了在IOC容器未定义bean之前实现操作,即在BeanFactoryPostProcessor接口之前

    @Component
    public class MyRegistry implements BeanDefinitionRegistryPostProcessor {
        @Override
        public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
            //        这里我们可以先手动注册一个bean
    //        RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Count.class);
            AbstractBeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Count.class).getBeanDefinition();
            beanDefinitionRegistry.registerBeanDefinition("c1",rootBeanDefinition);
            System.out.println("手动注册了一个bean C1");
    
        }
    
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
            System.out.println("postProcessBeanDefinitionRegistry-->"+configurableListableBeanFactory.getBeanDefinitionCount());
        }
    }
    
  • 还是一样的类,一样的配置类,一样的测试,我们来看看结果

    //手动注册了一个bean C1
    //postProcessBeanDefinitionRegistry-->30
    //有《2》个bean
    //count创建了对象
    //count创建了对象
    

ApplicationListener<“ApplicationEvent”>

  • 事件监听器

    @Component
    public class MyListener implements ApplicationListener<ApplicationEvent> {
    
    //    监听发布的事件而除法的方法
        @Override
        public void onApplicationEvent(ApplicationEvent applicationEvent) {
            System.out.println("发布了事件-->"+applicationEvent);
        }
    }
    //发布了事件-->org.springframework.context.event.ContextRefreshedEvent[source=org.springframework.context.annotation.AnnotationConfigApplicationContext@333291e3, started on Sat Dec 18 17:00:20 CST 2021]
    //发布了事件-->org.springframework.context.event.ContextClosedEvent[source=org.springframework.context.annotation.AnnotationConfigApplicationContext@333291e3, started on Sat Dec 18 17:00:20 CST 2021]
    

    ContextRefreshedEvent 为容器刷新完成事件,ContextClosedEvent为容器关闭事件

  • 事件发布:

    @Test
    public void t10(){
        AnnotationConfigApplicationContext an = new AnnotationConfigApplicationContext(ExtentConfig.class);
        Count count=(Count) an.getBean("count");
        an.publishEvent(new ApplicationEvent(new String("我发布的事件")) {});
        an.close();
    }
    //发布了事件-->com.hyb.beanTest$1[source=我发布的事件]
    
  • 自定义方法监听事件:

    @EventListener(classes = {ApplicationEvent.class})
    public void listener(ApplicationEvent applicationListener){
        System.out.println("Count类监听事件-->"+applicationListener);
    }
    

Servlet3.0

@WebServlet

  • 在Servlet3.0以上,我们不需要再配置web.xml而是利用Spring的注解驱动开发。

  • 要演示例子,我们首先得导入Servlet的jar,这个jar必须是3.0以上的。

    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>provided</scope>
    </dependency>
    
  • 然后编写index.jsp

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <title>Title</title>
    </head>
    <body>
        <a href="hello">hello</a>
    </body>
    </html>
    
  • 最后我们编写一个Servlet,这里不用配置web.xml

    @WebServlet("/hello")
    public class HelloServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            resp.getWriter().write("hello World");
        }
    }
    

    在这个类中,我们可以看到,我们只需要用一个注解@WebServlet 便可以实现Servlet的访问

ServletContainerInitializer

  • 该接口提供给开发者以在项目启动的时候做一些初始化的工作,如Servlet,Filter注册等,来代替传统的Servlet配置。

  • 要实现该功能,就必须实现该接口,该接口只有一个方法可以重写

    @HandlesTypes(value = {MyObject.class})
    public class MyServletContainerInitializer implements ServletContainerInitializer {
    
    //    Set<Class<?>> 表示@HandlesTypes注解传入的感兴趣的类型
        @Override
        public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
            for (Class<?> c :
                    set) {
                System.out.println(c);
            }
        }
    }
    
  • 在上面的代码中,我们会有看到注解@HandlesTypes(value = {MyObject.class}) 该注解用来在初始化时传入一些开发者想传进去的类的子类,接口等等,不包含本类。例如MyObject.class ,表示传入MyObject类的子类或其实现接口等向下转型的类或接口。

    例如,我们实现接口MyObject

    public class MyObjectService implements MyObject {
    }
    

    在初始化过程中,就会将该类传入到ServletContainerInitializer接口的实现类MyServletContainerInitializer中,然后通过Set<Class<?>>便可以保存传入的类的全类型。

  • 注意:在实现ServletContainerInitializer接口后,必须在resource目录下创建MTETA-INF/services 目录,然后在该目录下创建一个名为 javax.servlet.ServletContainerInitializer 的txt文件,文件里写上ServletContainerInitializer接口的实现类全类名。

ServletContext

  • 该类型代替了原有的web.xml 来注册Servlet,Listener和Filter。

  • 我先手工实现三个组件和一个页面

    public class UserListener implements ServletContextListener {
    
    //    监听ServletContext初始化
        @Override
        public void contextInitialized(ServletContextEvent servletContextEvent) {
            System.out.println("Servlet.UserListener init");
        }
    
    //    监听ServletContext销毁
        @Override
        public void contextDestroyed(ServletContextEvent servletContextEvent) {
            System.out.println("Servlet.UserListener destroy ");
        }
    }
    
    @WebServlet("/hello")
    public class HelloServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            System.out.println("Servlet.HelloServlet");
        }
    }
    
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <title>Title</title>
    </head>
    <body>
        <a href="hello">hello</a>
    </body>
    </html>
    
    public class UserFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
    
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("Servlet.UserFilter");
            filterChain.doFilter(servletRequest,servletResponse);
        }
    
        @Override
        public void destroy() {
    
        }
    }
    
  • 之后,还是一样的接口实现类,我们用第二个形参便可以注册三大组件

    @HandlesTypes(value = {MyObject.class})
    public class MyServletContainerInitializer implements ServletContainerInitializer {
    
    //    Set<Class<?>> 表示@HandlesTypes注解传入的感兴趣的类型
        @Override
        public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
            for (Class<?> c :
                    set) {
                System.out.println(c);
            }
    //        注册一个servlet
            ServletRegistration.Dynamic userServlet = servletContext.addServlet("helloServlet", HelloServlet.class);
            userServlet.addMapping("/hello");
    //        注册一个监听器
            servletContext.addListener(UserListener.class);
    //        注册一个过滤器
            FilterRegistration.Dynamic userFilter = servletContext.addFilter("userFilter", UserFilter.class);
    //        EnumSet.of(DispatcherType.REQUEST) 拦截类型 Request ,/*代表拦截所有路径
            userFilter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST),true,"/*");
        }
    }
    
  • 启动项目后,ServletContext容器加载,调用监听器init方法,然后对每个请求进行过滤,最后我们点击hello后,跳转页面,调用Servlet程序。

整合SpringMVC

  • 用Servlet3.0版本可以整合SpringMVC和Spring,省去配置文件的操作。

  • 创建web容器

    // web 容器启动的时候会创建对象,调用该类的方法初始化容器和前端控制器
    public class MyWebApp extends AbstractAnnotationConfigDispatcherServletInitializer {
    
    //    获取根容器,相当于Spring的配置类,相当于父容器
        @Override
        protected Class<?>[] getRootConfigClasses() {
            return new Class<?>[]{RootConfig.class};
        }
    
    //    获取web容器,相当于SpringMVC配置文件,相当于子容器
        @Override
        protected Class<?>[] getServletConfigClasses() {
            return new Class<?>[]{AppConfig.class};
        }
    
    //    DispatcherServlet 编写映射信息
    //    / 表示拦截不包含jsp页面之外的请求,包括静态资源(.js,.cs,.png.....)
    //    /* 表示拦截包括jsp页面之内的所有请求,包括静态资源
        @Override
        protected String[] getServletMappings() {
            return new String[]{"/"};
    
        }
    }
    
  • 创建Spring容器和SpringMVC容器

    //SpringMVC config file: only scan the aspect of Controller ,so useDefaultFilters to be false
    //useDefaultFilters: prohibit default filter rule
    @ComponentScan(value = {"com.hyb","config","controller"},
    includeFilters = {
            @ComponentScan.Filter(type= FilterType.ANNOTATION,classes = {Controller.class})
    },useDefaultFilters = false)
    public class AppConfig {
    }
    
    // root container is same to spring config file
    // no scan the aspect of controller
    @ComponentScan(
            value = {"com.hyb","config","controller"},
            excludeFilters = {
                    @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})
            }
    )
    public class RootConfig {
    }
    
  • 创建controller层

    @Controller
    public class HelloController {
    
        @ResponseBody
        @RequestMapping("/helloc")
        public String string(){
            return "hello world";
        }
    }
    
  • 启动项目,在地址栏输入请求地址/helloc 便会发现可以正常请求访问。

  • 注意:在实现该例子的时候,还要导入SpringMVC的整合包

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.3.9</version>
    </dependency>
    
  • 注意:在Spring和SpringMVC两个配置类中,扫描包的时候,最好将包含了web容器,两个配置类,和controller层的包都扫描进来。

定制SpringMVC

  • 注解驱动开发提供了以编程的方式来编写SpringMVC的配置文件。

  • @EnableWebMvc == <mvc:annotation-driven/>
    
  • 下面来演示视图解析器,拦截器和静态资源访问的编程法配置

    @ComponentScan(value = {"com.hyb","config","controller"},
    includeFilters = {
            @ComponentScan.Filter(type= FilterType.ANNOTATION,classes = {Controller.class})
    },useDefaultFilters = false)
    @EnableWebMvc
    public class AppConfig implements WebMvcConfigurer {
    
    //    视图解析器
        @Override
        public void configureViewResolvers(ViewResolverRegistry registry) {
    //        public UrlBasedViewResolverRegistration jsp() {
    //            return this.jsp("/WEB-INF/", ".jsp");
    //        }
    //        registry.jsp();
            registry.jsp("/WEB-INF/view/",".jsp");
        }
    
    //    静态资源的访问
        @Override
        public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
            configurer.enable();
        }
    
    //    拦截器
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(你的拦截器对象).addPathPatterns("/**").excludePathPatterns("/admin/**");
    
        }
    }
    

异步请求

  • 在Servlet 3.0之前,Servlet采用Thread-Per-Request的方式处理请求,即每一次Http请求都由某一个线程从头到尾负责处理,当过来一个请求之后,会从tomcat的线程池中拿出一个线程去处理这个请求,处理完成之后再将该线程归还到线程池。但是线程池的数量是有限的,如果一个请求需要进行IO操作,比如访问数据库(或者调用第三方服务接口等),那么其所对应的线程将同步地等待IO操作完成, 而IO操作是非常慢的,所以此时的线程并不能及时地释放回线程池以供后续使用,在并发量越来越大的情况下,这将带来严重的性能问题。即便是像Spring、Struts这样的高层框架也脱离不了这样的桎梏,因为他们都是建立在Servlet之上的。为了解决这样的问题,Servlet 3.0引入了异步处理,在Servlet 3.1中又引入了非阻塞IO来进一步增强异步处理的性能。

    //asyncSupported = true -> allow asy quest
    @WebServlet(value = "/hello",asyncSupported = true)
    public class HelloServlet extends HttpServlet {
        @Override
        protected void doGet(final HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            System.out.println(Thread.currentThread());
            final AsyncContext asyncContext = req.startAsync();
            asyncContext.start(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(Thread.currentThread());
                        saySleep();
                        asyncContext.complete();
    //                    return data
    //                    1.get asy context
    //                    2. get response or request
                        ServletResponse response = asyncContext.getResponse();
                        response.getWriter().write("hello asy");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
    
        }
    
        public void saySleep() throws Exception {
            Thread.sleep(3_000);
        }
    }
    

    在上述例子可以看出,当有耗时间的处理出现的时候,我们就可以另开一个线程,不占用主线程的事件,而且,每当另开的线程用完会及时关闭,这就使得线程池能够及时处理高并发量。

SpringMVC整合异步请求

返回Callable

/*
* 该程序会执行两次请求,第一次得到的Callable 会返回给TaskExecutor 使用一个隔离的线程进行执行
* 得到返回结果后,SpringMVC将请求重新派发给容器,恢复以前的处理,然后才到视图解析进行解析
* 所以,如果用普通的拦截器去处理异步请求是行不通的,得用原生的异步处理拦截器API:AsyncListener接口
* 或者是SpringMVC 提供的AsyncHandlerInterceptor接口
* */
@RequestMapping("/hellob")
public Callable<String> hellob(){
    System.out.println("Main Thread-->"+Thread.currentThread());
    return new Callable<String>() {
        @Override
        public String call() throws Exception {
            System.out.println("Son Thread"+Thread.currentThread());
            Thread.sleep(2000);
            return "success";
        }
    };
}

模拟消息中间列

  • 在开发中,异步请求不会像返回Callable那么简单。请看下面一张图

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-REaulrDJ-1649379832703)(C:\Users\46894\AppData\Local\Temp\WeChat Files\819a37567a1e6a916c20ee0ce10de44.png)]

    在该图片可以看出,如果我们有一个消息中间件,就可以将一个任务分担给不同的线程执行,这样就可以做到异步的效果。

  • 下面我们来模拟上面图的异步效果,首先,我们得有一个实际的请求,比如,一个创建订单的请求,但这个创建订单的请求我们只是做发出这个动作的,创建并不属于我们

        @ResponseBody
        @RequestMapping("/helloa")
        public DeferredResult<Object> helloa(){
            DeferredResult<Object> deferredResult = new DeferredResult<>();
    //      利用消息中间列将创建订单的消息临时保存起来
            
    //        下面的返回结果是一行字符串,如果没有@ResponseBody ,会被视图解析器解析
            return deferredResult;
        }
    

    可以看到,我们先new了一个对象,表明我们发出了一个创建订单的请求,此刻我们就需要将该消息保存在消息队列中。

  • 所以,我们需要一个消息队列,这里用一个普通的队列来进行演示

    public class DeferredResultQueue {
    
    //    模拟消息中间列,创建一个队列保存信息
        private static final Queue<DeferredResult<Object>> queue=new ConcurrentLinkedDeque<>();
    
    //    保存信息
        public static void save(DeferredResult<Object> deferredResult){
            queue.add(deferredResult);
        }
    
    //    发布信息
        public static DeferredResult<Object> get(){
            return queue.poll();
        }
    }
    
  • 之后,在创造发出消息的对象后,将消息保存

        @ResponseBody
        @RequestMapping("/helloa")
        public DeferredResult<Object> helloa(){
            DeferredResult<Object> deferredResult = new DeferredResult<>();
    //      利用消息中间列将创建订单的消息临时保存起来
            DeferredResultQueue.save(deferredResult);
    //        下面的返回结果是一行字符串,如果没有@ResponseBody ,会被视图解析器解析
            return deferredResult;
        }
    
  • 然后需要另一个线程进行监听,比如又是一个请求

        @ResponseBody
        @RequestMapping("/helloh")
        public String helloh(){
    //        创建订单号
            String uuid = UUID.randomUUID().toString();
    //        从中间列拿到消息对象
            DeferredResult<Object> objectDeferredResult = DeferredResultQueue.get();
    //        用该对象创建订单
            objectDeferredResult.setResult(uuid);
            return uuid;
        }
    

    这个线程会首先从队列里拿到这个消息,然后用这个消息对象来创建真实的订单号。

  • 当我们启动项目的时候,如果直接访问/helloa ,并不会得到结果,只有当/helloh 进行真实的订单创建后,/helloa才会得到真正的订单返回。

举报

相关推荐

0 条评论