您的位置:首页 > 编程语言 > Java开发

Spring入门(注解版)

2018-02-02 00:00 411 查看
摘要: Spring-boot是当前java最热门的框架.被应用于大量的项目中.尤其是现在微服务的风潮,更加速了spring-boot的普及.要用好spring-boot,需要了解它的核心spring.但是,现有网络上的spring入门教程,都是基于XML配置,很少有基于注解的.而在spring-boot中,已经很难见到XML配置了.这就给spring的学习带来了脱节,甚至让人产生这些注解是spring-boot的独有功能的误解.因此,本人写了一个补充性质的教程,讲述一下如何通过注解使用spring框架.

Spring入门注解版

Spring-boot是当前java最热门的框架.被应用于大量的项目中.尤其是现在微服务的风潮,更加速了spring-boot的普及.要用好spring-boot,需要了解它的核心spring.但是,现有网络上的spring入门教程,都是基于XML配置,很少有基于注解的.而在spring-boot中,已经很难见到XML配置了.这就给spring的学习带来了脱节,甚至让人产生这些注解是spring-boot的独有功能的误解.因此,本人写了一个补充性质的教程,讲述一下如何通过注解使用spring框架.

前言

虽然是补充性质的教程,但仍然有必要介绍一下spring框架.Spring框架的核心是IOC(控制反转)和AOP(基于切面编程),这两个功能是面向对象编程的补充.IOC使得类的声明和初始化被集中到一个地方控制,降低了类的耦合,而AOP将面向对象编程中基于继承的树状结构加入了截面的概念,使得业务类能够更为专注.这两个功能,像胶水一样把其他框架整合进来.最终形成了spring的庞大生态.
但是,spring并不是项目开发的唯一方案.php等脚本语言即使没有类的概念(最近才加入),也发展的很好.在项目开发中,没有最好的语言和框架,只有最合适的框架.

创建项目

本文是用IntelliJ IDEA做开发.从空项目开始,一步一步搭建spring项目.但是为了引用包方便,还是引入了maven来管理项目,免得还要手工下载和引入jar包.

在IDEA的向导中创建一个maven项目.

创建一个启动类MainClass.java,然后编写入口函数.

在pom中添加spring的引用.

<!--核心-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.0.3.RELEASE</version>
</dependency>
<!--注解-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.3.RELEASE</version>
</dependency>

IOC控制反转

Spring最核心的就是IOC了,因此先演示IOC的使用

声明bean

为了对比,这里先用XML方式写一个例子.
创建beans.xml,并放置在resources文件夹下(一定要放在这里,否则编译的时候会被maven更改),内容如下.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="testBean1" class="win.somereason.test.spring.TestBean1">
<property name="prop1" value="123456789"/>
</bean>
</beans>

这里声明了一个TestBean1,有一个属性prop1,这个类定义如下:

public class TestBean1 {
private int prop1;

public int getProp1() {
return prop1;
}

public void setProp1(int value) {
prop1 = value;
}
}

然后修改入口函数

public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
TestBean1 myBean = ctx.getBean(TestBean1.class);
System.out.println(myBean.getProp1());
}

运行程序,会输入默认值123456789.在这里spring通过配置文件注册了所有需要管理的bean,并能给生成的bean属性赋予默认值.接下来看看通过注解,不用xml文件是如何实现同样功能的.

首先将TestBean1修改为:

@Component
public class TestBean1 {
@Value("123456789")
private int prop1;

public int getProp1() {
return prop1;
}

public void setProp1(int value) {
prop1 = value;
}
}

然后将入口函数修改为:

public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("win.somereason.test.spring");
ctx.refresh();
TestBean1 myBean = ctx.getBean(TestBean1.class);
System.out.println(myBean.getProp1());
}

这里可以比较一下差别.首先,应用上下文加载器改变了,从ClassPathXmlApplicationContext变成了AnnotationConfigApplicationContext,而TestBean1也添加了注解Component.
AnnotationConfigApplicationContext通过的scan方法,扫描指定包(以及子包)下的所有带有@Component、@Repository、@Service、@Controller的类,并将其认作bean.然后通过getBean就可以获得实例了.
这几个注解的区别:

