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去死吧