spring 基本使用
DI 依赖注入
请看下面的例子
Step.1
首先定义 Animal 接口
public interface Animal {
    public String sayHello();
}然后定义一个狗的实现类,并且添加 @Component(“dog”) 注解配置为一个 Bean
@Component("dog")
public class Dog implements Animal {
    public String sayHello() {
        return "wang wang wang !!!";
    }
}然后定义一个人,他养了一个动物
public class People {
    private Animal animal;
    public People(Animal animal) {
        this.animal = animal;
    }
    public void playWithAnimal(){
        System.out.println(animal.sayHello());
    }
}下面是一个配置文件,用于配置开启注解自动扫描,同时,我还想不使用 @Component 注解把 People 也配置为一个 Bean 可以直接配置在这个配置文件中
@Configuration
@ComponentScan(basePackageClasses = {AopSpringConfig.class, DiSpringConfig.class})
public class BaseConfiguration {
    @Bean(name = "people")
    public People getPeople(Animal animal){
        return new People( animal );
    }
}@Configuration 说明这个类可能通过 @Bean 注解声明了一些 Bean,同时还有可能进行了一些其他配置
@ComponentScan(basePackageClasses = {DiSpringConfig.class}) 这个标签用于开启自动扫描注解,配置 basePackage 有多种配置方法,推荐配置 basePackageClasses 指向包下的一个空接口,这样在重构的时候比不容易出问题
@Bean 作用跟 @Component 一样,只不过用的地方不同
public People getPeople(Animal animal) 方法需要的参数会自动注入进去
下面编写测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {BaseConfiguration.class})
public class SpringTest {
    @Autowired
    private People people;
    @Test
    public void testDi(){
        people.playWithAnimal();
    }
}需要依赖 spring-test 包
@RunWith 是用来搞定环境的,必须要写
@ContextConfiguration(classes = {BaseConfiguration.class}) 指定配置文件
@Autowired 表明需要注入 People 对象
测试发现装配成功,方法正常执行。
Step.2
在上面的基础上再编写 cat 类
@Component("cat")
public class Cat implements Animal {
    public String sayHello() {
        return "miao miao miao !!!";
    }
}然后重写运行测试,出错了,哈哈哈,如何解决?
这里问题是自动装配存在歧义,可以使用 @Primary 或者 @Qualifier() 来消除歧义
- 在 Dog 类上添加 @Primary 注解,再次运行发现输出 wang wang wang !!! 
- 在 Dog 类上添加 @Qualifier(“wang”) 在 Cat 类上添加 @Qualifier(“miao”) 然后再对 People 略作修改,不再通过 java配置方式声明为 Bean 而是通过 @Component 方式 - @Component public class People { @Autowired @Qualifier("cat") private Animal animal; public void playWithAnimal(){ System.out.println(animal.sayHello()); } }- 再次运行测试,歧义消除,@Qualifier 创建限定符可以配合 @Component @Bean 使用,但是注入时好像只能在 @Autowired 上起作用 
Step.3
现在,假设在开发的时候我们需要人养的宠物是狗,测试的时候人养的宠物是猫,应该怎么做? 用 @Profile
给 Dog 加上 @Profile(“dev”) 给 Cat 加上 @Profile(“qa”)
@Component("dog")
@Profile("dev")
public class Dog implements Animal {
    public String sayHello() {
        return "wang wang wang !!!";
    }
}
@Component("cat")
@Profile("qa")
public class Cat implements Animal {
    public String sayHello() {
        return "miao miao miao !!!";
    }
}至于具体是 dev 还是 qa 依赖于 spring.profile.active spring.profile.default 两个属性
这两个属性可以通过 jvm 参数,环境变量, @ActiveProfiles 注解等方式配置,这里我们在 SpringTest类上添加注解 @ActiveProfiles(“dev”) 把环境配置为 dev 测试,运行测试输出 wang wang wang !!!
Step.4
现在我的系统中有一个属性叫做 animal.type 假设我希望如果这个属性值等于 cat 则调用 cat 否则调用 dog 该怎么做?
使用 @Conditional() 注解可以完成上面的要求 @Conditional() 注解需要一个实现了 condition 接口的类作为判断逻辑
public class DogConditionImpl implements Condition {
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment env = context.getEnvironment();
        return env.containsProperty("animal.type")&&env.getProperty("animal.type").equals("Dog");
    }
}
@Component("dog")
@Conditional(DogConditionImpl.class)
public class Dog implements Animal {
    public String sayHello() {
        return "wang wang wang !!!";
    }
}
public class CatConditionImpl implements Condition {
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment env = context.getEnvironment();
        return env.containsProperty("animal.type")&&env.getProperty("animal.type").equals("cat");
    }
}
@Component("cat")
@Conditional(CatConditionImpl.class)
public class Cat implements Animal {
    public String sayHello() {
        return "miao miao miao !!!";
    }
}在运行 java 测试的时候,启动参数添加 -Danimal.type=cat 然后运行测试类,输出 miao miao miao !!!
Step.5
假设现在这个人养了两只猫,然后两只猫每次叫都能记录自己叫了几次,代码如下
@Component("cat")
public class Cat implements Animal {
    public int i = 0;
    public String sayHello() {
        ++i;
        return "miao : " +i;
    }
}
@Component
public class People {
    @Autowired
    private Animal cat1;
    @Autowired
    private Animal cat2;
    public void playWithAnimal(){
        System.out.println(cat1.sayHello());
        System.out.println(cat2.sayHello());
    }
}然后测试发现运行结果为
miao : 1
miao : 2原来spring 注入的两个对象,是用一只猫,这里就要涉及到spring 的作用域了,spring 定义了四个作用域:
* 单例(ConfigurableBeanFactory.SCOPE_SINGLETON) 整个应用只有一个实例
* 原型(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 每次注入都是创建一个新的实例
* Session 用于web 一个会话一个
* Request 用于web 一个请求一个作用域可以通过 @Scope() 来修改,我们可以把猫类的作用域注解为原型 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 来解决这个问题
Step.6
目前为止小猫小狗还有人的使命已经结束了。下面来看一看运行时值的注入
- 创建 app.properties 随便输入点内容 
 com.acyouzi.spring=test
 lol=happy
- 使用 @PropertySource(“app.properties”) 引入 app.properties - @Configuration @ComponentScan(basePackageClasses = {AopSpringConfig.class, DiSpringConfig.class}) @PropertySource("app.properties") public class BaseConfiguration { }
- 使用 Environment 变量访问属性值 - @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {BaseConfiguration.class}) public class SpringTest { @Autowired Environment env; @Test public void simpleReadPropertyTest(){ System.out.println("com.acyouzi.spring = " + env.getProperty("com.acyouzi.spring")); System.out.println("lol = " + env.getProperty("lol")); } }- 注意需要注入 Environment 
- 使用 @Value + 占位符,占位符的形式是 “${…}”, 注意两个 {} 中间不能有空格 - @Component public class ReadProperty { @Value("${com.acyouzi.spring}") private String spring; private String lol; public ReadProperty( @Value("${lol}") String lol) { this.lol = lol; } @Override public String toString() { return "ReadProperty{" + "spring='" + spring + '\'' + ", lol='" + lol + '\'' + '}'; } } 输出 ReadProperty{spring='test', lol='happy'}- @Value 可以用在属性上,也可以用在函数上 
 @Value 内部可以直接写普通字符串,也可以写占位符,SpEL 表达式
