Spring 简介

# Spring 简介

Spring 基于 IOC(Inversion Of Control) 和 DI(Dependency Inject)。Spring 出现以前,类对象是用户手动创建的(例如使用 new)与组装的,而使用 Spring 框架后类对象将由 Spring 框架生成并由 Spring 控制对象的生命周期

graph TB A[Spring] B[上下文] C[4类Scope] D[Component<br/>ComponentScan] E[Bean<br/>Primary<br/>Autowired] F[Profile<br/>Conditional] G[Value<br/>SpEL] A --> B B --> C A --> D A --> E A --> F A --> G

Spring 组成

Spring 上下文

在基于 Spring 的应用中,对象生存于 Spring 容器(container)中,Spring 容器负责创建、装配、配置并管理对象的整个生命周期,从创建到死亡

Spring 容器归为两大类型:Bean 工厂和应用上下文,常用的是 Spring 上下文。Spring 自带了多种类型的应用上下文:

  • AnnotationConfigApplicationContext
    • 从 Java 配置类中加载 Spring 应用上下文
    • AnnotationConfigApplicationContext(com.springinaction.knights.config.KnightConfig.class);
  • AnnotationConfigWebApplicationContext
    • 与上同,用于加载 web 配置
  • ClassPathXmlApplicationContext
    • 默认从所有已知路径下查找并载入 xml 配置文件
    • 给出文件名即可: ClassPathXmlApplicationContext("knight.xml");
  • FileSystemXmlapplicationcontext
    • 从文件系统中的 xml 配置文件中加载上下文
    • 需明确指明文件位置: FileSystemXmlApplicationContext("c:/knight.xml");
  • XmlWebApplicationContext

Bean 作用域

默认情况下,Spring 应用上下文中的所有 bean 都以单例(singleton) 模式创建。有时候,你所使用的类是易变的(mutable),它们会保持一些状态,因此重用是不安全的。Spring 提供了几种作用域,可以按照需求来创建 bean

  • 单例(Singleton),默认作用域,只创建一个 bean 实例
  • 原型(Prototype),每次注入或者通过 Spring 上下文获取时会创建一个新的 bean 实例
  • 会话(Session),web 应用中,为每一个会话创建一个 bean
  • 请求(Rquest),web 应用中,为每一个请求创建一个 bean

@Scope

使用 @Scope 注解可以设置 bean 的作用域:

@Component // @Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
// @Scope("prototype"),使用上面那行的方式更安全一些
public class Notepad { ... }

xml 配置形式如下:

<bean id="notepad" class="com.myapp.Notepad" scope="prototype" />

Spring 依赖注入

Spring 可以使用配置文件或者注解来实现依赖注入,下面以配置文件形式和注解形式来简单解释 IOC 与 DI

配置文件形式(XML)

需要 Spring 进行管理的对象:

public class Category {
    private int id;
    private String name;
    
    // getter & setter func...
}

public class Product {
    private int id;
    private String name;
    private Category category;
    
    // getter & setter func...
}

Spring 配置文件(IOC & DI):

<?xml version=...
    <!-- 同时指定 name 和 id,此时 id 为标识符,而 name 为 Bean 的别名,两者都可以找到目标Bean -->
	<bean id="category" name="c" class="xyz.yearn.Category">
        <property name="name" value="category 1" />
    </bean>
    <bean name="p" class="xyz.yearn.Product" primary="true">
        <property name="name" value="product1" />
        <property name="category" ref="c" />
    </bean>
</beans>

位于 <bean> 标签内的配置用于声明 bean

从 Spring 上下文获取对象

public class TestSpring {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext(
                new String[] { "applicationContext.xml" });
 		// 对象 p 中的 category 由 name 为 c 的 bean 自动进行填充:ref="c",这是 DI
        Product p = (Product) context.getBean("p"); // IOC,xml 配置文件中创建了一个 name 为 p 的对象
        System.out.println(p.getName());
        System.out.println(p.getCategory().getName());
    }
}

