概述
本文是Spring第四讲:在使用Spring过程中遇到的问题review
文章目录
- 问题的来源
- 1、坑点1 Spring Bean的定义常见错误
- 案例1:隐式扫描不到Bean的定义
- 案例2:定义的Bean缺少隐式依赖
- 案例3:原型Bean被固定
- 2、坑点2 Spring Bean依赖注入常见错误
- 案例1:required a single bean, but 2 were found
- 案例2:显式引用Bean时首字母忽略大小写
- 案例3:引用内部类的Bean遗忘类名
- 案例4:@Value没有注入预期的值
- 案例5:@Value没有注入预期的值
- 3、坑点3 Spring Bean依赖注入常见错误
- 2 使用Spring AOP实现日志系统 20210322
问题的来源
1、我和同事在生产环境中经常遇到问题;
2、Stack Overflow 网站上的一些高频问题;
3、常用搜索引擎检索到的一些高频问题。
Spring可能产生的问题如图所示:
Spring Core:Spring Core 包括 Bean 定义、注入、AOP 等核心功能,可以说它们是Spring 的基石。不管是做 Spring Web 开发,还是使用 Spring Cloud 技术栈,都绕不开这些功能。重点介绍在这些功能使用上的常见问题。
Spring Web 篇:大多项目使用 Spring 还是为了进行 Web 开发,所以我也梳理了从请求URL 解析、Header 解析、Body 转化到授权等 Web 开发中绕不开的问题。不难发现,它们正好涵盖了从一个请求到来,到响应回去这一完整流程。
Spring 补充篇:这部分会重点介绍 Spring 测试、Spring 事务、SpringData 相关问题。最后,系统总结下 Spring 使用中发生问题的根本原因。
1、坑点1 Spring Bean的定义常见错误
使用@Bean注解的向容器中注册组件,我们团队的瓦力同学就踩过这个坑
- 我们在使用注解方式向Spring的IOC容器中注入JavaBean时,如果没有在@Bean注解中明确指定bean的名称,就使用当前方法的名称(首字母小写)来作为bean的名称;如果在@Bean注解中明确指定了bean的名称,则使用@Bean注解中指定的名称来作为bean的名称。
案例1:隐式扫描不到Bean的定义
背景:使用 Spring Boot 来快速构建Web应用,包结构如下:
然后发现,我们找不到HelloWorldController 这个 Bean 了。
原因: SpringBootApplication注解 —》ComponentScan注解(当 Spring Boot 启动时,ComponentScan的启用意味着会去扫描出所有定义的 Bean)
- 扫描的位置由basePackages属性指定,默认为启动类所在的包
解决方案:显式配置 @ComponentScan。
@SpringBootApplication
// 支持多个包的扫描范围指定
@ComponentScans(value = { @ComponentScan(value ="com.spring.puzzle.class1.example1.controller") })
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
案例2:定义的Bean缺少隐式依赖
背景:把一个类定义成bean
@Service
public class ServiceImpl {
private String serviceName;
public ServiceImpl(String serviceName){
this.serviceName = serviceName;
}
}
报下面这种错误:
Parameter 0 of constructor in com.spring.puzzle. class1.example2. ServiceImpl required a bean of type ‘java.lang.String’ that could not be found.
原因:创建bean是通过寻找构造器和通过反射调用构造器创建实例。
在使用 Spring时,我们不能直接显式使用 new 关键字来创建实例。Spring只能是去寻找依赖来作为构造器调用参数。
我们可以调用 createArgumentArray 方法来构建调用构造器的参数数组,而这个方法的最终实现是从 BeanFactory 中获取 Bean,
根据参数来寻找对应的 Bean,在本案例中,如果找不到对应的Bean 就会抛出异常,提示装配失败。
解决方案:我们定义一个类为 Bean,如果再显式定义了构造器,那么这个 Bean 在构建时,会自动根据构造器参数定义寻找对应的 Bean,然后反射创建出这个 Bean。
@Bean
public String serviceName(){
return "MyServiceName";
}
补充:倘若serviceName的类型变为List,即如下代码所示,还会报错吗?
@Service
public class ServiceImpl {
private List<String> serviceNames;
public ServiceImpl(List<String> serviceNames){
this.serviceNames = serviceNames;
System.out.println(this.serviceNames);
}
}
运行程序会发现这并不会报错,而是输出 []。
原因:我们可以直接定位构建构造器调用参数的代码所在地(即ConstructorResolver #resolveAutowiredArgument)
案例3:原型Bean被固定
背景:在定义 Bean 时,有时候我们会使用原型 Bean(即每次请求生成新的对象),
// bean的定义
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceImpl {
}
// bean的使用
@RestController
public classHelloWorldController{
@Autowired
private ServiceImpl serviceImpl;
@RequestMapping(path = "hi", method = RequestMethod.GET)
public String hi () {
return "helloworld, service is:" + serviceImpl;
}
}
结果,我们会发现,不管我们访问多少次 http://localhost:8080/hi,访问的结果都是不变的。
原因:当一个属性成员 serviceImpl 声明为 @Autowired 后,那么在创建HelloWorldController 这个 Bean 时,会先使用构造器反射出实例,然后来装配各个标记为 @Autowired 的属性成员。
关键执行代码
protected void inject(Object bean, @Nullable String beanName, @Nullable Proper ) {
Field field = (Field) this.member;
Object value;
//寻找“bean”
if(this.cached) {
value = resolvedCachedArgument(beanName, this.cachedFieldValue);
} else {
// 省略其他非关键代码
value = beanFactory.resolveDependency(desc,beanName,autowiredBeanNames, );
}
if(value!=null){
//将bean设置给成员字段
ReflectionUtils.makeAccessible(field);
field.set(bean, value);
}
}
从代码可知:待我们寻找到要自动注入的 Bean 后,即可通过反射设置给对应的 field。这个 field 的执行只发生了一次,所以后续就固定起来了,它并不会因为 ServiceImpl 标记了SCOPE_PROTOTYPE 而改变。
解决方案:
有两种,第一种是自动注入 ApplicationContext
@RestController
public class HelloWorldController {
@Autowired
private ApplicationContext applicationContext;
@RequestMapping(path = "hi", method = RequestMethod.GET)
public String hi(){
return "helloworld,service is:" + getServiceImpl();
}
public ServiceImpl getServiceImpl(){
return applicationContext.getBean(ServiceImpl.class);
}
}
第二种方法是使用 Lookup 注解
添加一个 getServiceImpl 方法,不过这个方法是被 Lookup 标记的。代码如下:
@RestController
public class HelloWorldController {
@RequestMapping(path = "hi", method = RequestMethod.GET)
public String hi() {
return "helloworld, service is: " + getServiceImpl();
}
@Lookup
public ServiceImpl getServiceImpl(){
return null;
}
}
Lookup 是如何生效的?
因为标记了 Lookup 而走入了CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor,这个方法的关键实现参考 LookupOverrideMethodInterceptor#intercept:
private final BeanFactoryowner;
public Object intercept(Object obj, Method method, Object[] args, MethodProxy) {
LookupOverride lo =(LookupOverride)getBeanDefinition().
getMethodOverrides();
Assert.state(lo!= null, "LookupOverride not found");
Object[] argsToUse = (args.length > 0 ? args:null);
//if no-arg,don't
if(StringUtils.hasText(lo.getBeanName())) {
return(argsToUse!=null this.owner.getBean(lo.getBeanName(),argsToU)
this.owner.getBean(lo.getBeanName()));
} else {
return(argsToUse!=null?this.owner.getBean(method.getReturnType(),a)
this.owner.getBean(method.getReturnType()));
}
}
方法调用最终并没有走入案例代码实现的 return null 语句,而是通过BeanFactory来获取 Bean。所以从这点也可以看出,其实在我们的 getServiceImpl 方法实现中,随便怎么写都行,这不太重要。
2、坑点2 Spring Bean依赖注入常见错误
案例1:required a single bean, but 2 were found
背景:我们在开发一个学籍管理系统案例,需要提供一个 API 根据学生的学号(ID)来移除学生,学生的信息维护肯定需要一个数据库来支撑,所以大体上可以实现如下:
@RestController
@Slf4j
@Validated
public class StudentController {
@Autowired
DataService dataService;
@RequestMapping(path = "students/{id}", method = RequestMethod.DELETE)
public void deleteStudent(@PathVariable("id") @Range(min = 1,max= 100) id) {
dataService.deleteStudent(id);
}
}
//DataService 是一个接口,其实现依托于 Oracle
public interface DataService {
void deleteStudent(int id);
}
@Repository
@Slf4j
public class OracleDataService implements DataService{
@Override
public void deleteStudent(int id) {
log.info("delete student info maintained by oracle");
}
}
@Repository
@Slf4j
public class CassandraDataService implements DataService{
@Override
public void deleteStudent(int id) {
log.info("delete student info maintained by cassandra");
}
}
程序启动时,报错信息如下:
原因:需要对@Autowired 实现的依赖注入的原理有一定的了解。
当一个 Bean 被构建时,核心包括两个基本步骤:
1、执行 AbstractAutowireCapableBeanFactory#createBeanInstance 方法:通过构造器
反射构造出这个 Bean,在此案例中相当于构建出 StudentController 的实例;
2、执行 AbstractAutowireCapableBeanFactory#populate 方法:填充(即设置)这个Bean,在本案例中,相当于设置 StudentController 实例中被 @Autowired 标记的dataService属性成员。
- “填充”过程的关键就是执行各种 BeanPostProcessor 处理器(找出合适的 DataService 的 bean 并设置给StudentController#dataService)
- 1、寻找出所有需要依赖注入的字段和方法,
- 2、根据依赖信息寻找出依赖并完成注入,
@Override
protected void inject(Object bean, @Nullable String beanName, @Nullable Property){
Field field =(Field)this.member;
Object value;
//省略非关键代码
try {
DependencyDescriptor desc = new DependencyDescriptor(field,this.req);
//寻找“依赖”,desc为"dataService"的DependencyDescriptor
value = beanFactory.resolveDependency(desc, beanName,autowiredBeanNa); //重点
}
}
//省略非关键代码
if(value!=null){
ReflectionUtils.makeAccessible(field);
//装配“依赖”
field.set(bean,value);
}
}
调试信息如下所示:
我们根据 DataService 这个类型来找出依赖时,我们会找出 2 个依赖,分别为 CassandraDataService 和 OracleDataService。在这样的情况下,如果同时满足以下两个条件则会抛出本案例的错误:
1、调用 determineAutowireCandidate 方法来选出优先级最高的依赖,但是发现并没有优先级可依据。
- 优先级的决策是先根据 @Primary 来决策,其次是 @Priority 决策,最后是根据 Bean 名字的严格匹配来决策。
2、@Autowired 要求是必须注入的(即 required 保持默认值为 true),或者注解的属性类型并不是可以接受多个 Bean 的类型,例如数组、Map、集合。
解决方案:打破上述两个条件中的任何一个即可,即让候选项具有优先级或压根可以不去选择。
要同时支持多种 DataService,且能在不同业务情景下精确匹配到要选择到的 DataService,我们可以使用下面的方式去修改:
// 根据 Bean 名字的严格匹配来决策
@Autowired
DataService oracleDataService;
- 修改方式的精髓在于将属性名和 Bean 名字精确匹配,这样就可以让注入选择不犯难:需要 Oracle 时指定属性名为 oracleDataService,需要 Cassandra 时则指定属性名为 cassandraDataService。
解决方案2:即采用 @Qualifier 来显式指定引用的是那种服务,例如采用下面的方式:
@Autowired
@Qualifier("cassandraDataService")
DataService dataService;
Bean 只有一个(即精确匹配),所以压根不会出现后面的决策过程。
案例2:显式引用Bean时首字母忽略大小写
背景:使用 @Qualifier 时,我们有时候会犯另一个经典的小错误,就是我们可能会忽略Bean 的名称首字母大小写。
@Autowired
@Qualifier("CassandraDataService")
DataService dataService;
运行程序,会报错如下:
Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name ‘studentController’: Unsatisfied dependency expressed through field ‘dataService’; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No
qualifying bean of type ‘com.spring.puzzle.class2.example2.DataService’ available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true), @org.springframework.beans.factory.annotation.Qualifier(value=CassandraDataService)}
原因:对于 Bean 的名字,如果没有显式指明,就应该是类名,不过首字母应该小写。
@Qualifier注解解析的大体流程只有两步:看 Bean 有没有显式指明名称,如果有则用显式名称,如果没有则产生一个默认名称。很明显,在我们的案例中,是没有给 Bean 指定名字的,所以产生的Bean 的名称就是生成的默认名称,查看默认名的产生方buildDefaultBeanName。
protected String buildDefaultBeanName(BeanDefinition definition) {
String beanClassName = definition.getBeanClassName();
Assert.state(beanClassName!=null,"No bean classname set");
String shortClassName = ClassUtils.getShortName(beanClassName);
return Introspector.decapitalize(shortClassName);
}
public static String decapitalize(String name) {
if( name==null||name.length()==0){
return name;
}
if(name.length()>1 && Character.isUpperCase(name.charAt(1))&& Character.isUpperCase(name.charAt(0))){
return name;
}
char chars[] = name.toCharArray();
chars[0] = Character.toLowerCase(chars[0]);
return new String(chars);
}
结论:如果一个类名是以两个大写字母开头的,则首字母不变,其它情况下默认首字母变成小写。
SQLiteDataService 的 Bean,其名称应该就是类名本身,而 CassandraDataService 的Bean 名称则变成了首字母小写(cassandraDataService)
解决方案:
方案1:引用处纠正首字母大小写问题:
- 倘若熟悉源码,使用这种方式
@Autowired
@Qualifier("cassandraDataService")
DataService dataService;
方案2:定义处显式指定 Bean 名字,我们可以保持引用代码不变,而通过显式指明CassandraDataService 的 Bean 名称为 CassandraDataService 来纠正这个问题。
- 倘若不想纠结于首字母到底是大写还是小写,使用这种方式避免困扰
@Repository("CassandraDataService")
@Slf4j
public class CassandraDataService implements DataService {
//省略实现
}
案例3:引用内部类的Bean遗忘类名
背景:需要定义一个内部类来实现一种新的 DataService
public class StudentController {
@Repository
public static class InnerClassDataService implements DataService{
@Override
public void deleteStudent(int id) {
//空实现
}
}
//省略其他非关键代码
}
// 显示引用该bean
@Autowired
@Qualifier("innerClassDataService")
DataService innerClassDataService;
报错“找不到 Bean”。
原因:
Spring对 class 名字的处理,
public static String getShortName(String className) {
Assert.hasLength(className,"Class name must not be empty");
int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR);
int nameEndIndex = className.indexOf(CGLIB_CLASS_SEPARATOR);
if(nameEndIndex==-1){
nameEndIndex=className.length();
}
String shortName = className.substring(lastDotIndex + 1, nameEndIndex);
shortName = shortName.replace(INNER_CLASS_SEPARATOR,PACKAGE_SEPARATOR);
return shortName;
}
在经过这个方法的处理后,我们得到的其实是下面这个名称:StudentController.InnerClassDataService
最后经过 Introspector.decapitalize 的首字母变换,最终获取的 Bean 名称如下:studentController.InnerClassDataService
解决方案:
@Autowired
@Qualifier("studentController.InnerClassDataService")
DataService innerClassDataService;
案例4:@Value没有注入预期的值
背景:
原因:
解决方案:
案例5:@Value没有注入预期的值
背景:
原因:
解决方案:
3、坑点3 Spring Bean依赖注入常见错误
背景:
原因:
解决方案:
2 使用Spring AOP实现日志系统 20210322
该日志系统使用的技术有
1、Spring AOP 用于在指定的方法执行前后做一些额外的逻辑处理
2、RocketMQ 用来解耦日志的生产和消费逻辑,防止由于引入日志而打挂系统
3、Apollo 用作配置中心,起是否记录日志的开关作用
遇到的问题
1、在切入点的后置处理逻辑中,应当使用@AfterReturning注解
@AfterReturning与@After的区别
- @AfterReturning 返回通知方法 连接点方法成功执行后,返回通知方法才会执行,如果连接点方法出现异常,则返回通知方法不执行;
- @After 后置通知方法 后置方法在连接点方法完成之后执行,无论连接点方法执行成功还是出现异常,都将执行后置方法
而且在使用@AfterReturning时,不要使用try catch捕获异常,不然,返回通知方法还是会执行。
关于AspectJ切面注解中的五种通知注解
- @Before 前置通知,在方法执行之前执行
- @After 后置通知,在方法执行之后执行
- @AfterReturning 返回通知,在方法有返回结果后执行
- returning 能够将目标方法的返回值传到切面增强方法里
@AfterReturning(pointcut = "testAfterReturing()",returning = "rvt")
public void logTestAfterReturing4(String rvt){
System.out.println("测试AfterReturning---returning:"+str);
}
- @AfterThrowing 异常通知,在方法抛出异常后执行
- @Around 环绕通知,围绕着方法执行
具体可以看这篇文章:https://blog.csdn.net/u010502101/article/details/78823056
最后
以上就是坚定彩虹为你收集整理的Spring第四讲:使用Spring过程中遇到的问题的全部内容,希望文章能够帮你解决Spring第四讲:使用Spring过程中遇到的问题所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复