目录
关于Spring框架
Spring框架创建对象
Spring框架创建对象的方式之一--组件扫描
Spring框架创建对象的方式之二--@Bean方法
Spring框架创建对象的方式的选取
Spring Bean的名称
Spring Bean的作用域
Spring Bean的生命周期
Spring的自动装配
关于为属性注入值的做法
关于IoC与DI
关于Spring AOP
关于Spring框架
Spring框架主要解决了创建对象、管理对象的相关问题。
创建对象比如说之前的controller,我们都没有去new过对象,但可以调用里面的方法区处理请求。还有Impl实现类、jwt过滤器、配置类、全局异常处理器这些也都没有去new,但它们的方法都是正常执行的,这就是因为spring框架去创建了对象。
Spring框架的核心有:Spring IOC、Spring AOP。
Spring框架创建对象
为什么要让spring创建对象?
所有被Spring创建出来的对象都可以称之为:Spring Bean。
所有Spring Bean都会存在于Spring的ApplicationContext
中,由于Spring框架会维护这个ApplicationContext
容器,所以,Spring框架也通常被称之为“Spring容器”。
所有Spring Bean都可以被自动装配。用spring创建对象就可以自动装配。
怎么让spring框架去创建的对象?
Spring框架创建对象的方式之一--组件扫描
需要在配置类上添加@ComponentScan
注解,以开启组件扫描,则Spring框架会自动扫描添加此注解的类所在的包及其子孙包,检查其中是否包含“组件类”,如果存在,则自动创建出这些“组件类”的对象!
在Spring Boot项目中,默认就开启了组件扫描,因为Spring Boot项目中的启动类上添加了@SpringBootApplication
注解,此注解包含了@ComponentScan
。
提示:可以在@ComponentScan
注解上配置参数,以显式的指定扫描哪些包,这样就只扫描这两个包及子孙包,例如:@ComponentScan({"cn.tedu.csmall.product.controller", "cn.tedu.csmall.product.service"})
组件扫描发上在启动过程中,并不是在运行过程中。按理说因该显示的指定扫描哪些包以防止对不必要的包进行扫描影响效率,比如pojo包,但实际情况是往往不显示的指定,因为只影响启动的时候的一秒半秒,对运行不影响。
所有被添加了@Component
注解的类,都是“组件类”,反之,则不是!
在Spring框架中,还有一些基于@Component
注解的组合注解,添加这些注解,也能将类标记为“组件类”,包括:
-
@Repository
:添加在存储库类(用于访问数据的类)上 -
@Service
:添加在业务类上 -
@Controller
:添加在控制器类上 -
@Configuration
以上注解,除了@Configuration
以外,各组件注解的用法、功能是完全相同的,只是语义不同!
在Spring MVC框架中,也有一些基于@Component
的组件注解,包括:
-
@RestController
-
@ControllerAdvice
-
@RestControllerAdvice
Spring框架创建对象的方式之二--@Bean方法
在任何配置类中,可以自定义方法,并在方法上添加@Bean
注解,则Spring框架会自动调用此方法,并获取此方法返回的对象,将此方法放在Spring容器中。
Spring框架创建对象的方式的选取
对于自定义的类,建议优先使用组件扫描的做法;对于非定义的类,只能使用@Bean
方法的做法!
Spring Bean的名称
当使用组件扫描的方式来创建Spring Bean时,默认情况下,如果类名的第1个字母是大写且第2个字母是小写的,Spring Bean的名称就是将类名的首字母改为小写的名称,如果不满足此条件,则Spring Bean的名称就是类名,例如:AdminController
类的Spring Bean的名称默认是adminController
,ABController
类的Spring Bean的名称默认是ABController
。
可以通过组件注解的参数来指定Spring Bean的名称,例如:
@Controller("controller")
public class AdminController {}
所有的组件注解都可以使用以上方式来指定Spring Bean的名称。
当使用@Bean
方法的方式来创建Spring Bean时,默认情况下,Spring Bean的名称就是方法的名称,也可以配置@Bean
注解的value
属性来指定名称。
Spring Bean的作用域
默认情况下,Spring Bean都是“单例”的,可以通过@Scope("prototype")
将其配置为“非单例”的。
单例就可以在controller中不管是添加请求还是查询请求用的都是同一个controller,单例有一个特点是常驻内存的,一旦创建出来,直到程序结束才结束。如果不是单例的,在第一次添加相册的时候帮你new一个controller,用完之后销毁了,在第二次添加相册的时候有帮你new一个controller,用完之后又销毁了,这样就没有必要。有一点,如果你在单例的比如配置类里面自己new了一个对象,因为配置类是单例的,这个new对象间接也会是单例的效果。
如果使用组件扫描的方式来创建对象,则在类上添加
@Scope("prototype")
注解;如果使用
@Bean
方法的方式来创建对象,则在方法上添加@Scope("prototype")
注解。
默认情况下,单例的Spring Bean是“预加载”的(相当于单例模式中的饿汉式模式),在启动的时候就会创建出对象,可以通过@Lazy
将其配置为“懒加载”的(相当于单例模式中的懒汉式模式),懒汉式是在什么时候需要什么时候创建。
如果使用组件扫描的方式来创建对象,则在类上添加
@Lazy
注解;如果使用
@Bean
方法的方式来创建对象,则在方法上添加@Lazy
注解。
至于是预加载好还是懒加载好?预加载好。在预加载的情况下,所有的组件都是在准备就绪的状态下,当程序在很忙的时候,也不会因为几个关键对象没有创建而去创建,从而导致卡顿。就算是启动会慢一点,但是运行会很顺畅。那么预加载相对于懒加载带来的资源浪费怎么办呢?这是无解的,如果一个程序启动30天,就算用懒加载在使用第10天的时候创建了对象,由于单例的特性,也会有20天的浪费。所以资源浪费是无解的,同时因为现在内存都比较大,不在乎创建的这几个对象,所以预加载是比较划算的做法。
注意:Spring管理的对象的表现可能是单例的(根据你是否修改了默认的配置),但并没有使用单例模式来实现,所以,不要将其与单例模式划等号。
错误的说法:Spring使用单例模式来管理对象的作用域
错误的说法:Spring就是单例模式的
Spring Bean的生命周期
Spring框架允许你在组件类中自定义初始化方法和销毁方法,这2个方法应该是:
-
公有的访问权限
-
使用
void
作为返回值类型 -
方法的名称是自定义的
-
参数列表为空
在初始化方法上添加@PostConstruct
注解,则此方法会在构造方法之后自动被调用;在销毁方法上添加@PreDestroy
注解,则此方法会在对象被销毁之前的一刻自动被调用。
如果使用@Bean
方法的方式来创建对象,则在@Bean
注解中通过initMethod
属性来配置初始化方法的名称,通过destroyMethod
属性来配置销毁方法的名称。
Spring的自动装配
Spring的自动装配表现为:当某个组件类的属性需要值时,或被Spring自动调用的方法的参数需要值时,Spring框架可以自动的从容器中找到合适的值。
典型表现为:
@RestController
public class AdminController {
@Autowired // 自动装配
private IAdminService adminService; // AdminServiceImpl类的对象
}
关于合适的值:类型匹配的Spring Bean,或当存在多个类型匹配的Spring Bean时,也要考虑Spring Bean的名称。
例如:需要装配private IAdminService adminService;
属性,并且,IAdminService
有2个实现类都是被Spring管理对象的,默认的Spring Bean名称可能是adminServiceImpl1
和adminServiceImpl2
,则名称也无法匹配,在加载Spring时,就会直接报NoUniqueBeanDefinitionException
错误,此时,可以选择:
-
修改属性名,使之与某个Spring Bean名称相同,例如
private IAdminService adminServiceImpl1;
-
修改某个Spring Bean名称,使之与属性名相同,例如
@Serivce("adminService")
-
在属性上补充
@Qualifier
注解,指定Spring Bean的名称,例如@Qualifier("adminSesrviceImpl1")
除了使用@Autowired
以外,还可以使用@Resource
注解添加在属性上,完成属性值的自动装配,例如:
@RestController
public class AdminController {
@Resource // @Autowired // 自动装配
private IAdminService adminService; // AdminServiceImpl类的对象
}
关于@Autowired
和@Resource
的区别:
-
@Autowired
是Spring框架的注解,而@Resource
是javax.annotation
包中的注解-
如果你不使用Spring框架,改为使用其它可以实现自动装配的框架,
@Resource
仍是有效的-
由于Spring框架的应用普级程度非常高,几乎没有Java WEB项目不使用Spring框架,所以,使用
@Autowired
几乎没有缺点
-
-
-
@Resource
注解是优先根据名称来匹配的,如果无匹配,再根据类型来匹配;@Autowired
注解是优先根据类型来查找匹配的对象,如果存在多个类型匹配的,再根据名称来匹配
关于@Autowired
的具体装配过程,首先,会查询匹配类型的Spring Bean的数量,然后:
-
0个:取决于
@Autowired
注解的required
属性的值-
true
(默认):无法装配,在加载ApplicationContext
时会出现NoSuchBeanDefinitionException
-
false
:放弃装配,在加载ApplicationContext
不会因为无法装配而报错,在后续使用时可能出现NPE
-
强烈不推荐
-
-
-
1个:直接装配,且成功
-
多个:尝试按照名称来装配,如果存在名称匹配的Spring Bean,则装配且成功,如果没有,则在加载
ApplicationContext
时会出现NoUniqueBeanDefinitionException
关于为属性注入值的做法
当组件类的属性需要值时,可以有3种做法:
字段注入:在属性上添加@Autowired
/ @Resource
注解,例如:
@RestController
public class AdminController {
@Autowired
private IAdminService adminService;
}
Setter注入:通过Setter方法为属性赋值,此Setter方法需要添加@Autowired
注解,例如:
@RestController
public class AdminController {
private IAdminService adminService;
@Autowired
public void setAdminService(IAdminService adminService) {
this.adminService = adminService;
}
}
构造方法注入:通过构造方法为属性赋值,例如:
@RestController
public class AdminController {
private IAdminService adminService;
public AdminController(IAdminService adminService) {
this.adminService = adminService;
}
}
从理论上分析:通过构造方法注入是最安全的做法,而字段注入是最不推荐的做法,所以,在IntelliJ IDEA中,使用字段注入时会有相关提示!
关于Spring调用构造方法:
-
如果类中没有显式的添加构造方法,Spring会自动调用默认的构造方法
-
如果类中显式的声明了1个构造方法,无论是否有参数,Spring都会自动尝试调用它
-
如果类中有多个构造方法,在默认情况下,Spring会自动调用无参数的构造方法(如果存在的话),如果你希望Spring调用特定的构造方法,则需要在构造方法上添加
@Autowired
注解
关于IoC与DI
IoC(Inversion of Control):控制反转,表现为“将对象的控制权交给了Spring框架”
在没有spring的情况下,我作为写代码的人,我是对这个对象有完全的控制权的,就说这个对象从创建到销毁一切都尽在我在的掌握,但有了spring框架之后,这个事就不用我做了,表现为“将对象的控制权交给了Spring框架”,这就是控制反转。
DI(Dependency Injection):依赖注入,表现为“为依赖项注入值”
我们用的 @Autowired的注入都是DI。
如果要说IoC与DI的关系和区别:
Spring框架负责创建对象、管理对象都是Spring IoC的表现。
Spring框架通过DI完善了IoC,所以,DI是一种实现手段,IoC是需要实现的目标。
在开发实践中我们不仅仅需要spring把对象创建出来,还需要各个属性有值,否则创建出来也没有什么用。所以没有DI,IoC是不完善的。
关于Spring AOP
AOP:面向切面的编程
注意:AOP源自AspectJ,并不是Spring特有的技术,只是Spring很好的支持了AOP
AOP主要解决了“横切关注”的问题,具体为“若干个不同的方法均需要执行相同的任务”的问题!
使用AOP的常用场景有:安全检查、异常处理、事务管理等。
假设存在需求:统计各个Service中的各方法的执行耗时。
在Spring Boot项目中,当需要使用AOP时,需要添加spring-boot-starter-aop
依赖项:
<!-- Spring Boot支持Spring AOP的依赖项,主要解决横切关注问题 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>${spring-boot.version}</version>
</dependency>
在项目的根包下创建aop.TimerAspect
切面类,在类上添加@Aspect切面注解
和@Component组件
注解,在类中编写切面方法:
package cn.tedu.csmall.product.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Slf4j
@Aspect
@Component
public class TimerAspect {
// 连接点(JoinPoint):程序执行过程中的某个节点,可能是某个方法的调用,或抛了某个异常
// 切入点(PointCut):选择1个或多个连接点的表达式
// ------------------------------------------------------------------------
// 通知(Advice)注解
// @Around:环绕,包裹了连接点,你的代码可以在连接点之前和之后执行
// @Before:只能在连接点之前执行
// @After:只能在连接点之后执行
// @AfterReturning:只能在成功的执行了连接点之后执行
// @AfterThrowing:只能在连接点方法抛出异常之后执行
// 以上各Advice执行情况大致是:
// @Around开始
// try {
// @Before
// 执行连接点
// @AfterReturning
// } catch (Throwable e) {
// @AfterThrowing
// } finally {
// @After
// }
// @Around结束
// ------------------------------------------------------------------------
// execution配置的就是切入点表达式,用于匹配某些方法
// 在切入点表达式中,可以使用通配符:
// -- 1个星号:任意1次匹配
// -- 2个连续的小数点:任意n次匹配,只能用于包或和参数列表
// ↓ 返回值类型
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 包名
// ↓ 类名
// ↓ 方法名
// ↓↓ 参数列表
// 另外,在表达式中,在方法的返回值的左侧还可以指定修饰符,修饰符是可选的
// 注解是典型的修饰符之一
// 其实,Spring MVC的统一异常处理就是通过AOP实现的,
// Spring JDBC的事务管理也是如此
// Spring Security的权限检查也是如此
@Around("execution(* cn.tedu.csmall.product.service.*.*(..))")
public Object xxx(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
// 获取匹配的方法的相关信息
String targetClassName = pjp.getTarget().getClass().getName(); // 获取匹配的方法所在的类
String signatureName = pjp.getSignature().getName(); // 获取匹配的方法的签名中的方法名称
Object[] args = pjp.getArgs(); // 方法的参数列表
System.out.println("类型:" + targetClassName);
System.out.println("方法名:" + signatureName);
System.out.println("参数列表:" + Arrays.toString(args));
// 执行以上表达式匹配的方法,即某个Service的某个方法
// 注意-1:必须获取调用proceed()方法返回的结果,作为当前切面方法的返回值
// -- 如果没有获取,或没有作为当前切面方法的返回值,相当于执行了连接点方法,却没有获取返回值
// 注意-2:调用proceed()时的异常必须抛出,否则,Controller将无法知晓Service抛出过异常,就无法向客户端响应错误信息
// -- 前提:本例的切面是作用于Service的
// -- 其实,你也可以使用try...catch捕获调用proceed()时的异常,但必须在catch中也抛出异常
Object result = pjp.proceed();
long end = System.currentTimeMillis();
System.out.println("执行耗时:" + (end - start) + "毫秒");
// 返回调用proceed()得到的结果
return result;
}
}
代码解释:
可以看出,我们不管执行什么方法,它的执行耗时都统计出来了,但在刚才整个编写代码的过程中,都没有去修改service的代码,这就是AOP技术最强大的地方,它使得每个service的执行,我们都可以去干预这个执行的过程,但我们却不需要去修改原有的任何代码。
这些方法可以得到配置的方法的相关信息: