Java小白修炼手册--第五阶段-- Spring框架(day02)

目录

Spring

基于注解的组件扫描

组件扫描

关于注解的使用

使用组件扫描后配置作用域与生命周期

关于Spring管理对象的小结

关于Spring的解耦

String IOC

IOC 简介

IOC应用

自动装配

常见匹配类型错误

 通过Spring框架读取.properties文件

通过Environment读取.properties配置文件



Spring

基于注解的组件扫描

组件扫描

指定扫描类路径后,并不是该路径下所有组件类都扫描到Spring容器的,只有在组件类定义前面有以下注解标记时,才会扫描到Spring容器。
 

注解标记 描述
@Component 通用注解
@Named 通用注解
@Repository 持久化层组件注解,推荐添加在处理持久层的类之前.
@Service 业务层组件注解,推荐添加在业务类之前;
@Controller 控制层组件注解,推荐添加在控制器类之前;

首先,必须让Spring 扫描组件所在的包,并且组件类的声明必须添加@Component注解!

其实,除了@Component注解以外,还可以使用以下注释实现同样的效果:

  • @Controller:推荐添加在控制器类之前;

  • @Service:推荐添加在业务类之前;

  • @Repository:推荐添加在处理持久层的类之前.

以上4个注解在Spring框架的作用领域中,效果是完全相同的,用法也完全相同,只是语义不同。

在使用组件扫描时,还可以自定义某个类,作为配置类,在这个类的声明之前使用@ComponentScan注解来配置组件扫描的包:

package cn.tedu.spring;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan("cn.tedu.spring")
public class SpringConfig {

}

后续,程序运行时,就需要加载这个配置类:

//1.加载配置 获取Spring 容器
		AnnotationConfigApplicationContext ac =
				new AnnotationConfigApplicationContext(SpringConfig.class);

关于组件扫描的包,严格来说,是配置需要被扫描的“根包base package)”,也就是说,在执行扫描时,会扫描所设置的包及其所有子孙包中的所有组件类!当设置为扫描cn.tedu包时,会把cn.tedu.spring甚至cn.tedu.spring.dao这些包中的组件类都扫描到!

关于注解的使用

@Bean 注解为例,其声明是:

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {

}

可以看到,注解都是通过@interface 声明的!

在注解的声明之前,还添加了一系列的注解,例如以上的@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented,则表示当前@Bean注解同时具有以上3个注解的特性。也就是说,@Bean注解相当于以上3个注解的同时,还具有自身的特性!

@Bean 注解内部,还有:

    /**
	 * Alias for {@link #name}.
	 * <p>Intended to be used when no other attributes are needed, for example:
	 * {@code @Bean("customBeanName")}.
	 * @since 4.3.3
	 * @see #name
	 */
	@AliasFor("name")
	String[] value() default {};

以上String[] value() default {};有点像接口中的抽象方法,但是,在注解中,这是声明的注解属性!value是属性名称,所以,在使用当前注解时,可以配置:

@Bean(value=???)

以上源代码中的String[]看似是抽象方法的返回值,实则是value属性的值的数值类型!所以,可以配置为:

@Bean(value={"a", "b", "c"})

以上源代码中的default {}表示该属性的默认值,所以,以下2段配置是完全等效的:

@Bean

@Bean(value={})

在配置注解属性时,如果属性名称是value,它是默认的属性,在配置时,可以不用显式的写出value=部分,也就是说,以下2段配置是完全等效的:

@Bean(value={"a", "b", "c"})


@Bean({"a", "b", "c"})
在配置注解属性时,如果属性的值的类型是数组类型,但是,当前只需要配置1个值时,可以不用写成数组格式,只需要写成数组元素的格式即可!也就是说,以下2段配置是完全等效的: 
@Bean({"a"})

@Bean("a")

所以,总的来说,关于@Bean注解的value属性,如果需要配置的值是"user",则以下4段代码都是完全等效的:

@Bean("user")

@Bean({"user"})

@Bean(value="user")

@Bean(value={"user"})

在以上源代码中,注释中还标明了@since 4.3.3,表示该属性从Spring框架4.3.3版本开始才加入的,如果当前使用的环境改为4.3.3以下的版本,将导致该属性不可用,因为在更低的版本中,根本就没有这个属性,甚至可能连个注解本身都不存在!

在以上源代码中,在value属性的声明之前还添加了@AliasFor("name")注解,表示当前value属性另有别名name,所以,在@Bean注解的源代码中,还有:

/**
 * The name of this bean, or if several names, a primary bean name plus aliases.
 * <p>If left unspecified, the name of the bean is the name of the annotated method.
 * If specified, the method name is ignored.
 * <p>The bean name and aliases may also be configured via the {@link #value}
 * attribute if no other attributes are declared.
 * @see #value
 */