SpEL 表达式
SpEL 的形式是 “#{…}” ,表达式能够使用 bean 的 id 来引用 bean, 调用方法,访问对象属性,算数逻辑关系运算,正则表达式,集合操作
"#{ systemProperties['com.acyouzi.spring'] }" 
"#{ 'aaabbccdd' marches 'b+c' }"
"#{ T(java.lang.Math).random() }"
"#{ readProperty.toString() }"总之 SpEL 很强大,spel 中还有判断空值的类型安全运算符( ?. ), 集合索引运算, 过滤集合的查询运算 xxx.?[],xxx.^[],xxx.![] ,但是 SpEL 在程序里缺乏可见性,不容易调试,所以最好不要写太复杂的。
AOP
Spring Aop 是在运行时织入的,基于动态代理实现的,所以 Spring Aop 只支持方法连接点, Spring Aop 有如下5种类型的通知
- 前置通知
- 后置通知
- 返回通知
- 异常通知
- 环绕通知
Spring 借助 AspectJ 的切点表达式来语言来定义 Spring 切面,但是 Spring 仅支持部分切点表达式,但是实际上貌似我一般只使用 execution() 指示器,同时 spring 还引入了一个新的 bean() 指示器,还有下面几个指示器,我也不太会用
- within:用于匹配指定类型内的方法执行;
- this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;
- target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;
- args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;
- @within:用于匹配所以持有指定注解类型内的方法;
- @target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;
- @args:用于匹配当前执行的方法传入的参数持有指定注解的执行;
- @annotation:用于匹配当前执行方法持有指定注解的方法;
下面简单介绍切点的编写
execution( modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern (param-pattern) throws-pattern? )上面就是切点表达式的格式,带 ? 的代表可以省略。下面举几个例子
- 匹配任意访问修饰符,任意返回值的 com.acyouzi.spring.aop.Admin 类的 任意数量类型参数的 managementSystem 方法 - @Before("execution(** com.acyouzi.spring.aop.Admin.managementSystem(..))") @Before("execution(* com.acyouzi.spring.aop.Admin.managementSystem(..))")
- 匹配以 set 开头的任意方法 - @Before("execution(* set(..))")
- 匹配 com.acyouzi.spring.aop 包下所有类的所有方法 - @Before("execution(* com.acyouzi.spring.aop.*.*(..))")
- 匹配 com.acyouzi.spring 包及其子包下所有类的所有方法 - @Before("execution(* com.acyouzi.spring..*.*(..))")
- 匹配 com.acyouzi.spring.aop.Admin 类下的 managementSystem 方法,但是只有 bean 的 ID 为 admin - @Before("execution(* com.acyouzi.spring.aop.Admin.managementSystem(..)) and bean('admin') ")
- 匹配 com.acyouzi.spring.aop.Admin 类下的 managementSystem 方法,但是只有 bean 的 ID 不为 admin - @Before("execution(* com.acyouzi.spring.aop.Admin.managementSystem(..)) and !bean('admin') ")
下面是一个例子
step.1
现在有一个管理员,他可以管理系统,但是管理员只能管理 ID 比他大的,并且不能是特权ID 110 的账户:
@Component()
public class AdminAction {
    public int myid = 10;
    public boolean managementSystem(int id) throws Exception {
        if( id == 110 ){
            throw new Exception(" id not found");
        }
        return id > myid ;
    }
}现在希望能记录管理员的每一次操作,并且对成功或者失败的结果进行记录,可以写一个切面,并注册为 Bean:
@Component
@Aspect
public class LogService {
    @Before("execution(* com.acyouzi.spring.aop.AdminAction.managementSystem(..))")
    public void haveVisit(){
        System.out.println("有一条访问");
    }
    @AfterReturning("execution(* com.acyouzi.spring.aop.AdminAction.managementSystem(..))")
    public void normalState(){
        System.out.println("访问完成");
    }
    @AfterThrowing("execution(* com.acyouzi.spring.aop.AdminAction.managementSystem(..))")
    public void exceptionState(){
        System.out.println("访问出现问题");
    }
}
// 配置类上添加注解
@EnableAspectJAutoProxy
public class BaseConfiguration {}@Aspect 注解表示这是一个切面
@Before 注解表示在切点表达式匹配的方法执行前执行服务方法。
@AfterReturning 方法正常结束后调用
@AfterThrowing 方法抛出异常时调用
还有一个 @After 方法,在正常或者异常结束方法时都会调用
这几个注解都是 AspectJ 的注解,所以需要引入 AspectJ 依赖
compile group: 'org.aspectj', name: 'aspectjrt', version: '1.8.9'
compile group: 'org.aspectj', name: 'aspectjweaver', version: '1.8.9'下面是测类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {BaseConfiguration.class})
public class SpringTest {
    @Autowired
    AdminAction admin;
    @Test
    public void AopTest() throws Exception {
        admin.managementSystem(210);
    }
}可以运行测试,看到日志输出
step.2
上面的切面,相同的表达式我们重复了三遍,这个对后面代码重构,表达式维护不太友好,我们可以通过 @Pointcut 注解定义一个可重用的切入点
@Component
@Aspect
public class LogService {
    @Pointcut("execution(* com.acyouzi.spring.aop.AdminAction.managementSystem(..)) and !bean('adminAction')")
    public void managementSystemPointCut(){}
    @Before("managementSystemPointCut()")
    public void haveVisit(){
        System.out.println("有一条访问");
    }
    @AfterReturning("managementSystemPointCut()")
    public void normalState(){
        System.out.println("访问完成");
    }
    @AfterThrowing("managementSystemPointCut()")
    public void exceptionState(){
        System.out.println("访问出现问题");
    }
}step.3
上面这些切面方法,完全可以写在一个方法里面,而且更方便进行控制,可以使用环绕注解 @Around 实现
@Aspect
public class LogService {
    @Pointcut("execution(* com.acyouzi.spring.aop.AdminAction.managementSystem(..))")
    public void managementSystemPointCut(){}
    @Around("managementSystemPointCut()")
    public boolean watchManagement(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("一次访问:");
        try {
            boolean res = (Boolean)joinPoint.proceed();
            System.out.println("正常结束");
            return res;
        } catch (Throwable throwable) {
            System.out.println("发生一次");
            throw throwable;
        }
    }
}这里有几点需要注意
@Around 注解的方法 需要有一个 ProceedingJoinPoint joinPoint 参数,这个参数用来调用被调用的对象。
这个方法返回值必须与表达式匹配的实际方法的返回值一致
joinPoint.proceed() 会最终调用实际方法,返回值是实际方法的返回值,但是类型是 Object 类型,需要我们自己转换。
实际上,在 @Around 注解的方法中可以完成错误重试,安全验证等多种任务
step.4
现在新的需求是希望能够记录每次请求的参数值,并且每次请求参数值,以及方法返回参数值,可以写成下面的形式
@Aspect
public class LogService {
    @Pointcut("execution(* com.acyouzi.spring.aop.AdminAction.managementSystem(int)) && args(id)")
    public void managementSystemPointCut(int id){}
    @Before("managementSystemPointCut(id)")
    public void haveVisit(int id){
        System.out.println("有一条访问 : "+id);
    }
    @Around("managementSystemPointCut(id)")
    public boolean watchManagement(ProceedingJoinPoint joinPoint,int id) throws Throwable {
        System.out.println("一次访问 : "+id);
        try {
            boolean res = (Boolean)joinPoint.proceed(new Object[]{id});
            System.out.println("正常结束 : " +res);
            return res;
        } catch (Throwable throwable) {
            System.out.println("发生异常");
            throw throwable;
        }
    }
}注意切点表达式 execution(* com.acyouzi.spring.aop.AdminAction.managementSystem(int)) && args(id) 多个一个 args 这个的值要与 PointCut 还有 各种通知方法的参数一致。
另外其实 Before After AfterReturning AfterThrowing,几个事件都可以注入 JoinPoint 对象,完全可以通过这个对象获得方法参数,而不用去修改切点表达式,我觉得使用 JoinPoint 获取输入参数要好过使用上面的这种方法
other
Before AfterReturning AfterThrowing里面可以注入 JoinPoint,注意 JoinPoint 相比 @Around 注入的 ProceedingJoinPoint 少了 proceed 方法。
@AfterReturning 可以注入结果
@AfterThrowing 可以注入异常值
@Before("managementSystemPointCut(id)")
public void haveVisit(JoinPoint jp , int id){
    System.out.println(jp.getArgs()[0]);
    System.out.println("有一条访问 : "+id);
} 
@AfterReturning(value = "execution(* com.acyouzi.spring.aop.AdminAction.managementSystem(..))", returning = "res")
public void haveVisit( boolean res ){
    System.out.println("res : "+res);
}
@AfterThrowing(value = "execution(* com.acyouzi.spring.aop.AdminAction.managementSystem(..))",  throwing = "res")
public void throwVisit( Exception res ){
    System.out.println("res : "+res);
}至于,配置 xml 使用 spring 的方式…让xml去死吧