个人技术博客(α)

α阶段主要学习了Spring技术。Spring技术作为JavaWeb框架中的重要一环,对项目的服务端搭建有重要意义。

一.部分概念

POJO(Plain Old Java Object):简单老式对象
DI(Dependency Injection ):依赖注入
AOP(Aspect-Oriented Programming):面向切面编程

二.依赖注入

1.目的

    耦合具有两面性。一方面,紧密耦合的代码难以测试、难以复用、难以理解。另一方面,耦合又是必须的----完全没有耦合的代码什么也做不了。为了完成有意义的功能,不同的类必须以适当的方式进行交互。总而言之,耦合式必须的,但应当小心谨慎地管理。
    通过DI,对象的依赖关系将由系统中负责协调个对象的第三方组件在创建对象时进行设定。对象无需自行创建或管理他们的依赖关系,依赖关系将自动注入到需要他们的对象中去。

2.方法

(1).自动化装配Bean

Spring从两个角度实现自动化装配:
    组件扫描:Spring会自动发现应用上下文中创建的bean
    自动装配:Spring自动满足bean之间的依赖

  • 1.@Component注解:这个注解表明该类会作为组件类,并告知Spring要为这个类创建bean,从而没有必要显式配置该类的bean。
package soundsystem;

import org.springframework.stereotype.Component;

@Component
public class SgtPeppers implements CompactDisc {

	public String title = "sgt. Pepper's Lonely ~~~";
	public String artist = "The Beatles";
	
	public void play() {
		System.out.println("playing "+ title +" by "+artist);

	}
}
  • 2.@ComponentScan注解:开启组件扫描,从而寻找带有@Component注解的类
package soundsystem;

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

@Configuration
@ComponentScan
public class CDPlayerConfig {

}
  • 3.@Autowired注解:实现自动装配。可以用于构造器,属性的Setter方法等等一些类的方法上。
    @Autowired
    public CDplayer(CompactDisc cd){
        this.cd = cd;
    }
注意:当Sping初始化bean时,会尽可能满足bean依赖。假如有且只有一个bean匹配的话就会被装配进来。如果没有匹配的bean,那么在应用上下文创建时,Spring会抛出异常。为了避免异常出现,可以将@Autowired的required属性设置为false:
    @Autowired(required=false)
    public CDplayer(CompactDisc cd){
        this.cd = cd;
    }
注意:如果有多个可匹配的bean的话,Spring会抛出异常,表明没有明确指出选择哪个bean进行装配。

(2).Java代码装配Bean

  • 1.创建配置类
  • 2.@Bean注解:会告诉spring这个方法会返回一个对象,该对象要注册为spring应用上下文的bean。方法体中包含了最终产生bean实例的逻辑
  • 3.借助javaconfig实现注入
 package soundsystem;

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

@Configuration
public class CDPlayerConfig {
	@Bean
	public CompactDisc sgtPeppers() {
		return new SgtPeppers();
	}
	@Bean
	public CDplayer cdplayer() {
		return new CDplay(sgtPeppers());
	}
}

(3).XML代码装配Bean

<bean id="自己命名的id号" class="所在类" >
    <construct-arg ref="构造器参数的引用对象的id" />
    <!--等价于上述构造器--->
    <!--格式为 <c:要装配的参数名-ref="   " --->
    <c:cd-ref="构造器参数的引用对象的id"/>
    <!--中间替换为参数的索引值 --->
    <c:_0-ref="构造器参数的引用对象的id"/>
    <!--中间替换为空(如果构造器只有一个参数)--->
    <c:_-ref="构造器参数的引用对象的id"/>
    <!--字面量注入值--->
     <construct-arg value=“字面量” />
    <c:_属性名=“字面量”/>
    <c:_索引=“字面量”/>
     <!--中间替换为空(如果构造器只有一个参数)--->
    <c:_=“字面量”/>
    <!-- 装配聚合 (没有相应的c标签)-->
    <construct-arg>
    <list>
        <value>值1</value>
        <value>值2</value>
        <value>值3</value>
    </list>
    </construct-arg>
    <!--设置属性--->
    <property name="属性名" ref="引用的bean的id"/>
    <p:属性名-ref="注入bean的id" />
     <!--字面量注入值--->
    <property name="属性名" value="  "/>
    <p:属性名="  "/>
    <!-- 装配聚合与之前类似,不表(没有相应的p标签)--->
    <!-- 可以使用util-命名空间对集合进行装配--->
    <util:list id="id号">
        <value>值1</value>
        <value>值2</value>
        <value>值3</value>
    </util:list>
