文章目录
建议先学
这已经算是高级部分了,如果你还没学习SSM这最基本的三大框架,还是赶紧如学上吧,并且做个增删改查的小项目
- 五万字的Spring5学习笔记,带你熟悉运用Spring5
- 四万多字的SpringMVC学习总结,带你领略不一样的SpringMVC
- 怎么请求数据库的数据?这套四万多字的Mybatis学习笔记给你答案,只做入门,不做深层次分析
- 基于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才会得到真正的订单返回。