day60( 关于框架 , 关于Spring框架,通过Spring管理对象,自动装配, Ioc与DI,@Qualifier ,构造方法,@Autowired的警告 ,@Resource注解 )
1.关于框架
1.概念:
-
框架(Framework)一个框架是一个可复用的设计构件,它规定了应用的体系结构,阐明了整个设计、协作构件之间的依赖关系、责任分配和控制流程,表现为一组抽象类以及其实例之间协作的方法,它为构件复用提供了上下文(Context)关系。
-
应用框架指的是实现了某应用领域通用完备功能(除去特殊应用的部分)的底层服务。使用这种框架的编程人员可以在一个通用功能已经实现的基础上开始具体的系统开发。框架提供了所有应用期望的默认行为的类集合。具体的应用通过重写子类(该子类属于框架的默认行为)或组装对象来支持应用专用的行为。
-
你可以将框架理解为现实生活中的“毛胚房”,它已经完成了住房最基础部分的设计,例如打地基、设计住房的基本格局、预留电路、水路的线路接入等……当你使用一个框架时,就相当于得到了一间毛胚房,如果你想住进去,你需要做的事情主要是“装修”,把这个毛胚房加工成你希望的样子。
2.关于Spring框架
1.作用
-
Spring框架主要解决了创建对象、管理对象的问题。
-
在开发实践中,Spring框架的核心价值在于:开发者可以通过Spring框架提供的机制,将创建对象、管理对象的任务交给Spring来完成,以至于开发者不必再关心这些过程,当需要某个对象时,只需要通过Spring获取对象即可。
2.创建对象
-
其实,创建对象并不是一个复杂的任务,假设项目中存在类:
public class UserMapper {
public void insert() {
// 向数据表中的“用户表”中插入数据
}
} -
创建对象的语法是极为简单的,例如:
UserMapper userMapper = new UserMapper();
-
但是,在开发实践中,类与类之间是存在依赖关系的,例如: UserController依赖UserMapper
public class UserController {
public UserMapper userMapper;
public void reg() {
userMapper.insert();
}
}-
以上userMapper属性如果不赋值,则程序将无法正确运行;
-
在整个项目运行过程中,UserMapper对象只需要1个且始终存在即可;
-
如果自行创建对象,当多个类都依赖UserMapper时,各自创建UserMapper对象,违背了以上“只需要1个”的思想。
-
-
在开发实践中,有许多类型的对象、配置值都需要常驻内存、需要有唯一性,或都需要多处使用,自行维护这些对象或值是非常繁琐的,通过Spring框架可以极大的简化这些操作。
3. 在Maven工程中使用Spring
-
当某个项目需要使用Spring框架时,推荐使用Maven工程。
-
使用Spring框架所需的依赖项是 spring-context,依赖代码为: 代码中版本号可按需调整
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.14</version>
</dependency>
3.通过Spring管理对象
1.创建对象的方式
-
如果需要Spring管理对象,就必须先创建对象,然后Spring获取到对象才可以进行管理
-
被Spring管理的对象,通常也称之为Spring Bean
-
创建对象的方式有2种:
-
通过@Bean方法
-
通过组件扫描
-
2.通过@Bean方法
-
编码步骤:
-
创建cn.tedu.spring.Springbeanfactory类
-
在类中添加方法,方法的返回值类型就是你希望Spring创建并管理的对象的类型,并在此方法中自行编写返回有效对象的代码 – 在此类上添加@Configuration注解
-
示例代码:
package cn.tedu.spring;
@Configuration
public class Springbeanfactory {
@Bean
public Random random() {
return new Random();
}
} -
测试运行的编码步骤:
-
测试运行:
public class springrunner {
public static void main(String[] args) {
// 1. 加载Spring
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext(Springbeanfactory.class);
// 2. 从Spring中获取对象
Random random = (Random) ac.getBean("random");
// 3. 测试使用对象,以便于观察是否获取到了有效的对象
System.out.println("random > " + random);
// 4. 关闭
ac.close();
}
} -
关于以上代码,你需要知道
-
在AnnotationConfigApplicationContext的构造方法中,应该将Springbeanfactory.class作为参数传入,否则就不会加载Springbeanfactory类中内容
-
实际上,在以上案例中,Springbeanfactory类上的@Configuration注解并不是必须的,但@Bean方法的使用规范是将其声明在@Configuration类中
-
-
在getBean()时,传入的字符串参数"random"是Springbeanfactory类中的@Bean方法的名称
-
在Springbeanfactory类中的方法必须添加@Bean注解,其作用是使得Spring框架自动调用此方法,并管理此方法返回的结果
-
-
示例:配置@Bean注解参数以指定beanName :获取对象时,需使用random作为beanName,而不是 xxx 作为beanName
package cn.tedu.spring;
@Configuration
public class Springbeanfactory {
@Bean("random")
public Random xxx() {
return new Random();
}
}
3.通过组件扫描创建对象
-
编码步骤:
-
自行创建某个类,例如创建cn.tedu.spring.UserMapper类,并在类的声明之前添加@Component注解
-
示例代码:
package cn.tedu.spring;
@Component
public class UserMapper {
}
-
-
自行创建某个类,例如创建cn.tedu.spring.SpringConfig类,并在类的声明之前添加2个注解:
-
@Configuration
-
@ComponentScan,且在注解参数指定包名为cn.tedu.spring
-
示例代码:
package cn.tedu.spring;
@Configuration
@ComponentScan("cn.tedu.spring")
public class SpringConfig {
}
-
-
-
测试运行的编码步骤:
-
测试运行:
public class springrunner {
public static void main(String[] args) {
// 1. 加载Spring
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext(SpringConfig.class);
// 2. 从Spring中获取对象
UserMapper userMapper = ac.getBean("userMapper", UserMapper.class);
// 3. 测试使用对象,以便于观察是否获取到了有效的对象
System.out.println("userMapper > " + userMapper);
// 4. 关闭
ac.close();
}
} -
关于以上代码,你需要知道
-
使用@ComponentScan配置的是执行组件扫描的根包,当创建AnnotationConfigApplicationContext对象时,由于传的SpringConfig添加了此注解,则Spring框架会扫描所配置的包,如果包中有组件类,Spring框架就会创建组件类的对象并管理
-
UserMapper类必须在@ComponentScan注解配置的包中,否则Spring框架不会知道此类的存在
-
在UserMapper上的@Component表示此类是个“组件”,如果无此注解,Spring框架不会创建此类的对象
-
在@ComponentScan中配置的包是执行组件扫描的“根包(basePackage)”,在执行时,会扫描此包及其下所有子孙包,例如配置为cn.tedu时,如果存在cn.tedu.spring、cn.tedu.mybatis、cn.tedu.boot、cn.tedu.boot.mapper、cn.tedu.boot.controller等包,则这些包都会被扫描
-
你甚至可以把根包配置为cn,也可以完全扫描到以上列举的包,但并不推荐这么做,毕竟你的开发环境中的其它库中的类也是项目的一部分,例如依赖的第三方框架或工具等,如果这些框架或工具的包名的第1级也是cn,也会被扫描到,尽管不确实是否会导致意外的问题,但这种做法肯定是不对的
-
-
当getBean()时,由Spring创建的组件类的对象,默认的beanName都是将首字母改为小写
-
以上规则仅适用于:类名中的第1个字母是大写,且第2个字母是小写的情况,如果类名不符合这种情况,则getBean()时传入的名称就是类名(与类名完全相同的字符串)
-
-
你可以配置@Component注解的参数以指定beanName
-
示例:配置@Component注解参数以指定beanName :
-
获取对象时,需使用 userMapperBean 作为beanName,而不是 userMapper 作为beanName
package cn.tedu.spring;
@Component("userMapperBean")
public class UserMapper {
} -
-
-
4.配置注解属性
-
关于@ComponentScan,其源代码片段有:
-
关于注解的配置语法:
-
-
关于注解的配置语法:
-
-
在配置注解属性时,value是默认的属性,当注解只需要配置value这1个属性的值时,可以缺省,例如以下2种语法是完全等效的:
@ComponentScan("cn.tedu.spring")
@ComponentScan(value = "cn.tedu.spring")
-
在配置注解属性时,如果属性类型是数组类型的,却只需要配置1个值,可以不使用大括号框住属性值,例如以下2种语法是完全等效的:
@ComponentScan("cn.tedu.spring")
@ComponentScan({"cn.tedu.spring"})
5.配件注解
-
除了@Component以外,在Spring框架中还可以使用@Repository、@Service、@Controller表示某个类是组件类
-
这4个注解选择其中1个使用即可,例如以下2个代码片段是等效的:
package cn.tedu.spring;
@Component
public class UserMapper {
}package cn.tedu.spring;
@Repository
public class UserMapper {} -
@Repository、@Service、@Controller使用@Component作为元注解(Meta Annotation),并且,这3个注解的value属性都等效于@Component的value属性,通过源代码可以看出这些特点,以@Repository为例 :
-
在Spring框架的解释范围内,组件注解的用法及作用表现是完全相同的,但字面语义不同,在开发实践中,应该根据类的定位进行选取:–
6.选择创建对象的方式
-
通过@Bean方法:需要在配置类中添加@Bean方法,需要Spring管理的对象越多,则需要添加的@Bean方法就越多,虽然每个方法的代码并不复杂,但是当方法的数量到一定程度后也比较繁琐,不易于管理,这种做法的优点是可以完全自定义对象的创建过程,在@Bean方法内部仍是传统的创建对象的语句–
-
通过组件扫描:只需要配置1次组件扫描,然后各组件类添加组件即可,且各组件类添加组件注解后也可增强语义,所以,无论编码成本还是代码的可读性都更好,这种做法的不足在于“只能适用于自定义的类” ,毕竟你不可以在引用的库中的类上添加组件注解
7.Spring Bean的作用域
-
在默认情况下,由Spring Bean的作用域是单例的
-
单例的表现为:实例唯一,即在任意时刻每个类的对象最多只有1个,并且,当对象创建出来之后,将常驻内存,直至Spring将其销毁(通常是ApplicationContext调用了销毁方法,或程序运行结束)
-
注意:这与设计模式中的单例模式无关,只是作用域的表现完全相同
-
@Scope注解的scopeName属性决定了作用域,此属性与value是互相等效的,所以,通常配置为value属性即可
-
当需要将Spring Bean的作用域改为“非单例的”,可以:
@Configuration
public class Springbeanfactory {
@Scope("prototype")
@Bean
public Random random() {
return new Random();
}
}@Scope("prototype")
@Repository
public class UserMapper {
} -
你可以反复通过getBean()获取对象,会发现各对象的hashCode()返回的结果都不相同
-
如果hashCode()没有被恶意重写,不同对象的hashCode()必然不同
-
-
由于不配置@Scope,与配置为@Scope("singleton")是等效的,所以,仅当需要将Spring Bean的作用域改为“非单例的”,才会添加配置为@Scope("prototype")
-
在默认情况下,单例的Spring Bean是预加载的,必要的话,也可以将其配置为懒加载的
-
如果某个对象本身不是单例的,则不在此讨论范围之内
-
-
预加载的表现为:加载Spring环境时就会创建对象,即加载Spring配置的环节,会创建对象
-
懒加载的表现为:加载Spring环境时并不会创建对象,而是在第1次获取对象的那一刻再创建对象
-
可以通过@Lazy注解来配置懒加载 –
-
@Lazy注解的value属性是boolean类型的,表示“是否懒加载
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.ParaMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Lazy {
boolean value() default true;
} -
两种加载优点:
8. 创建对象的小结
-
创建对象的方式有2种:
-
使用组件扫描时,在@ComponentScan中指定的包是扫描的根包,其子孙包中的类都会被扫描,通常,指定的包不需要特别精准,但也不宜过于粗糙,你应该事先规划出项目的根包并配置在组件扫描中,且保证自定义的每个组件类都在此包或其子孙包中
-
在Spring框架的解释范围内,@Component、@Repository、@Service、@Controller的作用是完全相同的,但语义不同,应该根据类的定位进行选取
-
@Configuration是特殊的组件注解,Spring会通过代理模式来处理,此注解应该仅用于配置类
-
使用组件扫描时,beanName默认是将类名首字母改为小写的名称(除非类名不符合首字母大写、第2字母小写的规律),也可以在@Component或其它组件注解中配置参数来指定beanName
-
Spring Bean的作用域默认是预加载的单例的,可以通过@Scope("prototype")配置为“非单例的”,在单例的前提下,可以通过@Lazy配置为“懒加载的” ,通常,保持为默认即可
-
关于配置注解参数:
4.自动装配
1.自动装配机制
-
Spring的自动装配机制表现为:当某个量需要被赋值时,可以使用特定的语法,使得Spring尝试从容器找到合适的值,并自动完成赋值
-
代码:
-
SpringConfig
package cn.tedu.spring;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("cn.tedu.spring")
public class SpringConfig {
} -
UserMapper
package cn.tedu.spring;
import org.springframework.stereotype.Repository;
@Repository
public class UserMapper {
public void insert() {
System.out.println("UserMapper.insert() >> 将用户数据写入到数据库中……");
}
} -
UserController
package cn.tedu.spring;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
@Autowired // 注意:此处使用了自动装配的注解
private UserMapper userMapper;
public void reg() {
System.out.println("UserController.reg() >> 控制器即将执行用户注册……");
userMapper.insert(); // 注意:此处调用了userMapper属性的方法
}
} -
public class springrunner {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext(SpringConfig.class);
UserController userController = ac.getBean("userController", UserController.class);userController.reg();
ac.close();
}
}
-
代码解析:
-
– 在main()方法中,由于加载了SpringConfig类,根据SpringConfig上配置的@ComponentScan,将执行组件扫描
-
由于UserMapper和UserController都在组件扫描的包范围内,所以Spring框架会自动调用它们的构造方法以创建对象,并把对象保管在spring容器中
-
由于UserController中的userMapper属性添加了@Autowired注解,所以Spring框架会尝试为此属性注入值,且由于在spring容器中存在UserMapper对象,则可以成功注入,使得userMapper属性是有值的
-
最终userController调用reg()方法时,实现过程中还通过userMapper调用了insert()方法,整个执行过程中不会出错,在控制台可以看到对应的输出文本
-
-
除了对属性装配以外,Spring的自动装配机制还可以表现为:如果某个方法是由Spring框架自动调用的(通常是构造方法,或@Bean方法),当这个方法被声明了参数时,Spring框架也会自动的尝试从容器找到匹配的对象,用于调用此方法
-
代码:
@Configuration
public class SpringConfig {
@Bean
public UserMapper userMapper() {
return new UserMapper();
}
@Bean
public UserController userController(UserMapper userMapper) {
UserController userController = new UserController();
userController.userMapper = userMapper;
return userController;
}
}
-
2.@Autowired的装配机制
3.小结
-
※ 当某个属性需要被注入值,且你肯定此值存在于spring容器中,你可以在属性上添加@Autowired注解,则Spring框架会自动为此属性注入值
-
※ 如果某个方法是由Spring调用的,当方法体中需要某个值,且你肯定此值存在于spring容器中,你可以将其声明为方法的参数,则Spring框架会自动从容器中找到此值并且于调用此方法,如果声明了多个这样的参数,各参数的先后顺序是不重要的
-
@Autowired的装配机制的表现是可以根据类型实现装配,并且,当匹配类型的Bean有多个时,还可以根据名称进行匹配,从而实现装配,你需要熟记具体装配机制
5.其它
6.课后阅读
1. IoC
-
IoC(Inversion of Control:控制反转)是Spring框架的核心,在传统的开发模式下,是由开发者创建对象、为对象的属性赋值、管理对象的作用域和生命周期等,所以,是开发者拥有“控制权”,当使用了Spring之后,这些都交给Spring框架去完成了,开发者不必关心这些操作的具体实现,所以,称之为“控制反转
-
无论是创建对象,还是自动装配等等,只要是Spring创建并管理对象的操作,都可以称之为Spring IoC的过程
2.DI
-
DI(Dependency Injection:依赖注入)是Spring框架实现IoC的核心实现,当某个类中声明了另一个类的属性(例如在UserController类中声明了UserMapper类型的属性),则称之为依赖(即UserController依赖了UserMapper),Spring框架会帮你完成依赖项的赋值,只是你在你的代码中看不到赋值过程或赋值符号,所以称之为注入
3.Ioc与DI关系
-
Spring通过DI实现了IoC,所以,IoC是一种目标,而DI是实现此目标的重要手段
4. 关于@Qualifier
-
@Qualifier注解是在自动装配机制中,用于指定beanName的注解
-
通常是因为存在多个类型匹配的Bean,但是所有Bean的beanName与被装配的属性名称/参数名称都不匹配,且不想修改beanName也不想修改属性名称/参数名称,则可以通过@Qualifier指定beanName
-
这种现象在开发实践中并不多见
-
-
@Controller
public class UserController {
@Autowired
@Qualifier("userMapper")
private UserMapper xxx;
public void reg() {
System.out.println("UserController.reg() >> 控制器即将执行用户注册……");
xxx.insert();
}
} -
@Configuration
public class SpringConfig {
@Bean
public UserMapper userMapper() {
return new UserMapper();
}
@Bean
public UserController userController(@Qualifier("userMapper") UserMapper xxx) {
UserController userController = new UserController();
userController.userMapper = xxx;
return userController;
}
}
5. 关于构造方法
-
如果类中有多个构造方法,默认情况下,Spring会自动调用无参数构造方法(如果存在的话),如果某个构造方法添加了@Autowired,则Spring会自动调用添加了此注解的构造方法
-
被Spring调用的构造方法是允许有参数的,作为开发者,你需要保证Spring可以利用自动装配机制为参数注入值(你需要保证spring容器中存在匹配的值),否则会导致NoSuchBeanDeFinitionException
6. 关于@Resource注解
-
@Resource注解是javax.annotation包中的
-
如果某属性添加了@Resource注解,Spring也可以实现自动装配
@Controller
public class UserController {
@Resource
private UserMapper userMapper;
public void reg() {
System.out.println("UserController.reg() >> 控制器即将执行用户注册……");
userMapper.insert();
}
} -
@Resource注解的装配机制是:先尝试根据名称进行装配(即:要求属性名称与beanName相同),如果失败,则尝试根据类型装配,如果不存在类型的Bean,则抛出NoSuchBeanDeFinitionException,如果只有1个匹配类型的Bean,则装配成功,如果匹配类型的Bean超过1个,则抛出NoUniqueBeanDeFinitionException
-
在开发实践中,绝大部分类型的Bean都只有1个,无论是@Autowired还是@Resource,当匹配类型的Bean有且仅有1个时,都可以成功装配,所以,在绝大部分情况下,这2个注解的装配机制的差异对于开发人员来说是无感的
-
当需要讨论@Autowired与@Resource的区别时,除了这2个注解所在的包不同、装配机制不同以外,还存在以下区别:
-
综合来看,由于@Autowired是Spring框架专门定制的注解,且@Autowired可以添加在构造方法上,相比@Resource有更多的应用场景(虽然不一定真的需要这样用),所以,在开发实践中,当需要使用注解显式的表示自动装配时,推荐优先使用@Autowired
7. 关于@Autowired的警告
-
建议的解决方案是这样的:
@Controller
public class UserController {
private UserMapper userMapper;
public UserController(UserMapper userMapper) {
this.userMapper = userMapper;
}
public void reg() {
System.out.println("UserController.reg() >> 控制器即将执行用户注册……");
System.out.println(userMapper.getClass().getName());
userMapper.insert();
}
} -
使用构造方法虽然安全,不会出现NPE,但是,当需要被赋值的属性的数量出现增或减时,都需要调整构造方法,当属性数量较多时,构造方法的参数也会变多,这些都是在开发实践中非常现实的问题
-
由于字段注入的NPE问题只是开发阶段人为错误使用导致的,出现概率非常低、可控、容易解决,且字段注入的语法简洁、直观,所以,在开发实践中,字段注入仍是最常见的用法