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

    技术2022-07-11  81

    目录

    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注解中,name和value这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管理某个类的对象,可以:

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

    设置组件扫描的包,并在类的声明之前添加@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尝试为某个属性实现自动装配时,采取的模式主要有:

    byName:根据名称实现自动装配,在这种模式下,要求被装配的属性名称,与被Spring管理的对象的名称(调用`getBean()`方法给出的参数名)必须相同;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(); } }

     

    Processed: 0.012, SQL: 9