Spring框架搭建指南(一)基本篇

前言:整理一下Spring框架的基本使用

1.Spring框架有两大最核心的功能:IOC(控制反转/依赖注入),AOP(面向切面)。

1.1 控制反转

所谓控制反转,就是将创建对象的权利交给Spring容器管理,也就是说你不需要再去new一个对象,这个事情Spring会帮你做好,你只需要告诉容器你需要什么对象就OK 了。
那么,为什么只是这样的一项功能会被认为是Spirng最重要的功能之一呢,我们自己难道不能去new一个对象吗?我们当然可以选择自己new一个对象,但是这样就会增强组件之间的耦合度,使我们后期的优化维护变得很麻烦,需要改动很多代码。
例如:有一个电视剧需要一个男主角,而这个男主角就由霍建华饰演。我们用java就可以这样表现:

public interface actor{
	play(String dialogue)
}
public class huojianhua implements actor{
	@Override
	play(String dialogue){
		System.out.println(dialogue);
	}
}
public class TVplay{
	public actor a;
	public String dialogue="水善利万物而不争,处众人之所恶,故几于道。师傅,善人者,不善人之师,不善人者,善人之资,请掌门允许弟子,成为景兄弟的手下。";
	public void play(){
		this.a=new huojianhua();
		this.a.play()
	}	
}

此时我们可以看到上面的代码中,我们自己new了一个huojianhua对象来充当主角。那么如果想更换演员的话,我就得重新new实现了actor接口的对象,并把它放到play方法中。如果只有这一个接口是这样还可这样解决。但是如果有很多个接口都是类似的问题,那一个个去换就很麻烦了。这时候Spring的IOC作用就体现出来了,将所有的对象都交给Spring管理,我只需要告诉Spring我需要一个actor对象就行了,不管他是霍建华还是胡歌。

1.2 面向切面

所谓面向切面就是对某些业务方法进行方法增强,而不改动业务方法中的代码。打个比方,我有一个播放音乐的方法,现在需要统计一下音乐的播放次数。如果我们直接去修改方法中的代码,那当其他方法也需要进行统计的话,我们又要重复添加代码。所以这时候我们就可以面向切面编程。将这些代码抽离出来做成一个方法,然后切入到需要统计的业务方法中。

下面介绍一下AOP的一些概念
(1) 通知(Advice):就是你想要实现的功能,在上面的例子中,通知就是用来统计播放次数的方法,其他的类似日志,安全之类的功能也可以使用通知去实现。
(2) 连接点(JoinPoint):就是通知具体在什么时候执行,你可以选择在目标方法开始前执行,方法结束后执行,发生异常时执行等等
(3) 切入点(Pointcut):切入点就是你想要将通知织入到哪些方法上,织入在方法前还是再方法后等等。
(4) 切面(Aspect):切面就是切入点和通知的结合,通知说明了要干什么事,切入点说明了要将通知切入到哪些地方

2. 引入Maven依赖包

<dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-core</artifactId>
                <version>5.1.7.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aop</artifactId>
                <version>5.1.7.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-beans</artifactId>
                <version>5.1.7.RELEASE</version>
            </dependency>
             <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>5.1.7.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-web</artifactId>
                <version>5.1.7.RELEASE</version>
            </dependency>
                        <dependency>
                <groupId>commons-logging</groupId>
                <artifactId>commons-logging</artifactId>
                <version>1.2</version>
            </dependency>
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>1.2.17</version>
            </dependency>

3.IOC容器管理

要让Spring容器管理所有的对象,必须要先将需要管理的bean加入到容器中,这样才能从容器中获得你想要的对象,Spring框架提供了XML和java两种配置方法,首先我们来看看怎么使用xml文件注册bean到容器中。

xml管理容器
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:context="http://www.springframework.org/schema/context"
     xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

</beans>

上面的代码是Spring配置文件的基本结构,读者可以先复制代码到一个xml中。取名时注意规范,以便日后维护。
Spring注册bean到容器中有三种方式,使用bean标签注册,基于注解方式注册,java方式注册。我们通常用的都是第二种方式,因为这种方式方便快捷。下面先介绍前两种注册bean的方法

使用bean标签注册
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:context="http://www.springframework.org/schema/context"
      xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
       
   <bean id="Music" class="FrameWork.bean.Music"></bean>
   
   <bean id="UserService" class="FrameWork.ServiceImpl.UserServiceImpl">
       <constructor-arg name="user" ref="User" type="FrameWork.bean.User"></constructor-arg>
   </bean>
   
   <bean id="MusicService" class="FrameWork.ServiceImpl.MusicServiceImpl">
       <property name="music" ref="Music"></property>
   </bean>
   
</beans>

bean标签是专门用于注册bean到Spring容器中的。在上面的代码中bean标签内还有property ,constructor-arg