@AliasFor("value")
String[] name() default {};

则在@Bean注解中,namevalue这2个注解是完全等效的!

之所以存在2个完全等效的属性,是因为:

  • value属性是默认的,在配置时可以不必显式的写出value=部分,配置时更加简单;

  • name属性表现的语义更好,更易于根据源代码读懂程序的意思,在其它注解中,也可能存在与value 等效的属性。

需要注意的是:在配置注解中的属性时,如果需要配置的是value属性的值,可以不用显式的写出value=部分,前提是当前注解只配置value这1个属性!如果需要配置多个属性,则必须写出每一个属性名,例如:

@Bean(value="user", initMethod="init")

而不能写成:

@Bean("user", initMethod="init")	// 错误

使用组件扫描后配置作用域与生命周期

通常受Spring管理的组件,默认的作用域是”singleton"如果需要其他的作用域可以使用@Scope注解,只要在注解中提供作用域的名称即可

在类的声明之前,添加@Scope("prototype")即可将当前类配置为“非单例”的对象!

例如:

package cn.tedu.spring;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Repository;

@Repository
@Scope("prototype")
public class User {

}

在单例的情况下,在类的声明之前添加@Lazy注解,就可以将对象配置为“懒加载”的模式:

package cn.tedu.spring;

import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Repository;

@Repository
@Lazy
public class User {

}

如果需要配置当前类中的生命周期的处理,首先,还是需要在类中自定义2个方法,分别表示“初始化方法”和“销毁方法”,然后,在初始化方法之前添加@PostConstruct注解,在销毁方法之前添加@PreDestroy注解,例如:

package cn.tedu.spring;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Repository;

@Repository
@Lazy
public class User {
	
	public User() {
		System.out.println("User.User()");
	}
	
	@PostConstruct
	public void init() {
		System.out.println("User.init()");
	}
	
	@PreDestroy
	public void destroy() {
		System.out.println("User.destroy()");
	}

}

注意:以上2个注解并不是Spring的注解,如果JRE环境版本太低,将无法识别以上2个注解,需要调整当前项目的JRE环境!

 

关于Spring管理对象的小结

如果需要Spring管理某个类的对象,可以:

  1. 自定义方法,将方法的返回值类型设置为期望管理的类型,并在方法中返回匹配类型的对象,最后,在方法的声明之前添加@Bean注解;

  2. 设置组件扫描的包,并在类的声明之前添加@Component / @Controller / @Service / @Repository这4个注解中的某1个。

在实际使用时,大多采取第2种做法,但是,如果需要Spring管理的类并不是自定义的类,就只能采取第1种做法!

关于Spring的解耦

在没有使用Spring框架的情况下,在项目中,各组件之间是存在依赖关系的,例如:

// 处理用户登录请求的Servlet组件类
public class UserLoginServlet {
    private UserJdbcDao userDao = new UserJdbcDao();
    public void doPost() {
        userDao.login();
    }
}
// 处理用户数据增删改查的组件
public class UserJdbcDao {
    public void login() {
        // 通过JDBC技术实现数据查询,判断用户名与密码是否正确
    }
}

以上代码就体现了类与类之前的依赖关系,具体表现就是UserLoginServlet是依赖于UserJdbcDao的!

如果直接依赖于某个类,将会导致耦合度过高的问题!

假设在UserJdbcDao中,是通过原生的JDBC技术实现数据访问的,后续,需要改为使用MyBatis框架技术来实现,则可能创建UserMybatisDao类,用于取代UserJdbcDao类!

如果需要替换,则项目中原有的以下代码:

private UserJdbcDao userDao = new UserJdbcDao();

全部需要替换为:

private UserMybatisDao userDao = new UserMybatisDao();

这种替换时需要调整大量原有代码的问题,就是高耦合的问题,我们希望的目标是低耦合,将原有高耦合的项目调整为低耦合的状态,就是解耦的做法!

可以将处理用户数据增删改查的相关操作声明在接口中,例如:

public interface UserDao {
    void login();
}

然后,各个处理用户数据增删改查的类都去实现这个接口:

public class UserJdbcDao implements UserDao {
    public void login() {
        // 通过JDBC实现处理用户登录
    }
}
public class UserMybatisDao implements UserDao {
    public void login() {
        // 通过MyBatis框架技术实现处理用户登录
    }
}

后续,在各个Servlet组件中,就可以声明为接口类型:

private UserDao userDao = new UserMybatisDao();