</bean

3.一些扩展

  • 为组件扫描的bean命名、
    @Component(命名)或者@Named(命名)

  • 设置组件扫描的一些基础包
    @ComponentScan(基础包名)
    @ComponentScan(basePackage=基础包名)
    多个基础包
    @ComponentScan(basePackage={基础包名1,基础包名2})
    更加具体指定到类
    @ComponentScan(basePackageClasses={类1.class,类2.class})

  • @Autowired可以替换成@Inject

  • 为bean命名
    @Bean(name=命名)

  • 在JavaConfig中引用JavaConfig配置
    使用@Import(配置类.class)

  • 在JavaConfig中引用XML配置
    @ImportResource("路径(比如classpath):配置文件名.xml")

  • 在XML中引用XML配置

     <import resoune="配置文件名.xml"/>
    
  • 在XML中引用JavaConfig配置

 <bean class="配置包.配置类" />
  • 最好的方法是另写一个配置文件或配置类,包含着所需的配置文件名或配置类名。

4.高级装配

(1)环境与profile

问题和需求:
在实际应用的过程中,某些bean会在不同的环境中有所不同。所以需要一种方法,使其在每种环境下都会选择最合适的配置。
简单的方法:
在单独的配置类或者XML文件中配置每个bean,然后在构建阶段确定要将哪一种配置编译到可部署的应用中。
又有问题:
要为每种环境都重新构建应用。
解决方法:profile bean

  • 1.配置profile bean
    使用@Profile("命名")注解。这样子配置类中的bean只有在 命名 profile激活时才会被创建。
    技巧:在Spring3.1中只能用于类级别上。Spring3.2开始,可以在方法级别上与@Bean注解一起使用。这样的话,就能将这两个bean的声明放到同一个配置类中。

    使用XML配置profile,使用的profile属性。

    <beans profile="命名"></beans>
    

    技巧:在根 <beans>元素中嵌套定义<beans>,而不用为每个环境都创建一个profile XML文件。

  • 2.激活profile
    Spring在确定哪个profile处于激活状态时,需要依赖两个独立属性:spring.profile.activespring.profile.default。如果设置了active属性,那么它的值就会用来确定哪个profile是激活的。否则查找default的值。如果都没有设置的话,那就没有激活的profile。
    有以下方式设置这两个属性:

    • 作为DispatcherServlet的初始化参数
    • 作为Web应用上下文参数
    • 作为JDNI条目
    • 作为环境变量
    • 作为JVM的系统属性
    • 在集成测试类上,使用@ActiveProfiles注解设置
      以web.xml配置为例
    <!-- 为上下文设置默认的profile--->
    <context-param>
        <param-name>spring.profiles.default</param-name>
        <param-value>dev</param-value>
    </context-param>
    <servlet>
        <!--省去了name,class配置代码 --->
        <!--为Servlet设置默认的profile --->
        <init-param>
         <param-name>spring.profiles.default</param-name>
         <param-value>dev</param-value>
        </init-param>
         <load-on-srartup>1</load-on-srartup>
         
    </servlet>
    

    注意:spring提供了@ActiveProfiles(环境名)注解来指定激活环境

(2)条件化bean(限于spring4之后)

使用@Conditional(条件类.class)注解。
如下:

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Conditional;
    import org.springframework.context.annotation.Configuration;
    @Configuration
    public class ConditionConfig {
        //条件化创建bean
        @Bean
        @Conditional(MagicExistsCondition.class)
        public MagicBean magicBean(){
         return new MagicBean();
        }
    }
    
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class MagicExistsCondition implements Condition {
    @Override
    public boolean matches(
            ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        Environment environment = conditionContext.getEnvironment();
        return environment.containsProperty("magic");//检查环境中是否有magic属性
    }
}