id属性代表bean在容器中的唯一标识。

class属性指定了bean的全类名。

name属性是bean的名称标识也不能重复,在注入时会用到此属性。

factory-method属性用于指定生产bean的静态工厂方法。使用此属性的bean,容器会调用该此方法获取一个bean返回给用户。

factory-bean属性用于指定生产对象要使用的工厂类,它的值是一个bean的引用id,必须配合factory-method属性一起使用,使用该属性不需要指定class属性。

init-method和destory-method属性代表初始化,销毁bean时调用的方法。

scope属性代表bean的作用域,总共有四个值,singleton代表单例,整个IOC容器只会创建一次,以后每次获取该对象是都是同一个,prototype代表每次获取都会创建新的对象,request代表每次HTTP请求将会生成各自的bean实例,session代表每次会话请求对应一个bean实例。

基于注解方式注册
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:context="http://www.springframework.org/schema/context"
      xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
//开启注解扫描
<context:component-scan base-package="FrameWork.ServiceImpl"></context:component-scan>

</beans>
@Service
public class MusicServiceImpl implements MusicService {

   @Autowired
   private Music music;

   @Override
   public void setMusic(Music music) {
           this.music=music;
       System.out.println("设置音乐信息");
   }

   @Override
   public Music getMusic(Music music) {
           return this.music;
   }

   @Override
   public void listenMusic(Music music) {
       System.out.println("正在收听音乐"+music);
   }
}

component-scan标签代表组件扫描,base-package属性指定要扫描的包路径,这个标签会查找base-package属性路径下所有被构造型(stereotype)注解所标注的类,比如@Component,@Repository,@Service,@Controller。

bean加入容器后,我们可以通过以下方式从容器中获取实例。
方式1:手动加载

ApplicationContext applicationContext=new ClassPathXmlApplicationContext("config/Spring_Config.xml");
        applicationContext.getBean(MusicService.class);
        musicService=(MusicServiceImpl)applicationContext.getBean(MusicService.class);
        musicService.listenMusic(new Music());

方式2:web.xml

<!DOCTYPE web-app PUBLIC
       "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
       "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
   <display-name>Archetype Created Web Application</display-name>

   <context-param>
       <param-name>contextConfigLocation</param-name>
       <param-value>classpath:config/Spring_Config.xml</param-value>
   </context-param>

   <listener>
       <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
   </listener>

</web-app>

web应用可用方式二加载配置文件,ContextLoaderListener类实现了Servlet API 中的ServletContextListener 接口。这个接口可以监听servletContext的生命周期,实际也相当于监听了web应用的生命周期,web应用的启动和终止都会触发ServletContextEvent 事件,这个事件是由ServletContextListener 接口处理的,所以ContextLoaderListener就可以在web应用启动时自动帮你加载配置文件。
要获取bean时,只需要给要注入实例的成员,添加@Autowire之类的注入类注解即可使用。
PS:注意使用xml配置Spring,要用@Autowire必须在容器注册下面这个bean

<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/>

使用java配置的不需要注册这个bean,原因不明,怀疑是java配置在加载配置文件时使用了AnnotationConfigWebApplicationContext类的原因。

若要获取applicationcontext可以加入下面这行代码:

WebApplicationContext webApplicationContext=WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());

WebApplicationContext 继承了applicationcontext类,所以你直接调用getBean方法获取你想要的对象。

java管理容器

自从Spring3.0以后就可以使用java配置Spring框架。使用java配置步骤如下:
1.注册bean到Spring容器
创建一个配置类,命名应注意规范,方便理解。

@Configuration
@ComponentScan("FrameWork")
public class SpringConfig {

    @Bean
    public Music music()
    {
        return new Music();
    }

    @Bean
    public User user()
    {
        System.out.println("实例化User对象");
        return new User();
    }

    @Bean
    public UserService userService()
    {
        return new UserServiceImpl();
    }

    @Bean
    public MusicService musicService()
    {
        return new MusicServiceImpl();
    }
}

@Configuration注解说明SpringConfig 类是一个配置类,这是一个bean定义的资源文件相当于beans标签,容器可以从这个类中获取对象。被注解的类也会被注册为一个bean。
@Bean注解 说明方法可以向Spring容器提供一个对象相当于bean标签,它向容器中定义了一个bean,bean的名称就是方法名。容器调用该方法获取对象,你可以方法内添加一些初始化操作。
@ComponentScan注解说明了Spring需要扫描哪些路径相当于context:component-scan标签。

2.从容器中获取bean
java配置的容器,获取方式与xml配置的类似,只是更换了ApplicationContext的实现类。

public static void main( String[] args )
    {
        ApplicationContext applicationContext=new AnnotationConfigApplicationContext(SpringConfig.class);
        User user=(User) applicationContext.getAutowireCapableBeanFactory().getBean(User.class);
        System.out.println(user);
    }