通过以上代码调整,就可以使得Servlet组件依赖于接口,而不再是依赖于类,从而实现了解耦!

另外,还可以通过设计模式中的工厂模式来生产对象,例如:

public class UserDaoFactory {
    public static UserDao newInstance() {
        return new UserMybatisDao();
    }
}

当有了以上工厂后,原本在Servlet组件中声明持久层对象的代码就可以再调整为:

private UserDao userDao = UserDaoFactory.newInstance();

至此,在项目中到底是使用UserJdbcDao还是使用UserMybatisDao,在以上代码都不会体现出来了,也就意味着当需要切换/替换时,以上代码是不需要修改的,而是修改UserDaoFactory工厂类的方法的返回值这1处即可!

所以,通过定义接口和创建工厂类就可以实现解耦,但是,在实际项目开发时,不可能为每一个组件都创建专门的工厂类,而Spring框架就可以当作是一个庞大的工厂,开发人员可以通过Spring框架的使用约定,将某些类的对象交给Spring框架进行管理,后续,在具体使用过程中,就不必自行创建对象,而是获取对象即可!

String IOC

IOC 简介

  • IOC全称是Inversion of Control,被译为控制反转;
  • IOC是指程序中对象的获取方式发生反转,由最初的new方式创建,转变为由第三方框架创建、注入(DI), 它降低了对象之间的耦合度。
  • Spring容器是采用DI方式实现了IOC控制, IOC是Spring框架的基础和核心;
  • DI全称是Dependency Injection,被译为依赖注入;
  • DI的基本原理就是将一起工作具有关系的对象,通过构造方法参数或方法参数传入建立关联,因此容器的工作就是创建bean时注入那些依赖关系。
  • IOC是一种思想,而DI是实现IOC的主要技术途径
  • DI主要有两种注入方式,即Setter注入和构造器注入
     

IOC应用

自动装配

在Spring框架的应用中,可以为需要被Spring自动赋值的属性添加@Autowired,则Spring框架会从Spring容器中找出匹配的值,并自动完成赋值!这就是Spring框架的自动装配机制!

  • Spring IOC容器可以自动装配( autowire )相互协作bean之间的关联关系, autowire可以针对单个bean进行设置, autowire的方便之处在于减少xml的注入配置
  • 在xml配置文件中,可以在<bean/>元素中使用autowire属性指定自动装配规则,一共有五种类型值
     
属性值 描述
no 禁用自动装配,默认值
byName 根据属性名自动装配。此选项将检查容器并根据名字查找与属性完全致的
bean,并将其与属性自动装配
byType 如果容器中存在个与指定属性类型相同的bean,那么将与该属性自动装配
constructor
 
与byType的方式类似,不同之处在于它应用于构造器参数
 
autodetect 通过bean类米决定是使用constructor还是byType方式进行自动装配。如果发现
默认的构造器,那么将使用byType方式


 

