Spring入门(注解版)

本文介绍Spring框架的核心概念及注解配置方式,包括IOC控制反转、AOP面向切面编程等功能,并通过示例详细解释如何使用注解进行Bean声明、作用域设置、初始化与销毁等操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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包.

  1. 在IDEA的向导中创建一个maven项目.
  2. 创建一个启动类MainClass.java,然后编写入口函数.
  3. 在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.youkuaiyun.com/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的核心,打好这个基础,掌握其他模块就会游刃有余.

转载于:https://my.oschina.net/somereasons/blog/1617295

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值