@Service用于标注业务层组件

@Controller用于标注控制层组件(如struts中的action)

@Repository用于标注数据访问组件,即DAO组件

@Component泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。

在实际项目中,获取bean的实例常常使用 @Autowired 注解.通常在项目中,impl层引用business层,business层再引用dao层.各个层之间的引用,不会用笨拙的getBean方法,而是直接使用Autowared注解,例子如下:

@Component
public class CreateClassTest {
@Autowired
TestBeanParent testBean2;
//支持list,map,set,props
@Autowired
List<TestBeanParent> testBean2List;

public void run() {
testBean2.setProp1("!!!!!!!!!!!!!!!!!!!!!!!!");
System.out.println(testBean2.getProp1());
}
}

在这里,在CreateClassTest是bean的前提下,当你获取了CreateClassTest的bean实例,spring会给有autowared注解的成员自动创建实例.这一点在实际项目中大量用到,以常见的spring mvc项目为例,由于框架会自动创建impl层所有Controller的实例,controller中引用的Business层\facade层的类,都会在autowared注解的作用下被自动创建.所以,做过springboot项目就会发现,根本没见过getBean方法,更没有见过AnnotationConfigApplicationContext了.

Bean的作用域

Bean作用域是值bean的声明方式,常用的有singleton单例模式(每次getBean都返回同一个的实例),和prototype原型模式(每次getBean都返回一个新的实例).Spring 框架还支持request,session,global-session这三种作用域,主要用于web开发.
默认的作用域是singleton.

名称作用
singleton该作用域将 bean 的定义的限制在每一个 Spring IoC 容器中的一个单一实例(默认)。
prototype该作用域将单一 bean 的定义限制在任意数量的对象实例。
request该作用域将 bean 的定义限制为 HTTP 请求。只在 web-aware Spring ApplicationContext 的上下文中有效。
session该作用域将 bean 的定义限制为 HTTP 会话。 只在web-aware Spring ApplicationContext的上下文中有效。
global-session该作用域将 bean 的定义限制为全局 HTTP 会话。只在 web-aware Spring ApplicationContext 的上下文中有效。
设置作用域的方式是添加注解

@Component
@Scope(scopeName = "prototype")
public class TestBean1 {
@Value("123456789")
private int prop1;

public int getProp1() {
return prop1;
}

public void setProp1(int value) {
prop1 = value;
}
}

还有一个问题是,不同作用域,初始化的时间是不同的.

对于singleton类型的bean,会在ApplicationContext.refresh()的时候生成实例,同时执行初始化代码.

如果是prototype类型,会在getbean的时候才生成实例

public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("win.somereason.test.spring");
ctx.refresh();//对于singleton,执行初始化代码
TestBean1 myBean = ctx.getBean(TestBean1.class);//如果是prototype,那么类会在getBean的时候执行初始化代码
System.out.println(myBean.getProp1());
}

初始化和销毁

spring支持在bean初始化和销毁的时候调用相应的函数,通过@PostConstruct和@PreDestroy就可以.

@Component
public class TestBean1 {
@PostConstruct
public void init(){
System.out.println("TestBean1初始化");
}
@PreDestroy
public void destory(){
System.out.println("TestBean1要去了~");
}
}

此外,spring还支持另外一种方式初始化,也就是继承InitializingBean, DisposableBean接口.

@Component
public class TestBean1 implements InitializingBean, DisposableBean {
public void afterPropertiesSet() {
System.out.println("TestBean1初始化");
}

public void destroy() {
System.out.println("TestBean1要去了~");
}
}

注意,要销毁bean,需要调用applicationContext.registerShutdownHook(),而且如果bean是单例模式,才会起作用.如果是prototype模式,那么需要用其他方式触发销毁bean的动作.

另外,初始化也不能不提到构造函数.通过构造函数初始化更加自然一些.这里写一个普通的bean和一个有构造函数的bean