通过ConditionContext,我们可以做到:

  • 借助getRegistry()返回的BeanDefinitionRegistry检查bean定义
  • 借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性
  • 借助getEnvironment()返回的Environment检查环境变量是否存在以及值
  • 读取并探查getResourceLoader()返回的ResourceLoader所加载的资源
  • 借助getClassLoader()返回的ClassLoader加载并检查类是否存在

通过借助AnnotatedTypeMetadata的isAnnotated()方法,可以判断带有@Bean注解的方法是不是还有其他特定的注解。

技巧:Spring4之后的@Profile属性基于@Condition和@Conditional实现

(3)处理自动装配的歧义性

问题与需求:
在之前的注意中我们指出若有多个可匹配的bean,spring会报异常。
解决方法:
标示与限定

  • 标示首选的bean
    法一:自动化装配下
@Component
@Primary
public class XX {...}

法二:java配置类下

@Bean
@Primary
public Dessert iceCream(){
    return new IceCrean();
}

法三:XML配置下

<bean primary="true" />
  • 限定自动装配的bean
    背景:当有多个primary标签时依旧存在歧义性
    方法

    • @Qualifier(命名)//命名不限于bean id号
      可以分别于@Component @Autowired @Bean组合使用
      另外@Qualifier标签个数不定,可以用多个@Qualifier标签进行限定
      最佳实践:为bean选择特征性或描述性的术语,而不是随意的名字。
    • 更高级的自定义注解

@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,
ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {
}

```

(4)bean的作用域

  • 单例 singleton
  • 原型 prototype
  • 会话 session
  • 请求 request
    @Scope(ConfigurableBeanFactory.作用域类型)注解
    或者
    `

(5)运行时值注入

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

  • 属性占位符
  • Spring表达式语言

属性占位符:
1.注入外部的值

@Configuration
//声明属性源
@PropertySource("classpath:/com/soundsystem/app.properties")
public class ExpressiveConfig {

    @Autowired
    Environment environment;

    @Bean
    public BlankDisc disc(){
        return new BlankDisc(
        //检索属性值(可以写上默认值)
          environment.getProperty("disc.title","Rattle and Hum"),
          environment.getProperty("disc.artist","U2")
        );
    }

在例子中,@PropertySource引用了类路径中一个名为app.properties的文件,该文件大致内容如下:

    disc.title=某值
    disc.artist=某值

三.面向切面编程

1.定义AOP术语

  • 通知
    切面的工作被称为通知

    • 前置通知:在目标方法调用之前调用通知功能
    • 后置通知:在目标方法调用之后调用通知功能,此时不关心方法的输出
    • 返回通知:在目标方法成功执行之后调用通知
    • 异常通知:在目标方法抛出异常之后调用通知
    • 环绕通知:通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义行为
  • 连接点
    在应用执行过程中能够插入切面的一个点。通俗的说就是应用通知的时机

  • 切点
    切点的定义会匹配所要织入的一个或者多个连接点。通俗的说,如果通知定义了切面的“什么”和“何时”,则切点定义了“何处”。

  • 切面
    切面是通知和切点的结合

  • 引入
    引入允许我们向现有类添加新方法或属性。

  • 织入
    织入是把切面应用到目标对象并创建新的代理对象的过程。
    在目标对象的生命周期里有多个点可以织入:

    • 编译期
    • 类加载期
    • 运行期

2.Spring对AOP的支持

  • spring通知是用Java编写的
  • Spring在运行时通知对象
  • spring只支持方法级别的连接点

3.通过切点选择连接点

在springAOP中,要使用AspectJ的切点表达式语言来定义切点。
下表列出了SpringAOP所支持的AspectJ切点指示器

AspectJ指示器 描述
arg() 限制连接点匹配参数为指定类型的执行方法
@args() 限制连接点匹配参数由指定注解标注的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配AOP代理的bean引用为指定类型的类
target 限制连接点匹配目标对象为指定类型的泪
@target() 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注释
within() 限制连接点匹配指定的类型
@within() 限制连接点匹配指定注释所标注的类型
@annotation 限定匹配带有指定注释的连接点

观察上表,可以发现只有execution()指示器是实际执行匹配的,而其他指示器都是用来限制匹配的,所以execution()是我们在编写切点定义时最主要使用的指示器。在此基础上使用其他指示器限制所匹配的切点。

(1) 编写切点

基本格式

//在方法执行时触发(返回任意类型 方法所属的类.方法(使用任意参数))
execution(* concert.Performance.perform(..))

限制匹配

//当concert包下任意类的方法被调用时执行Performance.perform()
execution(* concert.Performance.perform(..)) 
    && within(concert.*)

(2)在切点中选择bean

execution(* concert.Performance.perform())
    and bean(beanID)
    //或者and !bean(beanID) 表示不匹配该bean

4.使用注解创建切面

(1) 定义切面

