简介
本文用实例介绍SpringBoot如何装配bean。
注入的方式
说明
下边@Autowired基本可以用于@Value。但有一点要注意:@Value用于参数时,@Value不能省略,例如:
String name;
public abc(@Value(${"myName"}String myName) {
this.name = myName;
}
field注入
示例
@Controller
public class FooController {
@Autowired
private FooService fooService;
//简单的使用例子,下同
public List<Foo> listFoo() {
return fooService.list();
}
}
优点
- 代码少,简洁明了。
- 新增依赖十分方便,不需要修改原有代码
缺点
- 容易出现空指针异常。Field 注入允许构建对象实例时依赖的对象为空,导致空指针异常不能在启动时就爆出来,只能在用到它时才发现。
空指针异常不是必现的,与bean的实例化顺序有关。有时,把依赖的bean改个名字就会报空指针异常。 - 会出现循环依赖的隐患。
构造器注入
示例
@Controller
public class FooController {
private final FooService fooService;
// @Autowired不写也可以
@Autowired
public FooController(FooService fooService) {
this.fooService = fooService;
}
}
优点
- 保证注入的组件不可变
- 确保需要的依赖不为空
- 解决循环依赖的问题
能够保证注入的组件不可变,并且确保需要的依赖不为空。此外,构造器注入的依赖总是能够在返回客户端(组件)代码的时候保证完全初始化的状态。
- 依赖不可变(immutable objects)。
即:final关键字。 - 依赖不可为空(required dependencies are not null)。省去了我们对其检查。
当要实例化FooController时,由于自己实现了有参数的构造函数,所以不会调用默认构造函数,那么就需要Spring容器传入所需要的参数,所以就两种情况:
1、有该类型的参数=> 传入,OK 。2:无该类型的参数=> 报错。所以保证不会为空 ) - 完全初始化的状态(fully initialized state)。跟上面的依赖不可为空结合起来。
向构造器传参前,为确保注入的内容不为空,要调用依赖组件的构造方法完成实例化。而在Java类加载实例化的过程中,构造方法是最后一步(之前如果有父类先初始化父类,然后自己的成员变量,最后才是构造方法,这里不详细展开。)。所以返回来的都是初始化之后的状态。 - 解决循环依赖(spring 的三层缓存机制)
官方文档:
The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null
. Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state. As a side note, a large number of constructor arguments is a bad code smell, implying that the class likely has too many responsibilities and should be refactored to better address proper separation of concerns.
缺点
- 当注入参数较多时,代码臃肿。
setter注入
@Controller
public class FooController {
private FooService fooService;
@Autowired
public void setFooService(FooService fooService) {
this.fooService = fooService;
}
}
优点
- 注入参数多的时候比较方便。构造器注入参数太多了,显得很笨重
- 能让类在之后重新配置或者重新注入。
官方文档:
The Spring team generally advocates setter injection, because large numbers of constructor arguments can get unwieldy, especially when properties are optional. Setter methods also make objects of that class amenable to reconfiguration or re-injection later. Management through JMX MBeans is a compelling use case.
Some purists favor constructor-based injection. Supplying all object dependencies means that the object is always returned to client (calling) code in a totally initialized state. The disadvantage is that the object becomes less amenable to reconfiguration and re-injection.
缺点
有一定风险。set注入是后初始化其依赖对象,如果一个对象在没有完全初始化就被外界使用是不安全的(尤其是在多线程场景下更加突出)。
注入的顺序
其他网址
【基础系列】指定Bean初始化顺序的若干姿势 | 一灰灰Blog
简介
Bean初始化顺序与类加载顺序基本一致:静态变量/语句块=>实例变量或初始化语句块=>构造方法=>@Autowired
控制bean的加载顺序的方法
- 构造方法依赖
- @DependsOn 注解
- BeanPostProcessor 扩展
构造方法依赖(推荐)
创建两个Bean,要求CDemo2在CDemo1之前被初始化。
@Component
public class CDemo1 {
private String name = "cdemo 1";
public CDemo1(CDemo2 cDemo2) {
System.out.println(name);
}
}
@Component
public class CDemo2 {
private String name = "cdemo 2";
public CDemo2() {
System.out.println(name);
}
}
结果(和预期一致)
限制
- 要有注入关系,如:CDemo2通过构造方法注入到CDemo1中,若需要指定两个没有注入关系的bean之间优先级,则不太合适(比如我希望某个bean在所有其他的Bean初始化之前执行)
- 循环依赖问题,如过上面的CDemo2的构造方法有一个CDemo1参数,那么循环依赖产生,应用无法启动
另外一个需要注意的点是,在构造方法中,不应有复杂耗时的逻辑,会拖慢应用的启动时间
@DependsOn(不推荐)
不推荐的原因:这种方法是通过bean的名字(字符串)来控制顺序的,如果改了bean的类名,很可能就会忘记来改所有用到它的注解,那就问题大了。
当一个bean需要在另一个bean实例化之后再实例化时,可使用这个注解。
@DependsOn("rightDemo2")
@Component
public class RightDemo1 {
private String name = "right demo 1";
public RightDemo1() {
System.out.println(name);
}
}
@Component
public class RightDemo2 {
private String name = "right demo 2";
public RightDemo2() {
System.out.println(name);
}
}
上面的注解放在 RightDemo1 上,表示RightDemo1的初始化依赖于rightDemo2这个bean
它能控制bean的实例化顺序,但是bean的初始化操作(如构造bean实例之后,调用@PostConstruct注解的初始化方法)顺序则不能保证,比如我们下面的一个实例,可以说明这个问题
@DependsOn("rightDemo2")
@Component
public class RightDemo1 {
private String name = "right demo 1";
@Autowired
private RightDemo2 rightDemo2;
public RightDemo1() {
System.out.println(name);
}
@PostConstruct
public void init() {
System.out.println(name + " _init");
}
}
@Component
public class RightDemo2 {
private String name = "right demo 2";
@Autowired
private RightDemo1 rightDemo1;
public RightDemo2() {
System.out.println(name);
}
@PostConstruct
public void init() {
System.out.println(name + " _init");
}
}
结果(先实例的Bean反而在后边执行init)
把上面测试代码中的@Autowired的依赖注入删除,即两个bean没有相互注入依赖,再执行,会发现输出顺序又不一样
BeanPostProcessor(不推荐)
一种非典型的使用方式,如非必要,请不要用这种方式来控制bean的加载顺序。
场景1:希望HDemo2在HDemo1之前被加载
@Component
public class HDemo1 {
private String name = "h demo 1";
public HDemo1() {
System.out.println(name);
}
}
@Component
public class HDemo2 {
private String name = "h demo 2";
public HDemo2() {
System.out.println(name);
}
}
@Component
public class DemoBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements BeanFactoryAware {
private ConfigurableListableBeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) {
if (!(beanFactory instanceof ConfigurableListableBeanFactory)) {
throw new IllegalArgumentException(
"AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory);
}
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}
@Override
@Nullable
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
// 在bean实例化之前做某些操作
if ("HDemo1".equals(beanName)) {
HDemo2 demo2 = beanFactory.getBean(HDemo2.class);
}
return null;
}
}
将目标集中在postProcessBeforeInstantiation,这个方法在某个bean的实例化之前,会被调用,这就给了我们控制bean加载顺序的机会。
执行结果
场景2:希望某个bean在应用启动之后,首先实例化此Bean。
解决方法:重写DemoBeanPostProcessor的postProcessAfterInstantiation方法。
@Component
public class DemoBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
implements BeanFactoryAware {
private ConfigurableListableBeanFactory beanFactory;
@Override
public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
if ("application".equals(beanName)) {
beanFactory.getBean(FDemo.class);
}
return true;
}
}
@DependsOn("HDemo")
@Component
public class FDemo {
private String name = "F demo";
public FDemo() {
System.out.println(name);
}
}
@Component
public class HDemo {
private String name = "H demo";
public HDemo() {
System.out.println(name);
}
}
执行结果(HDemo, FDemo的实例化顺序放在了最前面)
装配方式(提供Bean)
常用方式
@SpringBootApplication+@Component
(@Controller/@Service/@Repository也可以,因为它里边包含@Component)
默认是加载和Application类所在同一个目录下的所有类,包括所有子目录下的类。
当启动类和@Component分开时,如果启动类在某个包下,需要在启动类中增加注解@ComponentScan,配置需要扫描的包名。例如:
@SpringBootApplication(scanBasePackages="com.test.chapter4")
此注解其实是@ComponentScan的basePackages,通过查看scanBasePackages即可得知。
@SpringBootApplication只会扫描@SpringBootApplication注解标记类包下及其子包的类,将这些类纳入到spring容器,只要类有@Component注解即可。
有的注解的定义中已加入@Component,所以这些注解也会被扫描到:@Controller,@Service,@Configuration,@Bean
@ComponentScan+@Configuration+@Component
DemoConfig在扫描路径之内
@Configuration
@ComponentScan(basePackages = { "com.example.demo.mybeans" })
public class DemoConfig {
}
MyBean1在com.example.demo.mybeans下
@Component
public class MyBean1{
}
@Configuration+@Bean
使用场景:将没有Component等注解的类导入。例如:第三方包里面的组件、将其他jar包中的类。
public class User {
//@Value("Tom")
public String username;
public User(String s) {
this.username = s;
}
}
@Configuration
public class ImportConfig {
@Bean
public User user(){
return new User("Lily");
}
}
@RestController
public class ImportDemoController {
@Autowired
private User user;
@RequestMapping("/importDemo")
public String demo() throws Exception {
String s = user.username;
return "ImportDemo@SpringBoot " + s;
}
}
@Import等
简介
- @Import(要导入到容器中的组件);容器会自动注册这个组件,id默认是全类名。(@Import是Spring的注解。)
- ImportSelector:返回需要导入的组件的全类名数组;
- ImportBeanDefinitionRegistrar:手动注册bean到容器中
@Import方式
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({ImportDemoConfig.class})
public @interface EnableImportDemo {
}
public class ImportDemoConfig{
@Bean
public User user(){
return new User("Lily");
}
}
@EnableImportDemo
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@RestController
public class ImportDemoController {
@Autowired
private User user;
@RequestMapping("/importDemo")
public String demo() {
String s = user.getName();
return "user.getName():" + s;
}
}
ImportSelector方式
//自定义逻辑返回需要导入的组件
public class MyImportSelector implements ImportSelector {
//返回值,就是到导入到容器中的组件全类名
//AnnotationMetadata:当前标注@Import注解的类的所有注解信息
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//当前类的所有注解
Set<String> annotationTypes = importingClassMetadata.getAnnotationTypes();
System.out.println("当前配置类的注解信息:"+annotationTypes);
//注意不能返回null,不然会报NullPointException
return new String[]{"com.paopaoedu.springboot.bean.user01","com.paopaoedu.springboot.bean.user02"};
}
}
public class User01 {
public String username;
public User01() {
System.out.println("user01...constructor");
}
}
public class User02 {
public String username;
public User02() {
System.out.println("user02...constructor");
}
}
@Configuration
@Import({ImportDemo.class, MyImportSelector.class})
public class ImportConfig {
@Bean
public User user(){
return new User("Lily");
}
}
@RestController
public class ImportDemoController {
@Autowired
private User user;
@Autowired
private ImportDemo importDemo;
@Autowired
private User01 user01;
@RequestMapping("/importDemo")
public String demo() throws Exception {
importDemo.doSomething();
user01.username = "user01";
String s = user.username;
String s1 = user01.username;
return "ImportDemo@SpringBoot " + s + " " + s1;
}
}
ImportBeanDefinitionRegistrar方式
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
/**
* AnnotationMetadata:当前类的注解信息
* BeanDefinitionRegistry:BeanDefinition注册类;
* 把所有需要添加到容器中的bean;调用
* BeanDefinitionRegistry.registerBeanDefinition手工注册进来
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
boolean definition = registry.containsBeanDefinition("com.paopaoedu.springboot.bean.User01");
boolean definition2 = registry.containsBeanDefinition("com.paopaoedu.springboot.bean.User02");
if(definition && definition2){
//指定Bean定义信息作用域都可以在这里定义;(Bean的类型,Bean。。。)
RootBeanDefinition beanDefinition = new RootBeanDefinition(User03.class);
//注册一个Bean,指定bean名
registry.registerBeanDefinition("User03", beanDefinition);
}
}
}
public class User03 {
public String username;
public User03() {
System.out.println("user03...constructor");
}
}
使用上和前面的类似就不举例了。
FactoryBean
默认获取到的是工厂bean调用getObject创建的对象。
要获取工厂Bean本身,我们需要给id前面加一个&。例如:&xxxFactoryBean 注意类名是X,这里就是小写的x
public class UserFactoryBean implements FactoryBean<User04> {
@Override
public User04 getObject() throws Exception {
// TODO Auto-generated method stub
System.out.println("UserFactoryBean...getObject...");
return new User04("User04");
}
@Override
public Class<?> getObjectType() {
// TODO Auto-generated method stub
return User04.class;
}
//是否单例?
//true:这个bean是单实例,在容器中保存一份
//false:多实例,每次获取都会创建一个新的bean;
@Override
public boolean isSingleton() {
return true;
}
}
public class User04 {
public String username;
public User04(String s) {
String nowtime= DateUtil.now();
username=s+" "+nowtime;
}
}
@Configuration
@Import({ImportDemo.class, MyImportSelector.class, MyImportBeanDefinitionRegistrar.class})
public class ImportConfig {
// 要获取工厂Bean本身,需要给id前面加一个&,&userFactoryBean
@Bean
public UserFactoryBean userFactoryBean(){
return new UserFactoryBean();
}
@Bean
public User user(){
return new User("Lily");
}
}
@RestController
public class ImportDemoController {
@Autowired
private User user;
@Autowired
private ImportDemo importDemo;
@Autowired
private User01 user01;
@Autowired
private UserFactoryBean userFactoryBean;
@RequestMapping("/importDemo")
public String demo() throws Exception {
importDemo.doSomething();
user01.username = "user01";
String s = user.username;
String s1 = user01.username;
String s4 = userFactoryBean.getObject().username;
return "ImportDemo@SpringBoot " + s + " " + s1 + " " + s4;
}
}
@SpringBootApplication
public class SpringBootLearningApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootLearningApplication.class, args);
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext("com.paopaoedu.springboot.config");
ImportDemo importDemo = context.getBean(ImportDemo.class);
importDemo.doSomething();
printClassName(context);
Object bean1 = context.getBean("userFactoryBean");
Object bean2 = context.getBean("userFactoryBean");
System.out.println(bean1 == bean2);
}
private static void printClassName(AnnotationConfigApplicationContext annotationConfigApplicationContext){
String[] beanDefinitionNames = annotationConfigApplicationContext.getBeanDefinitionNames();
for (int i = 0; i < beanDefinitionNames.length; i++) {
System.out.println("匹配的类"+beanDefinitionNames[i]);
}
}
}
测试结果
拦截器与注入
场景:Token拦截器中需要用@Autowired注入JavaJwtUtil类,结果发现注入的JavaJwtUtil为Null。
原因:拦截器的配置类是以new JwtInterceptor的方式使用的,那么这个JwtInterceptor不受Spring管理,因此,里边@Autowired注入JavaJwtUtil是不会注入进去的。
问题重现
application.yml
server:
port: 8080
spring:
application:
name: springboot-jwt
config:
jwt:
# 密钥
secret: abcd1234
# token过期时间(5分钟)。单位:毫秒.
expire: 300000
拦截器配置
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtInterceptor());
}
}
拦截器
package com.example.demo.interceptor;
import com.example.demo.util.JavaJwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
@Slf4j
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
JavaJwtUtil javaJwtUtil;
List<String> whiteList = Arrays.asList(
"/auth/login",
"/error"
);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
//放过不需要验证的页面。
String uri = request.getRequestURI();
if (whiteList.contains(uri)) {
return true;
}
// 头部和参数都查看一下是否有token
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)) {
token = request.getParameter("token");
if (StringUtils.isEmpty(token)) {
throw new RuntimeException("token是空的");
}
}
if (!javaJwtUtil.verifyToken(token)) {
log.error("token无效");
return false;
}
String userId = javaJwtUtil.getUserIdByToken(token);
log.info("userId:" + userId);
String userName = javaJwtUtil.getUserNameByToken(token);
log.info("userName:" + userName);
return true;
}
}
Jwt工具类
package com.example.demo.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JavaJwtUtil {
//过期时间
@Value("${config.jwt.expire}")
private Long EXPIRE_TIME;
//密钥
@Value("${config.jwt.secret}")
private String SECRET;
// 生成Token,五分钟后过期
public String createToken(String userId) {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
return JWT.create()
// 将 user id 保存到 token 里面
.withAudience(userId)
// date之后,token过期
.withExpiresAt(date)
// token 的密钥
.sign(algorithm);
} catch (Exception e) {
return null;
}
}
// 根据token获取userId
public String getUserIdByToken(String token) {
try {
String userId = JWT.decode(token).getAudience().get(0);
return userId;
} catch (JWTDecodeException e) {
return null;
}
}
// 根据token获取userName
public String getUserNameByToken(String token) {
try {
String userName = JWT.decode(token).getSubject();
return userName;
} catch (JWTDecodeException e) {
return null;
}
}
//校验token
public boolean verifyToken(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
// .withIssuer("auth0")
// .withClaim("username", username)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (JWTVerificationException exception) {
// throw new RuntimeException("token 无效,请重新获取");
return false;
}
}
}
Controller
package com.example.demo.controller;
import com.example.demo.util.JavaJwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
JavaJwtUtil javaJwtUtil;
@RequestMapping("/login")
public String login() {
// 验证userName,password和数据库中是否一致,如不一致,直接返回失败
// 通过userName,password从数据库中获取userId
String userId = 5 + "";
String token = javaJwtUtil.createToken(userId);
System.out.println("token:" + token);
return token;
}
//需要token验证
@RequestMapping("/info")
public String info() {
return "验证通过";
}
}
测试
访问:http://localhost:8080/auth/login
前端结果:一串token字符串
访问:http://localhost:8080/auth/info(以token作为header或者参数)
后端结果
java.lang.NullPointerException: null
at com.example.demo.interceptor.JwtInterceptor.preHandle(JwtInterceptor.java:55) ~[main/:na]
问题解决
配置类中将new JwtInterceptor()改为Bean的方式
配置类
package com.example.demo.interceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getJwtInterceptor());
}
@Bean
JwtInterceptor getJwtInterceptor() {
return new JwtInterceptor();
}
}
拦截器(此时无需@Component)
package com.example.demo.interceptor;
import com.example.demo.util.JavaJwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
JavaJwtUtil javaJwtUtil;
List<String> whiteList = Arrays.asList(
"/auth/login",
"/error"
);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
//放过不需要验证的页面。
String uri = request.getRequestURI();
if (whiteList.contains(uri)) {
return true;
}
// 头部和参数都查看一下是否有token
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)) {
token = request.getParameter("token");
if (StringUtils.isEmpty(token)) {
throw new RuntimeException("token是空的");
}
}
if (!javaJwtUtil.verifyToken(token)) {
log.error("token无效");
return false;
}
String userId = javaJwtUtil.getUserIdByToken(token);
log.info("userId:" + userId);
String userName = javaJwtUtil.getUserNameByToken(token);
log.info("userName:" + userName);
return true;
}
}
其他网址
为什么Spring推荐使用构造器注入而不是Field注入- OSCHINA - 中文开源技术交流社区
为什么Spring推荐使用构造器注入? - 知乎
spring为何推荐使用构造器注入