AnnotationConfigApplicationContext类用于加载java的配置类。你可以在构造方法中传入配置类,使用空的构造方法,然后调用scan方法扫描包,接着调用refresh方法将被扫描到的类注册到容器中。

对于web应用,可以在web.xml中做如下配置,使web应用启动时自动加载配置。

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>Archetype Created Web Application</display-name>

    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </context-param>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>FrameWork.SpringConfig</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

</web-app>

以上就是Spring容器的基本配置,更多详细配置请参考
在实际的应用开发中,你会发现在servlet中使用@Autowire之类注解自动注入实例时,会出现空指针异常。下面是发生异常的原因:

此处内容引用自 MasterT-J的文章
(1) tomcat启动后先加载web.xml文件。web.xml主要配置了servlet 、filter、listenner三种javaEE规范的类,加载顺序跟在web.xml文档中的位置无关。顺序为 listenner>filter>servlet 。

(2) 而spring的初始化类为org.springframework.web.context.ContextLoaderListener,就是一个listenner,它是先于servlet加载的。普通servlet和springmvc的入口servlet的加载顺序,就要看servlet的设置了。它们按照在Web.xml中定义的Servlet顺序加载。其中springmvc需要指定org.springframework.web.servlet.DispatcherServlet拦截所有的Web请求。

(3) 在 servletA类上加@WebServlet等注解时,spring或springmvc会扫面相关包,自动实例化一个servlet实例A;这个实例A的引用是spring IOC容器管理的。这个时候Spring ContextLoaderListener监听器首先初始化,扫描所有的java包,创建Bean对象。然后Tomcat容器在加载Servlet类,包括我们定义的Servlet以及Spring的DispatcherServlet。
Tomcat容器接下来会在web.xml配置加载Servlet类,这个时候加载DispatcherServle以及我们定义的Servlet类。这是tomcat容器会根据servler配置启动时或者第一次请求该url时实例化我们定义的Web servlet实例B.这个实例B的引用是tomcat容器管理的。

所以最终结果就是:拦截url的servlet和spring依赖注入的servlet不是同一个实例!!拦截url的servlet不受Spring容器的管理,所以就出现了不能依赖注入或者注解不起作用的现象。

原文:

出现这样的问题时你就需要手动从容器中获取实例了,我们可以用下面这种方法去解决问题。重写servlet的init方法,并在方法中执行以下代码

SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(this,this.getServletContext());

这段代码会将本类中所有被自动注入注解过的成员注入实例。
我们可以写一个servlet基类,重写init方法,将这段代码放入init方法中,之后的servlet只需要继承该基类即可。

4.AOP编程

AOP其实是一种编程思想,它的目的在于减少代码的重复性,降低耦合度。在Spring中对AOP的实现采用了两种方式,
第一种是jdk动态代理,通过实现和目标类B相同的接口生成类A,从而骗过java的类型检查。类A持有目标类B的对象引用,因此可以在调用类B方法的前后做一下事情,类A本身并不能做类B的事情,只是由于类A持有类B的引用所以达到了代理的效果,因此可以代理任何实现了接口的类。Spring默认使用这种方式。
第二种是CGLIB代理,通过继承目标类B生成类A,也可以骗过java的类型检查。和jdk代理不一样,CGLIB生成的类A是继承目标类B而来的,因此这个类A可以代理类B所有非final的事情(final方法不能被重写),通过重写方法,类A可以再调用父类方法的前后做一些别的事情,因此CGLIB可以代理所有没有final修饰的类。也因此在Spring中,如果一个业务对象并没有实现任何接口,默认就会使用CGLIB。

首先导入jar包,由于Spring采用了AspectJ的方式实现AOP,所以我们需要导入这个包

<dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>1.9.2</version>
            </dependency>
             <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aop</artifactId>
                <version>5.1.7.RELEASE</version>
            </dependency>

PS:不知道为什么我测试时,idea没帮我把这个包打到WEB-INF/lib目录,导致一运行就包缺少类的错误。只能手动打进去了。

下面我们先看看如何使用xml配置实现AOP吧

xml配置切面
package FrameWork.Advice;

import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.aspectj.lang.ProceedingJoinPoint;

public class myAdvice {

    public void doAfter()
    {
        System.out.println("后置通知");
    }

    public void doBefore()
    {
        System.out.println("前置通知");
    }