注解形式

@Component & @Bean

使用 @Component 告知 Spring 在进行组件扫描(@ComponentScan)时需要为哪些类创建 bean

// Spring 将为使用 Component 注解的类创建对象(bean)
// 每一个 Bean 都有一个 ID,默认 ID 为类首字母小写,可以指定,如下 p
@Component("p") // 大部分场景下可以使用 @Named 替换 Component
@Primary // 有歧义时,标识首选bean
public class Product {
    ...
}

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration// 表名这是一个配置类,包含创建 bean 的细节
@ComponentScan(basePackages={"soundsystem", "video"}) // 组件扫描默认是不开的,需要使用这个注释打开
// @ComponentScan(basePackageClasses={CDPlayer.class, DVDPlayer.class})
// @Import(CDPlayerConfig.class)
// @ImportResource("classpath:cd-config.xml")
public class CDPlayerConfig {
    // 默认情况下, bean的ID与带有@Bean注解的方法名是一样的
    // 因为 Bean 注解的存在,Spring 会拦截所有下面函数的调用,返回相同的对象实例
    @Bean(name="lonelyHeartsClubBand") // 告知 Spring 当前函数返回的对象需要注册为 bean
    @Primary
	public CompactDisc sgtPeppers() {
		return new SgtPeppers();
	}
}

自动装配(DI)

// 使用 Autowired 注解,Spring 自动将合适的 bean 绑定到变量
@Autowired // 变量注解,可以使用 @Inject 替换 Autowired
@Qualifier("cc1") // 只有 id 为 cc1 的 bean 才会注入 category 中
private Category category;

@Autowired // 函数注解,Autowired 按照类型进行装配
// @Resource,按照 name 属性进行装配
// @Autowired(required=false),自动装配失败不抛异常
public void setCategory(Category category) {
    this.category = category;
}

条件装配

Spring 可以在运行时根据环境变量选择合适的 Beans,这样在测试或上线时就不需要重新构建程序

@Profile

Spring 中可以使用 Profile 注解指定 bean 属于哪个 profile,@Profile 注解可以同时置于方法(Spring 版本需要大于 3.2)和类

@Configuration
@Profile("dev")// 控制配置类生效环境
public class DevelopmentProfileConfig {
	@Bean(destroyMethod="shutdown")
	public DataSource dataSource() {
        ...
XML 形式
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
...
profile="dev">
    
	<jdbc:embedded-database id="dataSource">
	...
	</jdbc:embedded-database>
</beans>

可以在 <beans> 元素中嵌套 <beans>,并指定不同的 profile

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
...>
    <beans profile="dev">
		<jdbc:embedded-database id="dataSource">
		...
		</jdbc:embedded-database>
    </beans>
    
    <beans profile="prod">
		<jee:jndi-lookup id="dataSource">
		...
    </beans>
</beans>

激活 profile

Spring 会依次查找 spring.profiles.activespring.profiles.default 这两个变量来确定哪个 profile 处于激活状态,如果二者都未定义,则 Spring 只会创建那些没有定义在 profile 中的 bean。

可以同时激活多个不同的 profile

有很多种方式配置上面两个属性,例如在 web.xml 中

<?xml ...>
<web-app version="2.5" ...>
...
<context-param>
	<param-name>spring.profiles.default</param-name>
	<param-value>dev</param-value>
</context-param>
...

在生产环境可以通过配置 spring.profiles.active 环境变量实现环境切换

@ActiveProfiles

测试环境可直接使用注解激活对应的 profile

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={PersistenceTestConfig.class})
@ActiveProfiles("dev") // 直接激活指定 profile
public class PersistenceTest {
	...
}

@Conditional

@Conditional 注解,它可以用到带有 @Bean 注解的方法上。如果给定的条件计算结果为 true,就会创建这个 bean,否则的话, 这个 bean 会被忽略

歧义消除