@Component()
@Scope(scopeName = "prototype")
public class TestBeanParent {
@Value("我是默认值")
protected String prop1;

public String getProp1() {
return prop1;
}

public void setProp1(String value) {
prop1 = value;
}
}

@Component
@Scope(scopeName = "prototype")
public class TestBeanConstructor {
public TestBeanConstructor(TestBeanParent para) {
System.out.println(String.format("构造函数接收参数,参数内容%s", para.getProp1()));
}
}

入口函数

public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("win.somereason.test.spring");
ctx.refresh();

TestBeanParent myBean2 = applicationContext.getBean(TestBeanParent.class);
TestBeanConstructor myBean5 = applicationContext.getBean(TestBeanConstructor.class, myBean2);
}

使用构造函数,不需要做任何额外的配置(反而xml方式是需要的),而且可以传入参数.传入的参数可以是bean,也可以不是.

继承关系

Spring支持bean的继承,继承分为两种情况,继承接口以及继承父类

接口继承

public interface MulitImplyInterface {
void printMsg();
}
@Component
@Scope(scopeName = "prototype")
public class MulitImply2 implements MulitImplyInterface {
public void printMsg() {
System.out.println("我是MulitImply2");
}
}
@Componen
@Scope(scopeName = "prototype")
public class MulitImply1 implements MulitImplyInterface {
public void printMsg() {
System.out.println("我是MulitImply1");
}
}

public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.scan("win.somereason.test.spring");
applicationContext.refresh();

MulitImply1 m1 = applicationContext.getBean(MulitImply1.class);
m1.printMsg();
MulitImply2 m2 = applicationContext.getBean(MulitImply2.class);
m2.printMsg();
MulitImplyInterface m3 = applicationContext.getBean(MulitImplyInterface.class);//报错
m3.printMsg();
}

接口的继承,会有一个问题,getBean获取类的话,不会有任何问题(也就是m1和m2都可以顺利实例化).但是如果获取的是接口的话,会报错,例子程序中,运行到声明m3那句就会报错,内容为
Exception in thread "main" org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'win.somereason.test.spring.package1.MulitImplyInterface' available: expected single matching bean but found 2: mulitImply1,mulitImply2
也就是说,当接口有1个实现的时候,这样写没有问题,但是当接口有2个以上实现的时候,spring就不知道应该实例化哪个类了.
要解决这个问题,可以在bean的声明中加入Primary注解,这样生成实例的时候就会生成Primary所指定的类实例.

@Component
@Primary
public class MulitImply2 implements MulitImplyInterface {
public void printMsg() {
System.out.println("我是MulitImply2");
}
}

当然,也可以在实例化的时候指定要哪个类.

MulitImplyInterface m3 = applicationContext.getBean(MulitImply2.class);

继承父类

对父类的继承,也会遇到和接口继承一样的问题.建议在父类的bean定义上加上primary, 否则会出现 expected single matching bean but found 2的错误.

@Component
@Primary//在继承的情况下,这是防止扫描出的bean重复的关键,要加到父类上.
public class TestBeanParent {
}
@Component
public class TestBeanChild extends TestBeanParent {
}
public static void main(String[] args){
//------------加载applicationContext----------------

TestBeanParent myBean2 = applicationContext.getBean(TestBeanParent.class);//报错
}

如果不在父类上添加@Primary,在获取TestBeanParent就会报错.

AOP基于切面编程

AOP是另一个杀手级的功能.有一篇文章说的很好.http://blog.csdn.net/javazejian/article/details/56267036.这篇文章总结了SPRING AOP的原理以及使用.但仍然使用XML配置作为例子.

要在我们的项目中使用AOP,需要引入spring-aop包.给maven项目添加引用

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.3.RELEASE</version>
</dependency>

然后在配置ApplicationContext的时候,打开对AOP的支持

public static void main(String[] args) {
AnnotationConfigApplicationContext appContext = new AnnotationConfigApplicationContext();
appContext.register(AnnotationAwareAspectJAutoProxyCreator.class);//打开AOP的支持
appContext.scan("win.somereason.test.spring.package2");
appContext.refresh();
WorkerInterface worker = appContext.getBean(WorkerInterface.class);//定义的bean,代码如下:
worker.work();
}