       public void doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知开始");
        proceedingJoinPoint.proceed();
        System.out.println("环绕通知结束");
    }

    public void doException(Exception e)
    {
        System.out.println(e.getMessage());
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--<context:component-scan base-package="FrameWork.ServiceImpl,FrameWork.bean,FrameWork.Servlets"></context:component-scan>-->

    <bean id="User" class="FrameWork.bean.User"></bean>

    <bean id="Music" class="FrameWork.bean.Music"></bean>

    <bean id="UserService" class="FrameWork.ServiceImpl.UserServiceImpl">
    </bean>

    <bean id="MusicService" class="FrameWork.ServiceImpl.MusicServiceImpl"  name="musicService">
    </bean>

    <bean id="myAdvice" class="FrameWork.Advice.myAdvice"></bean>
    
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy><!--开启自动代理-->
    
    <aop:config>
        <!--配置切面,需要切入的功能为myAdvice中的方法-->
        <aop:aspect ref="myAdvice">
        <!--配置切入点,expression表达式会确定要切入到哪些类的方法中-->
            <aop:pointcut id="mypointcut" expression="execution(* FrameWork.ServiceImpl.*.*())"></aop:pointcut>
             <!--配置前置通知,method为调用的方法,pointcut-ref引用切入点-->
            <aop:before method="doBefore" pointcut-ref="mypointcut"></aop:before>
            <aop:after method="doAfter" pointcut="execution(* FrameWork.ServiceImpl.MusicServiceImpl.listenMusic())"></aop:after>
            <aop:around method="doAround" pointcut-ref="mypointcut"></aop:around>
            <!--配置异常通知,method为调用的方法,pointcut配置切入点为MusicServiceImpl的所有方法,throwing配置异常参数-->
            <aop:after-throwing method="doException" throwing="e" pointcut="execution(* FrameWork.ServiceImpl.MusicServiceImpl.*())"></aop:after-throwing>
        </aop:aspect>
    </aop:config>
    <!--更多切面配置-->
</beans>

在上面的代码中,我们配置了myAdvice的切面,根据execution表达式将通知切入到其他方法中。需要注意的是你需要在xml文件中增加aop的命名空间及xsd约束,否则不会有代码提示。
另外要使用AOP功能你还需要 <aop:aspectj-autoproxy></aop:aspectj-autoproxy>这行代码,开启Spring的自动代理功能,其中proxy-target-class属性选择false为jdk代理,true为CGLIB代理,默认为false,如果属性为false,但是目标类没实现接口,Spring会自动使用CGLIB代理。

PS:如果你在测试时出现了循环依赖的错误,并且你确保配置中没有循环依赖,你可以检查一下切入点是不是配置成切入所有公共方法之类的形式,这样会使Spring将自动代理切入到你所依赖的jar包中,很可能会造成循环依赖。作者测试就出现了这样的情况。

java注解配置切面
package FrameWork.Advice;

import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.aspectj.lang.ProceedingJoinPoint;

/**
 * Created by forget on 2019/6/7.
 */
@Aspect
public class myAdvice {
    @After(value = "execution(* FrameWork.ServiceImpl.*.*())")
    public void doAfter()
    {
        System.out.println("后置通知");
    }
    @Before(value = "execution(* FrameWork.ServiceImpl.*.*())")
    public void doBefore()
    {
        System.out.println("前置通知");
    }
    @Around(value = "execution(* FrameWork.ServiceImpl.*.*())")
         public void doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知开始");
        proceedingJoinPoint.proceed();
        System.out.println("环绕通知结束");
    }
    @AfterThrowing(throwing = "e",pointcut = "execution(* FrameWork.ServiceImpl.*.*())")
    public void doException(Exception e)
    {
        System.out.println(e.getMessage());
    }
}
package FrameWork;

import FrameWork.Advice.myAdvice;
import FrameWork.Service.MusicService;
import FrameWork.Service.UserService;
import FrameWork.ServiceImpl.MusicServiceImpl;
import FrameWork.ServiceImpl.UserServiceImpl;
import FrameWork.bean.Music;
import FrameWork.bean.User;
import org.springframework.context.annotation.*;
import org.springframework.web.context.ConfigurableWebApplicationContext;


@Configuration
@EnableAspectJAutoProxy
public class SpringConfig {

    @Bean
    public Music music()
    {
        return new Music();
    }

    @Bean
    public User user()
    {
        return new User();
    }

    @Bean
    public UserService userService()
    {
        return new UserServiceImpl();
    }

    @Bean
    public MusicService musicService()
    {
        return new MusicServiceImpl();
    }

    @Bean
    public myAdvice myAdvice()
    {
        return new myAdvice();
    }
}

@EnableAspectJAutoProxy注解就相当于aop:aspectj-autoproxy标签,同样有个proxyTargetClass属性可以选择代理。

@Aspect注解代表这是一个切面配置类相当于aop:aspect标签

@After,@Before,@Around,@AfterThrowing注解代表通知的连接点,注解内的value属性通知的切入点。

从这些代码可以发现Spring对AOP的支持是非常强大,你只需几个注解就可以完成一个切面的配置。

PS:注意使用环绕通知时必须在方法中调用ProceedingJoinPoint对象的proceed方法,否则将不会调用被代理对象的目标方法。