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方法,否则将不会调用被代理对象的目标方法。