当Spring尝试为某个属性实现自动装配时,采取的模式主要有:

  1. byName:根据名称实现自动装配,在这种模式下,要求被装配的属性名称,与被Spring管理的对象的名称(调用`getBean()`方法给出的参数名)必须相同;
  2. byType:根据类型实现自动装配,在这种模式,要求被装配的属性的类型,在Spring容器中存在匹配类型的对象,当应用这种机制时,必须在Spring容器中保证匹配类型的对象只有1个,否则,将会出现`NoUniqueBeanDefinitionException(没有唯一的Bean定义异常`)`异常;

常见匹配类型错误

当使用`@Autowired`尝试自动装配时,Spring框架会先根据`byType`模式找出所有匹配类型的对象,如果匹配类型的对象的数量为0,也就是没有匹配类型的对象,默认情况下会直接报错,提示信息例如:
 

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException:
 No qualifying bean of type 'cn.tedu.spring.UserDao' available:
 expected at least 1 bean which qualifies as autowire candidate.
 Dependency annotations:{@org.springframework.beans.factory.annotation.Autowired(required=true)}

如果使用`@Autowired`时明确的配置为`@Autowired(required=false)`,当没有匹配类型的对象时,也不会因为装配失败而报错!

如果匹配类型的对象的数量为1,则直接装配;

如果匹配类型的对象的数量超过1个(有2个甚至更多个),会尝试`byName`来装配,如果存在名称匹配的对象,则成功装配,如果名称均不匹配,则装配失败,会提示如下错误:

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type 'cn.tedu.spring.UserDao' available:
 expected single matching bean but found 2:
userJdbcDao,userMybatisDao

当需要自动装配时,除了使用`@Autowired`注解以外,还可以使用`@Resource`注解!

当使用`@Resource`注解尝试自动装配时,其工作原理是先尝试`byName`装配,如果存在名称匹配的对象,则直接装配,如果没有名称匹配的对象,则尝试`byType`装配。

另外,如果某个方法是被Spring调用的,还可以将需要装配的对象设置为方法的参数(不需要添加注解即可正常使用),Spring也可以实现方法参数的自动装配!例如:


public void test(UserDao userDao) {}

 通过Spring框架读取.properties文件

首先,在案例的**src/main/resources**下创建**jdbc.properties**文件,并且,在文件中,添加一些自定义的配置信息:

url=jdbc:mysql://localhost:3306/db_name
driver=com.mysql.jdbc.Driver
username=root
password=1234

本次案例的目标是读取以上文件的信息,并不用于真实的连接某个数据库,所以,各属性的值可以不是真正使用的值!

如果要读取以上信息,可以将这些信息都读取到某个类的各个属性中去,则先创建一个类,并在类中声明4个属性(与以上**jdbc.properties**文件中的配置信息的数量保持一致):

package cn.tedu.spring;

public class JdbcConfig {
	
	private String url;
	private String driver;
	private String username;
	private String password;

}

然后,在类的声明之前,通过`@PropertySource`配置需要读取的配置文件:

@PropertySource("classpath:jdbc.properties")

然后,在各个属性的声明之前,通过`@Value`注解读取配置信息中的值,并注入到属性中,其基本格式是`@Value("${配置文件中的属性名称}")`,例如:

package cn.tedu.spring;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;

@PropertySource("classpath:jdbc.properties")
public class JdbcConfig {
	
	@Value("${url}")
	private String url;
	@Value("${driver}")
	private String driver;
	@Value("${username}")
	private String username;
	@Value("${password}")
	private String password;

    @Override
	public String toString() {
		return "JdbcConfig [url=" + url + ", driver=" + driver + ", username=" + username +                 
         ", password=" + password+ "]";
	}
    
}

由于期望的是由Spring读取配置文件,并为以上类的各个属性赋值,所以,以上`JdbcConfig`应该是被Spring管理的!所以,先使用一个配置组件扫描:
 

package cn.tedu.spring;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan("cn.tedu.spring")
public class SpringConfig {

}

然后,在`JdbcConfig`类的声明之前添加`@Component`注解即可!

注意:在Windows操作系统中,如果配置文件中的属性名是`username`,则最终注入属性的值将不是配置文件中的值,而是当前登录Windows操作系统的用户名,为了避免出现此类问题,建议在配置文件中,每个属性的名称之前都添加一些自定义的前缀。

例如:

db.url=jdbc:mysql://localhost:3306/db_name
db.driver=com.mysql.jdbc.Driver
db.username=root
db.password=1234
package cn.tedu.spring;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;

@PropertySource("classpath:jdbc.properties")
public class JdbcConfig {
	
	@Value("${db.url}")
	private String url;
	@Value("${db.driver}")
	private String driver;
	@Value("${db.username}")
	private String username;
	@Value("${db.password}")
	private String password;

    @Override
	public String toString() {
		return "JdbcConfig [url=" + url + ", driver=" + driver + ", username=" + username + 
         ", password=" + password+ "]";
	}
    
}

通过Environment读取.properties配置文件

假设在src/main/resources下存在jdbc.properties文件,并且,在该文件中存在若干条配置信息,如果需要读取该文件中的配置信息,可以先创建某个类,在类中声明Environment接口类型的对象,通过自动装配的方式为该类型对象注入值:

package cn.tedu.spring;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component
@PropertySource("classpath:jdbc.properties")
public class JdbcConfig {
	
	@Autowired
	private Environment environment;

	public Environment getEnvironment() {
		return environment;
	}

	public void setEnvironment(Environment environment) {
		this.environment = environment;
	}

}

后续,需要读取配置文件中的值时,从以上类中获取Environment类型的对象,然后,调用该对象的getProperty()方法即可获取对应的值,例如:

package cn.tedu.spring;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.Environment;

public class Demo {

	public static void main(String[] args) {
		AnnotationConfigApplicationContext ac
			= new AnnotationConfigApplicationContext(SpringConfig.class);
		
		JdbcConfig jdbcConfig = ac.getBean("jdbcConfig", JdbcConfig.class);
		
		Environment environment = jdbcConfig.getEnvironment();
		System.out.println(environment.getProperty("db.url"));
		System.out.println(environment.getProperty("db.driver"));
		System.out.println(environment.getProperty("db.username"));
		System.out.println(environment.getProperty("db.password"));
		
		ac.close();
	}

}

猜你喜欢

转载自blog.csdn.net/c202003/article/details/107069690