  1. 一般情况
//该接口表示任何类型的现场表演
public interface Performance {
    public void perform();
}
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class Audience {

    @Before("execution(* ch04.Performance.perform(..))")
    public void silenceCellPhones(){
        System.out.println("silencing cell phones");
    }

    @Before("execution(* ch04.Performance.perform(..))")
    public void takeSeats(){
        System.out.println("taking seats");
    }

    @AfterReturning("execution(* ch04.Performance.perform(..))")
    public void applause(){
        System.out.println("CLAP!");
    }

    @AfterThrowing("execution(* ch04.Performance.perform(..))")
    public void demandRefund(){
        System.out.println("Demanding a refund!");
    }
}

上例中,Audience类使用@AspectJ注解进行标注。该注解表明Audience不仅是POJO,还是一个切面。
Audience有四个方法,定义了一个观众可能在观看演出时可能做出的行为。在演出前观众就坐takeSeats(),手机静音silenceCellPhones()。如果演出精彩则要鼓掌applause()。演出没有达到观众预期则会要求退款demandRefund()

AspectJ提供了五个注解来定义通知。@After @AfterReturning @AfterThrowing @Around @Before

  1. 通过@Pointcut注解声明频繁使用的切点表达式
@Aspect
public class Audience {
    
    //定义命名的切点
    @Pointcut("execution(* ch04.Performance.perform(..))")
    public void performance(){}

    @Before("performance()")
    public void silenceCellPhones(){
        System.out.println("silencing cell phones");
    }

    @Before("performance()")
    public void takeSeats(){
        System.out.println("taking seats");
    }

    @AfterReturning("performance()")
    public void applause(){
        System.out.println("CLAP!");
    }

    @AfterThrowing("performance()")
    public void demandRefund(){
        System.out.println("Demanding a refund!");
    }
}

(2) 启用自动代理

javaConfig法

@Configuration
//启动AspectJ自动代理
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {
    
    //声明Audience bean
    @Bean
    public Audience audience(){
        return new Audience();
    }
}

XML法

<aop:aspectj-autoproxy />
<bean class="concert.Audience" />

(3) 与众不同的通知--环绕通知

    //环绕通知方法
    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint joinPoint){
        try {
            System.out.println("silencing cell phones");
            System.out.println("taking seats");
            //一定要调用该方法
            joinPoint.proceed();
            System.out.println("CLAP!");
        } catch (Throwable throwable) {
            System.out.println("Demanding a refund!");
        }
    }
  • 可以看到这个通知所达到的效果与之前配置的前置通知和后置同志是一样的。但是,位于同一方法中。
  • 这个新的通知方法接收ProceedingJoinPoint作为参数
  • 通知方法中可以做任何事情,当要将控制权交给被通知方法时,需要调用ProceedingJoinPointproceed()方法
  • 不调用proceed()方法则会阻塞对被通知方法的调用。所以按需进行不调用或者多次调用

(4) 处理通知中的参数

表明传入被通知方法中的参数也会传递到通知中去。而且需要注意的是指定参数的名称要与切点方法签名中的参数相匹配。

execution(* 方法所属类型.方法(接收参数类型)) && args(指定参数)

(5) 通过注解引入新功能

@Aspect
public class EncoreableIntroducer {

    @DeclareParents(value = "ch04.Performance+",
            defaultImpl = DefaultEncoreable.class)
    public static Encoreable encoreable;
}

可以看出,EncoreableIntroducer是一个切面。但是没有提供前置,后置,环绕通知,而是通过@DeclareParents注解,将Encoreable接口引入到Performance bean中。