  • @Primary,标识首选 bean
  • @Qualifier("iceCream") ,这类方法比较灵活,具体使用时还需查询文档

运行时注入

Spring 提供了两种在运行时求值的方式

  • 属性占位符(Property placeholder)
  • Spring 表达式语言(SpEL)

@PropertySource & Env

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;

@Configuration
@PropertySource("classpath:/xyz/yearn/app.properties")
public class Test{
    @Autowired
    Environment env;
    
    @Bean
    public BlankDisc disc()
    {
        // getProperty 有多种变种,用时请查 API
        return new BlankDisc(env.getProperty("disc.title"), env.getProperty("disc.artist"));
    }
}

// app.properties 的内容如下
// disc.title=Sgt.Peppers Lonely Hearts Club Band
// disc.artist=The Beatles

属性占位符 @Value

占位符使用 ${...} 包装属性名称,使用属性占位符需要先配置 PropertySourcesPlaceholderConfigurer

<context:property-placeholder />

<bean id="sgtPeppers" class="soundsystem.BlankDisc" c:_title="${disc.title}" c:_artist="${disc.artist}"/>
@Value
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
	return new PropertySourcesPlaceholderConfigurer();
}

public BlankDisc(@Value("${disc.title}") String title,
				 @Value("${disc.artist}") String artist) 
{
	this.title = title;
	this.artist = artist;
}

SpEL

SpEL 是 Spring 提供的内嵌脚本语言,SpEL 需要放置到 #{...} 之中,示例

  • #{sgtPeppers.artist}
    • SpEL 可以引用 bean 属性
  • #{T(System).currentTimeMillis()} & T(java.lang.Math).random()
    • T() 表达式会将 Java.lang.System 视为 Java 中对应的类型,因此可以调用其 static 方法
  • #{systemProperties['disc.title']}
  • 通过 systemProperties 对象引用系统属性
  • #{2 * T(java.lang.Math).PI * circle.radius}

和常见脚本语言一样,SpEL 可以计算十分复杂的计算,这部分内容请参考其他文档

面向切面编程(AOP)

AOP (Aspect Oriented Program)即面向切面编程。使用 AOP 可以剥离核心业务与辅助功能代码,减少业务程序员的心智负担:

  • 核心业务,比如登录,数据库操作等
  • 辅助功能,比如性能统计、日志、事务管理等。AOP 中的切面即为周边业务(或者辅助功能)

下面举个例子

需要添加切面的业务对象

public class ProductService {
    public void doSomeService(){
        System.out.println("doSomeService");
    }
}

切面对象

public class LoggerAspect {
    public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("start log:" + joinPoint.getSignature().getName());
        Object object = joinPoint.proceed();
        System.out.println("end log:" + joinPoint.getSignature().getName());
        return object;
    }
}

关联业务与切面(配置文件)

<aop:config>
   <!-- 定义切面位置 -->
   <aop:pointcut id="loggerCutpoint" expression="execution(* com.how2java.service.ProductService.*(..)) "/>
    <!-- 关联切面和切面对象 -->
   <aop:aspect id="logAspect" ref="loggerAspect">
   		<aop:around pointcut-ref="loggerCutpoint" method="log"/>
   </aop:aspect>
</aop:config>  

根据 AOP 配置中的 execution,ProductService 中所有方法的调用都将触发 LoggerAspect 切面

注解形式

@Aspect // 这是一个切面对象
@Component
public class LoggerAspect {
    // 指明切面的执行位置
    @Around(value = "execution(* com.how2java.service.ProductService.*(..))")
    public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("start log:" + joinPoint.getSignature().getName());
        Object object = joinPoint.proceed();
        System.out.println("end log:" + joinPoint.getSignature().getName());
        return object;
    }
}
posted @ 2019-11-13 15:28  jiahu  阅读(504)  评论(0编辑  收藏  举报