Spring 的两大核心:IOC、AOP,IOC 是由扫描器自动发现哪些类需要成为 bean,然后实例化成 bean(只能通过反射了);AOP 是拦截方法调用,进行一些扩展,这里也是用的反射,对代理对象进行增强后返回。
Spring 的简单易用得益于 “约定大于配置”。
问题:
刚接触 Spring 时可能有遇到这个约定——启动类和需要成为 baan 的类要写在同一个包下,比如某个 controller 和启动类不在一个包里,就会发现访问 controller 无效的情况。
分析:
关键点在于 @SpringBootApplication 注解,继承了另外一些注解,具体定义如下:
其中ComponentScan,就是用来扫描定义的 bean 的,它的 basePackages没有指定,所以默认为空,再看org.springframework.context.annotation.ComponentScanAnnotationParser,其中 parse 方法,debug 模式可以看到,当 basePackages 为空时,实际上扫描的是启动类所在的包。
接下来解决问题,当然不只是把 controller 移动到和启动类同一个包下就完事儿,而是应该真正满足需求——让启动类能够扫描到 controller (思路:默认情况下,扫描的是启动类所在的包,想让 controller 被扫描到,一种是让 controller位于启动类所在的包中,指标不治本~ 第二种是指定,而不走默认情况,让启动类能够扫描到 controller )
️ 解决方法:
显式声明启动类扫描包,如:
@ComponentScan("com.example.demo.controller")
(发现 IDEA 还挺智能的~,如果这里指定的是启动类所在的包,会报错:Redundant declaration: @SpringBootApplication already applies given @ComponentScan)
(说回来,启动类没必要像 controller、service、dao、entity 一样各搞一个包,就放在共有的包就好了,让以上都能扫描到)
不过这样一来,原先默认的扫描范围,即 启动类所在的包,就不会被添加进去了,如果想一并生效,可以使用多个 @ComponentScan,也可以使用 @ComponentScans。
问题:
有时候,除了默认的无参构造,我们可能也需要把某些参数传进来定义对象,比如:
@Service
public class ServiceImpl implements DemoService {
private String serviceName;
// 有参构造
public ServiceImpl(String serviceName) {
this.serviceName = serviceName;
}
(其实写出这种代码的下一步,就应该会想到说 怎么给这个参数 serviceName 赋值了,毕竟已经再用 Spring 了,就不像之前无它时,想 new 就 new 随时随地 new 了)
ServiceImpl 类,用了 @Service 注解,也就成了一个 bean,然后使用这个 bean:
@RestController
@RequestMapping("/demo")
public class DemoController {
@Autowired
DemoService demoService;
@RequestMapping("/getScope")
public String getScope(){
return demoService.toString();
}
运行报错:
报错大概是说 ServiceImpl 的构造器需要一个 String 类型的参数,但实际上没找到。
分析:
当创建一个 Bean 时,调用的方法是org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory #createBeanInstance。它主要包含两大基本步骤:寻找构造器和通过反射调用构造器创建实例。 关键代码:
Constructor<?>[] ctors = this.determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors == null && mbd.getResolvedAutowireMode() != 3 && !mbd.hasConstructorArgumentValues() && ObjectUtils.isEmpty(args)) {
ctors = mbd.getPreferredConstructors();
return ctors != null ? this.autowireConstructor(beanName, mbd, ctors, (Object[])null) : this.instantiateBean(beanName, mbd);
} else {
return this.autowireConstructor(beanName, mbd, ctors, args);
}
其中 determineConstructorsFromBeanPostProcessors 用来获取构造器,然后通过 autowireConstructor 方法,带着构造器 ctors 去创建实例,而这一步,不仅需要构造器,还需要构造器对应的参数 args··。 (待验证:这个报错其实也说明 bean默认是没有无参构造的,否则不传有参构造的参数,应该也能创建实例才对 补充:当只有一个有参构造时,是不会调用无参构造的;但是如果有多个有参构造可供调用,Spring 无从选择,将会尝试去调用默认构造;但是默认构造是不存在的,所以这种情况会报错。 )
既然在用 Spring 了,那就不直接显式使用 new 了,只能寻找依赖作为构造器调用参数。
至于这个构造器参数的获取,关键代码见 org.springframework.beans.factory.support.ConstructorResolver#autowireConstructor,其中:
argsHolder = this.createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames, this.getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);
这里的 createArgumentArray 方法,就是用来构建构造器参数的,最终是会到 beanFactotry 中获取到 bean,代码看不出啥名堂,在此就不罗列了。案例中目前没有 serviceName 实例,也就报错了。
️ 解决方法:
在 ServiceImpl 里定义一个能让 Spring 装配给 ServiceImpl 构造器参数的 bean,比如:
@Bean
public String serviceName() {
return "hello";
}
这时又会报新的错误,循环依赖:
这是因为这个写法,会导致 ServiceImpl 的构造,依赖于 ServiceName,而 ServiceName 又需要先有 ServiceImpl ,尴尬了。给挪到 Controller 层也是一样的报错,因为 Controller 依赖了 ServiceImpl ,而 ServiceImpl 的构造,依赖于 ServiceName,而 ServiceName 又需要先有 Controller,循环得更深了:
想要打破循环,重新定义一个类,用 @Component标识,然后再注册 ServiceImpl 即可:
@Component
public class TestComponent {
@Bean
public String serviceName(){
return "hello";
}
}
(待验证:这里方法名使用 getServiceName 也可,而且如果 serviceName() 和 getServiceName()方法同时存在,前者优先,这是什么原理?使用@Bean 作用在方法上,不指定 bean 名称的话,bean的命名规则是怎样的?
@Component
public class ServiceNameClass {
@Bean("serviceName")
public String getServiceName() {
return "hello";
}
@Bean("getServiceName")
public String serviceName() {
return "123";
}
}
结果输出的是 hello,说明 xxx 是比getxxx优先的。包括把两个方法颠倒也不影响的。而如果两个bean名称相同,似乎是哪个 bean 写在前面,就先
只是为什么 getXXX 的 bean 也能做装配,这里不太理解… )
以上是一个有参构造的情况,如果是多个呢:
@Service
public class ServiceImpl {
private String serviceName;
public ServiceImpl(String serviceName){
this.serviceName = serviceName;
}
public ServiceImpl(String serviceName, String otherStringParameter){
this.serviceName = serviceName;
}
}
如果我们仍用非 Spring 的思维去审阅这段代码,可能不会觉得有什么问题,毕竟 String 类型可以自动装配了,无非就是再增加了一个 String 类型的参数otherStringParameter 而已。但是如果了解 Spring 内部是用反射来构建 Bean 的话,就不难发现问题所在:存在两个构造器,都可以调用时,到底应该调用哪个呢?最终 Spring 无从选择,只能尝试去调用默认构造器,而这个默认构造器又不存在,所以测试这个程序它会出错。
问题:
再来看个 Bean 定义不生效的例子,使用原型 Bean:
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceImpl {
}
使用它:
@RestController
public class HelloWorldController {
@Autowired
private ServiceImpl serviceImpl;
@RequestMapping(path = "hi", method = RequestMethod.GET)
public String hi(){
return "helloworld, service is : " + serviceImpl;
};
}
结果发现,不管访问多少次这个路径,访问的结果都是不变的,跟个单例似的,这与它定义为原型 Bean 的 初衷背道而驰。
分析:
当一个属性成员 serviceImpl 声明为 @Autowired 之后,那么在创建 HelloWorldController 这个 Bean 时,会先使用构造器反射出这个实例,然后再装配各个标记为 @Autowired 的属性成员,具体的执行过程,会使用到很多 BeanPostProcessor 来完成工作,其中相关的是 AutowiredAnnotationBeanPostProcessor,它会通过 DefaultListableBeanFactory#findAutowireCandidates 寻找到 ServiceImpl 类型的 Bean,然后赋值给 serviceImpl 成员。【为啥删掉呢,因为这个类里我没找到这个方法:)】 关键步骤见org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.AutowiredMethodElement#inject:
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
if (!this.checkPropertySkipping(pvs)) {
Method method = (Method)this.member;
Object[] arguments;
if (this.cached) {
try {
arguments = this.resolveCachedArguments(beanName);
} catch (NoSuchBeanDefinitionException var8) {
arguments = this.resolveMethodArguments(method, bean, beanName);
}
} else {
arguments = this.resolveMethodArguments(method, bean, beanName);
}
if (arguments != null) {
try {
ReflectionUtils.makeAccessible(method);
method.invoke(bean, arguments);
} catch (InvocationTargetException var7) {
throw var7.getTargetException();
}
}
}
}
可以看到,第一次HelloWorldController自动注入的时候,会通过反射机制设置给对应的 field:serviceImpl,这个 field 的执行只发生一次,所以后续就固定起来了,并不会因为 ServiceImpl 标记了 SCOPE_PROTOTYP 而改变。也就是说,只要程序一启动,HelloWorldController 里的 serviceImpl 是固定的,@Scope并不起作用。
️ 解决方法:
破除这个“固定”的封印即可,使得每次使用时,都会重新获取一次属性,方法如下:
(1)自动注入 Context
定义个 getServiceImpl() 方法,通过方法能够获取到一个新的 ServiceImpl 类实例:
@Autowired
private ApplicationContext applicationContext;
public ServiceImpl getServiceImpl(){
return applicationContext.getBean(ServiceImpl.class);
}
(待验证:尚且不知道applicationContext.getBean的原理)
(2)使用 Lookup 注解
也是定义个 getServiceImpl() 方法,使用 @Lookup 注解:
@Lookup
public ServiceImpl getServiceImpl3(){
return null;
}
使用 @Lookup 注解的方法,具体实现是无所谓的,甚至在方法里打印日志,执行起来也并不会打印,只需知道 使用@Lookup 会使用 CGLIB (CGLIB:对代理对象类生成的 class 文件加载进来,通过修改其字节码生成子类来进行代理)实现动态代理,还有个异曲同工的方法:
(3)使用 scope注解的proxyMode:
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS,
value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceImpl {
}
这样注入到 controller 的 bean 也是代理对象来得,每次都会从beanfactory 里面重新拿来。
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
也还是单例,因为自程序启动后只会装配一次。想到啥记录啥~
在实例方法中有一类特殊的方法,它们一般不包含任何业务逻辑,仅仅是为类成员属性提供读取和修改的方法(really?好像在业务代码里见过夹带私货的…),这样设计的好处:
(1)满足面向对象语言封装的特性。尽可能将属性定义为 private,针对属性值的访问与修改需要使用对应的 getter 和 setter 方法,而不是直接对 publice 的属性进行读取和修改。
(2)有利于统一控制。虽然直接对属性进行读取和修改的方式 和 使用对应的 getter 与 setter 方法在效果上是一样的,但是前者难以应对业务的变化;比如 业务要求对某个值的修改要增加统一的权限控制,如果有 setter 作为统一的属性修改方法,则更容易实现,这种情况在一些使用反射的框架中作用尤其明显。(思考这个问题,可以从方法和直接赋值的区别去考虑,方法有返回值、访问权限控制符、入参,而且方法的存在相当于一个收口,方法声明了之后可以到处使用~)
顺便提一下,同时定义了 isXXX() 和 getXXX() ,在 iBatis、JSON序列化等场景下容易引起冲突,比如 iBatis 通过反射机制解析加载属性的 getter 方法时,首先会获取对象的所有方法,任何筛选出以 get 和 is 开头的方法,并存储到类型为 HashMap 的 getMethods 变量中,其中 key 为 属性名称,value 为 getter 方法,这样的话 isXXX() 和 getXXX() 只能保留一个,哪个方法被存储到 getMethods 变量中,就会保存哪个方法,具有一定的随机性,当两个方法定义不同时,可能导致误用哪个,进而产生问题。(启示:某个字段出问题,特别是 Boolean 类型的,可能就是获取方法有冲突,而实际只保留一个导致的,不失为一个排查思路)
像注册表设置(registry setting)对象、线程池、数据库连接池,这些都是共享的资源,但是只能有一个实例,如果制造出多个实例,就会导致一些问题:资源使用过量、结果不一致等;而且资源也比较珍贵,重用更佳。启示:思考xxx存在的意义这种问题,可以从必要性和优化的角度去考虑,使用xxx是否必要?是否更优?
按说,一个类只存在一个实例,只用 Java 的静态变量(静态变量是类的所有实例共享的,它属于类,不属于任何独立的对象)就能做到,但是如果用静态变量的话,有个缺点:要想把对象赋值给一个静态变量,就得一开始创建好对象,万一创建对象这一步非常耗费资源,而程序在这次执行过程中一直没有执行到它,不就形成浪费了吗,所以单例模式应运而生,需要时才去创建。