@DeclareParents注解由三部分构成:

  • value属性指定了哪种类型的bean要引入该接口
  • defaultImpl属性指定了为引入功能提供实现的类
  • @DeclareParents 注解所标注的静态属性指明了要引入的接口。

缺陷:必须为通知类添加注解,所以必须要有源码
解决方案:XML配置

5.在XML中声明切面

spring的AOP配置元素能够以非侵入性的方式声明切面

AOP配置元素 用途
<aop:advisor> 定义AOP通知器
<aop:after> 定义AOP后置通知(不管被通知的方法是否执行成功)
<aop:after-returning> 定义AOP返回通知
<aop:after-throwing> 定义AOP异常通知
<aop:around> 定义AOP环绕通知
<aop:aspect> 定义一个切面
<aop:aspectj-autoproxy> 启用@AspectJ注解驱动的切面
<aop:before> 定义一个AOP前置通知
<aop:config> 顶层的AOP配置元素。大多数<aop:*>元素必须包含在<aop:config>元素内
<aop:declare-parents> 以透明的方式为被通知的对象引入额外的接口
<aop:pointcut> 定义一个切点

(1)以之前环境为例:

<aop:config>
    <aop:aspect ref="audience"> <!-- 引用的bean --->
        <aop:before pointcut="execution(* ch04.Performance.perform(..))" method="silenceCellPhones" />
        <aop:before pointcut="execution(* ch04.Performance.perform(..))" method="takeSeats" />
        <aop:after-returning pointcut="execution(* ch04.Performance.perform(..))" method="applause" />
        <aop:after-throwing pointcut="execution(* ch04.Performance.perform(..))"method="demandRefund" />
    </aop:aspect>
</aop:config>

(2)消除重复元素:

<aop:config>
    <aop:aspect ref="audience"> <!-- 引用的bean --->
        <aop:pointcut id="performance" expression="execution(* ch04.Performance.perform(..))" />
        <aop:before pointcut-ref="performance" method="silenceCellPhones" />
        <aop:before pointcut="performance" method="takeSeats" />
        <aop:after-returning pointcut="performance" method="applause" />
        <aop:after-throwing pointcut="performance" method="demandRefund" />
    </aop:aspect>
</aop:config>

(3)环绕通知

<aop:config>
    <aop:aspect ref="audience"> <!-- 引用的bean --->
        <aop:pointcut id="performance" expression="execution(* ch04.Performance.perform(..))" />
        <aop:around pointcut-ref="performance" method="watchPerformance" />
    </aop:aspect>
</aop:config>

(4)通知传递参数

<aop:config>
    <aop:aspect ref="audience"> <!-- 引用的bean --->
        <aop:pointcut id="performance" expression="execution(* ch04.Performance.perform(int)) and args(num)" />
        <aop:before pointcut-ref="performance" method="watchPerformance" />
    </aop:aspect>
</aop:config>

(5)通过切面引入新功能

1.default-impl用全限定类名显示指定

 
<aop:aspect>
    <aop:declare-parents types-matching="ch04.Performance+" implement-interface="ch04.Encoreable" default-impl="ch04.DefaultEncoreable"/>
            
</aop:aspect>

2.delegate-ref属性引用了一个spring bean作为引入的委托

 
<aop:aspect>
    <aop:declare-parents types-matching="ch04.Performance+" implement-interface="ch04.Encoreable" delegate-ref="encoreableDelegate"/>
            
</aop:aspect>

6.注入AspectJ切面

相关背景代码

public aspect CriticAspect {

    public CriticAspect() {}

    pointcut performance() : execution(* perform(..));

    afterReturning() : performance(){
        System.out.println(criticsmEngine.getCriticsm());
    }

    private CriticsmEngine criticsmEngine;

    public void setCriticsmEngine(CriticsmEngine criticsmEngine){
        this.criticsmEngine = criticsmEngine;
    }
}

要注入AspectJ的话:

<bean class="xxxx.CriticAspect" factory-method="aspectOf">
    <!--- 其他属性配置 --->
</bean>
 

尤其要注意的是 factory-method="aspectOf"
因为spring不能负责创建Aspect,所以需要aspectOf工厂方法获得切面引用,然后像bean一样依赖注入

posted @ 2017-11-11 08:44  hughe  阅读(315)  评论(1编辑  收藏  举报