@Component
public class Worker {
public String work(){
System.out.println("函数执行了");
return "工作结果";
}
}

我们的项目仍然基于IOC,和之前的例子相比,并没有太大的变化,对bean没有添加任何特殊的东西,只是在AnnotationConfigApplicationContext中添加了对AOP的支持而已.这就是AOP的基本思想,让bean专注于业务逻辑,把通用的部分,如业务中需要用到的事物,日志等提取到单独的类中.看起来,像是对业务流插入了一个一个的切片.这就是面向切面编程的名字的由来.

切面的定义很简单,例子所示:

@Aspect
@Component
public class Aspect1 {
@AfterReturning(returning = "ret", pointcut = "execution(* win.somereason.test.spring.package2.*.*(..))")
public void finishLog(Object ret) {
System.out.println(String.format("函数执行完了,返回值%s", ret.toString()));
}

@Before(value = "execution(* win.somereason.test.spring.package2.*.*(..))")
public void BeforeLog() {
System.out.println("函数开始");
}
}

在这里定义了一个切面类,可以把一个功能作为一个切面,比如日志有一个切面类,错误捕获有一个切面类.注意切面类需要是bean,所以我们还加上了@Component的注解.

切面里的每个函数,叫切入点,通过注解,我们可以确定执行的时机,如AfterReturning是在返回值之后执行,Before是在函数执行之前执行.pointcut确定了要对哪些函数进行切入.execution(* win.somereason.test.spring.package2..(..)),代表要切入的函数为win.somereason.test.spring.package2下所有类的所有函数进行切入,这个写法提供了若干通配符,有很高的灵活性.

执行代码,可以看到输出为:

函数开始
函数执行了
函数执行完了,返回值工作结果

切入点类型

spring提供了很多类型的切入点

@Before("切入点指示符()")//函数之前
public void doBeforeTask(JoinPoint joinPoint){
...
}
@After("切入点指示符()")//函数之后
public void doAfterTask(JoinPoint joinPoint){
...
}
@AfterReturning(pointcut = "切入点指示符()", returning="retVal")//获取返回值之后
public void doAfterReturnningTask(JoinPoint joinPoint, Object retVal){
// you can intercept retVal here.
...
}
@AfterThrowing(pointcut = "切入点指示符", throwing="ex")//抛出异常之后
public void doAfterThrowingTask(Exception ex){
// you can intercept thrown exception here.
...
}
@Around("切入点指示符")//环绕,通过joinPoint,可以函数很多信息,并invoke,自由度非常高
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知前");
//执行目标函数
Object obj= (Object) joinPoint.proceed();
System.out.println("环绕通知后");
return obj;
}

注意joinPoint参数是可以省略的.

切入点指示符

切入点指示符,是一种支持若干通配符的表达式,在查找到对应名称的函数之后,就可以注入.表达式分为两部分,如execution(* win.somereason.test.spring.package2..(..)),前者execution指定了查找的范围,这里指定是函数,此外还有其他类型;后者查找的表达式,这个例子指定查找可见性为任意(最前面的*),处于包win.somereason.test.spring.package2下所有子包中所有函数,参数类型为任意(括号里的两个点).
查找范围包括:

execution:查找指定的函数,如execution(* win.somereason.test.spring.package2..(..))

within:查找指定的包,接口,类里的函数,within(win.somereason.test.spring.package2)

this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;

target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;

args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;

@within:用于匹配所以持有指定注解类型内的方法;

@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;

@args:用于匹配当前执行的方法传入的参数持有指定注解的执行;

@annotation:用于匹配当前执行方法持有指定注解的方法;

bean:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法;

reference pointcut:表示引用其他命名切入点,只有@ApectJ风格支持,Schema风格不支持。

表达式支持的通配符包括

(..) :匹配方法定义中的任意数量的参数,此外还匹配类定义中的任意数量包

(+) :匹配给定类的任意子类

(*) :匹配任意数量的字符

结语

以上是spring的核心,打好这个基础,掌握其他模块就会游刃有余.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  spring 注解 IOC AOP