因为今年的7月份要入职,之前也是个半吊子,说我自己是程序员确实是抬举我了(我还是很菜,不够格这个词儿,哈哈~~),所以趁着这段空闲的时间好好给自己充充电,把学的不扎实的、当前热点技术拿出来搞一搞,基于这样的目的,决定重新过一遍Spring,主要以撸代码为主,撸到哪就写到哪儿(2018/02/14)。
1.Hello Spring
Spring的入门程序。这里首先写一个HelloWorld的bean实体类,如下:
public class HelloWorld {
private String name;
public void setName2(String name) {
this.name = name;
}
public void hello() {
System.out.println("hello:" + name);
}
}
很简单,定义了name属性和其setter方法以及一个hello方法,然后在一个方法中来创建HelloWorld的对象并调用其方法:
public class Main {
public static void main(String[] args) {
//1.创建对象并设值
HelloWorld helloWorld = new HelloWorld();
helloWorld.setName2("HHU");
//2.调用方法
helloWorld.hello();
}
}
在利用Spring的时候,上述代码中的第一步创建对象和设值就可以交给Spring来完成了,在这个例子中,直接在src下创建Spring的配置文件applicationContext.xml,在里面配置bean,这里就一个HelloWorld,
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--配置bean,利用反射的方式由Spring创建这个Bean-->
<bean id="helloWorld" class="com.hhu.spring.beans.HelloWorld">
<!--这里为属性赋值是调用bean中的setter(注意setter的方法名),所以在定义bean的时候要给定setter方法-->
<property name="name2" value="HHU"></property>
</bean>
</beans>
配置完之后,在Main类中第一步创建对象和设值可以直接交给Spring通过上述的配置文件去做,我们要做的就是创建IOC容器然后获取创建好的bean即可:
//1.创建Spring的IOC容器ApplicationContext,在集成WEB时容器为WebApp..
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
//2.从容器中获取Bean实例,并且进行对应类型的强转
HelloWorld helloWorld = (HelloWorld) applicationContext.getBean("helloWorld");
这里注意在配置文件中property
中name
是对setter方法的定位,这里的name
要写setter的方法名而不是参数名,上面写成setName2(String name)
就是为了区分name
,如果是setFirstName(String name)
,那么property
中name
就是firstName
,这里在上述创建IOC容器的时候,Spring就帮我们构造了HelloWorld对象,并且调用setName2方法进行了设值操作,这一点可以将第二步注释掉再在HelloWorld的构造方法和setter方法中写一些输出代码可以得到证明。
2.Bean配置
IOC,即控制反转(Inversion of Control),这里是指资源的控制权反转,传统的资源查找是组件向容器发起资源查找请求,而使用IOC则是容器主动的将资源推送给它管理的组件,组件唯一要做的就是以一种合适的方式来接受即可。在Spring中
- 配置Bean的形式:基于xml文件的方式和基于注解的方式。
- 配置Bean的方式:通过全类名(反射),工厂方法(静态工厂和实例工厂)、FactoryBean
- 依赖注入的方式:属性注入、构造器注入、工厂方法注入(不常用)
常用的就是基于xml的通过全类名进行配置,上面配置HelloWorld的Bean的时候如下:
<!--配置bean:
class就是bean的全类名,通过反射在IOC容器中创建Bean,这里要求Bean中必须要有无参构造器。
id是bean的标识符,唯一,获取Bean的时候就是通过这个id
-->
<bean id="helloWorld" class="com.hhu.spring.beans.HelloWorld">
<property name="name2" value="HHU"></property>
</bean>
依赖注入方式,先看一下属性注入(这种注入方式最常用),属性注入就是通过setter注入Bean的属性值或者依赖的对象(所以往往在Bean中定义的像姓名、年龄等属性都会添加setter方法为Spring使用),就是上面的
<property name="name2" value="HHU"></property>
其中name用于定位Bean中对应的setter方法,value就是要设置的值,注意这种属性注入的方式必须提供setter方法;
还有就是构造方法注入,利用<constructor-arg>
标签来完成,其他的一样,这里先创建一个Car的bean:
public class Car {
private String brand;
private String corp;
private int price;
private int maxSpeed;
public Car(String brand, String corp, int price) {
this.brand = brand;
this.corp = corp;
this.price = price;
}
@Override
public String toString() {
return "Car{" +
"brand='" + brand + '\'' +
", corp='" + corp + '\'' +
", price=" + price +
", maxSpeed=" + maxSpeed +
'}';
}
}
然后在配置文件中构造Car对象,由于这里是通过构造器注入,所以就无须再各个属性的setter方法,配置文件中:
<bean id="car" class="com.hhu.spring.beans.Car">
<constructor-arg value="BMW"></constructor-arg>
<constructor-arg value="China"></constructor-arg>
<constructor-arg value="5000"></constructor-arg>
</bean>
当然在constructor-arg
标签中配置构造方法的参数默认按Bean中构造方法中的参数顺序来设置的,也可以不按这个顺序写,但是除了value外还要加上index属性字段(从0开始),比如像下面这样颠倒写,用索引号区分:
<bean id="car" class="com.hhu.spring.beans.Car">
<constructor-arg value="China" index="1"></constructor-arg>
<constructor-arg value="5000" index="2"></constructor-arg>
<constructor-arg value="BMW" index="0"></constructor-arg>
</bean>
除此之外,可能Car的构造器不止这一个,向上面的构造方法是用了品牌、产地、价格,那么我也可以用品牌、产地和速度来搞一个构造器,来搞吧,中间出现一个小插曲,用IDEA创建constructor的时候,死活创建不出来,我的猪脑袋在停留了近5秒的时间才反应过来,是重载的问题,所以将car中的price属性类型改成了double,然后再创建如下的构造器:
public Car(String brand, String corp, int maxSpeed) {
this.brand = brand;
this.corp = corp;
this.maxSpeed = maxSpeed;
}
此举意义在于如果区分多个构造器进行构造注入,因为此时存在两个构造器,仅靠默认的方式或者增加index属性已经不能区分第三个构造参数的传入到底要用哪个构造器,此时应增加类型来区分:
<bean id="car" class="com.hhu.spring.beans.Car">
<constructor-arg value="BMW"></constructor-arg>
<constructor-arg value="Shanghai"></constructor-arg>
<constructor-arg value="5000.0" type="double"></constructor-arg>
</bean>
<bean id="car2" class="com.hhu.spring.beans.Car">
<constructor-arg value="Audi" ></constructor-arg>
<constructor-arg value="Beijing"></constructor-arg>
<constructor-arg value="4000" type="int"></constructor-arg>
</bean>
用type指明参数的类型即可,当然也可以标明每个参数的类型如
<constructor-arg value="Audi" type="java.lang.String"></constructor-arg>
以上的注入方式,除了使用value属性注入,也可以通过<value></value>
子标签进行注入,比如:
<constructor-arg>
<value>Audi<value>
</constructor-arg>
当然,所有的字面值(字符串,基本数据类型)都可以value
这个关键字进行注入,但是中间可能会出现一些特殊字符,我们需要用<![CDATA[]]>
将特殊字符包裹还必须用<value>
子标签形式才可以,不能在value=".."
中直接写,比如<
符号:
<constructor-arg>
<value><![CDATA[<Audi]]></value>
</constructor-arg>
这样表示的就是<Audi
。除了上述的最基本的字面值以外,还有可能要注入某个对象(比如其它的bean),这个时候只需要将上述的value
改成ref
或者使用<ref></ref>
子标签即可。
最后一点是集合属性的注入:
- List
比如一个Person对象中可能有很多car:
private List<Car> cars;
public void setCars(List cars) {
this.cars = cars;
}
此时的配置文件中配置如下:
<property name="cars">
<list>
<ref bean="car"/>
<ref bean="car2"/>
</list>
</property>
- map
比如:
private Map<Integer,String> map;
public void setMap(Map<Integer, String> map) {
this.map = map;
}
配置文件中进行如下的配置:
<property name="map">
<map>
<entry key="1" value="one"></entry>
<entry key="2" value="two"></entry>
</map>
</property>
- Properties
比如数据库的配置文件:
public class DataSource {
private Properties properties;
public Properties getProperties() {
return properties;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
@Override
public String toString() {
return "DataSource{" +
"properties=" + properties +
'}';
}
}
配置文件
<bean id="dataSource" class="com.hhu.spring.beans.DataSource">
<property name="properties">
<props>
<prop key="user">root</prop>
<prop key="password">root</prop>
<prop key="jdbcUrl">jdbc:mysql:///test</prop>
<prop key="driverClass">com.mysql.jdbc.Driver</prop>
</props>
</property>
</bean>
当然也可将集合的部分单独领出来像普通定义bean一样进行定义,以便复用,这里需要导入util命名空间,IDEA中配置好Spring关联后可以直接使用这个标签会自动导入:
<util:list id="cars">
<ref bean="car"/>
<ref bean="car2"/>
</util:list>
以后想使用的这个list的时候,直接调cars
即可,其他的集合类似。当然Spring中也提供了自动装配的功能在<bean>
标签中指定好class
属性后,添加一个autowire
属性,可以选择按名字自动装配或者按类型自动装配,比如下面的:
<bean id="person" class="com.hhu.spring.autowire.Person" autowire="byName"></bean>
上述是Bean的基本配置方式,在实在配置中可能存在多个bean之间大部分的属性相同,这个时候,有很多工作都是机械的重复,类似于java的继承关系,Bean的配置也提供了这种关系,在定义bean的时候,可以通过parent
属性指定父类Bean的id,从而继承父类Bean的所有属性,子类Bean可以根据自己的需要对部分属性进行重写,比如下面的配置:
<bean id="person" class="com.hhu.spring.autowire.Person">
<property name="car" ref="car"></property>
<property name="address" ref="address"></property>
<property name="name" value="Jack"></property>
</bean>
<bean id="person2" parent="person">
<property name="name" value="testParent"></property>
</bean>
person2的属性继承了person,所以person的属性他也有,只是对person2的姓名进行了重写,比较方便,输出信息如下:
Person{name='Jack', address=Address{city='Nanjing', street='Xianlin'}, car=Car{brand='Audi', corp='Shanghai', price=5000.0, maxSpeed=0}}
Person{name='testParent', address=Address{city='Nanjing', street='Xianlin'}, car=Car{brand='Audi', corp='Shanghai', price=5000.0, maxSpeed=0}}
父类Bean可以作为一个实例Bean存在,就想上面的person,当然父类Bean也可以声明为抽象Bean,此时他不能被IOC容器实例化,专门用作其他Bean的继承(可以理解为一系列Bean的配置模板),在定义的时候要加上abstract="true"
的声明,如下:
<bean id="person" class="com.hhu.spring.autowire.Person" abstract="true"></bean>
【注意】如果一个Bean的class
属性没有被指定,那么该Bean必定是一个抽象Bean。
除此之外,Bean之间除了继承关系,也可以存在依赖关系,比如有一个需求就是每个人必须要定义一个Address的Bean,这里必须要有就可以理解为Person依赖于Address,如果在定义Person的Bean时,指定了对Address的依赖,那么必须对Address的Bean进行配置,否则报错,这种依赖关系在定义Bean的时候通过depends-on
属性值来指定,如果依赖多个Bean可以用逗号或者空格来区分开:
<bean id="person" class="com.hhu.spring.autowire.Person" depends-on="address"></bean>
这种被依赖的Bean称之为前置Bean,前置Bean在被IOC容器初始化的时候会在依赖该Bean的Bean初始化之前被初始化。
3.Bean的作用域
在Spring的IOC容器中,对Bean进行实例化的时候,默认是单例的,IOC容器只会对一个Bean对象进行一次实例化,调用Bean的时候返回的实例是唯一的。这里可以通过在定义Bean的时候设置scope
来指定该Bean的作用域,常用的作用域有两种:singleton和prototype,singleton就是单例的,每次从IOC容器中获取的Bean都是第一次实例化的对象或者说在IOC整个生命周期至创建这一个类的实例(默认就是这个),prototype是原型的,每次从IOC获取都会是新创建的一个实例。如:
<bean id="address" class="com.hhu.spring.autowire.Address" scope="prototype">
<property name="city" value="Nanjing"></property>
<property name="street" value="Xianlin"></property>
</bean>
获取实例:
ApplicationContext ctx = new ClassPathXmlApplicationContext("bean-scope.xml");
Address address = (Address) ctx.getBean("address");
Address address2 = (Address) ctx.getBean("address");
System.out.println("address.equals(address2):" + address.equals(address2));
虽然address和address2内容一样,但是它们是不同的,输出false。
【注意】如果是Singleton那么在初始化IOC容器的时候,Bean实例将会直接被创建好,即执行ApplicationContext ctx = new ClassPathXmlApplicationContext("bean-scope.xml")
后就已经存在实例了,而如果是Prototype,那么则不会,而是在真正获取的时候才会创建,即在执行Address address = (Address) ctx.getBean("address")
后才会有Bean的实例。
4. 使用外部配置文件
在配置系统细节信息的时候,通常不会直接在Spring的配置文件直接配置,因为这些信息可能会在以后进行改动,如果直接在Spring的配置文件中改动,这是非常不建议的,通常的做法是在Spring的配置文件外定义一个或者多个属性配置文件,然后在Spring的配置文件中直接引用即可,需要改动的时候只需要对外部的这个属性配置文件进行修改即可,典型的应用就是数据库的连接配置。下面是直接在Spring配置文件配置数据源:
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="***"></property>
<property name="password" value="***"></property>
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/seckill"></property>
</bean>
然后测试:
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans-properties.xml");
DataSource dataSource = (DataSource) ctx.getBean("dataSource");
try {
System.out.println(dataSource.getConnection());
} catch (SQLException e) {
e.printStackTrace();
}
这中间发生一个关于jdbc驱动插曲,新版的驱动位置从com.mysql.jdbc.Driver
变成了com.mysql.cj.jdbc.Driver
,在写的时候注意,但是上面的配置方式并不友好,通常我们都是把关于数据库的连接信息单独放到一个属性文件中的,如db.properties(IDEA创建配置文件是resources类型,所以新建properties时,要新建一个ResourceBundle类型的文件):
user=***
password=***
driverClass=com.mysql.jdbc.Driver
jdbcUrl=jdbc:mysql://localhost:3306/seckill
那么剩下的就是在Spring的配置文件中引入这个外部配置文件属性即可,Spring提供了一个PropertyPlaceholderConfigurer的BeanFactory后置处理器,这个处理器允许用户将Bean的配置内容移到属性文件中(在这里就是将dataSource这个Bean的user、passwor、driverClass和jdbcUrl属性放到db.properties文件中),然后在配置Bean时可以用${var}
的形式引用外部属性文件的属性值。所以首先应该注册PropertyPlaceholderConfigurer:导入context命名空间,使用property-placeholder,用location指定外部属性文件的位置即可(IDEA可以直接使用,关联Spring架构后在使用时会自动导入该命名空间)。
<!--导入外部属性文件-->
<context:property-placeholder location="db.properties"/>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--引用属性-->
<property name="user" value="${user}"></property>
<property name="password" value="${password}"></property>
<property name="driverClass" value="${driverClass}"></property>
<property name="jdbcUrl" value="${jdbcUrl}"></property>
</bean>
5.SpEL:Spring的表达式语言
Spring的表达式语言(简称SpEL)是一个支持运行时查询和操作对象的强大表达式语言,语法类似与EL(现在想起来都可笑,当初一直用这个东西,但不知道它的名字,一直叫它EI表达式,百度半天EI表达式也没有结果,现在想想都笑死人,哈哈~~),语法:#{}
作为定界符,所有在大括号内的字符都被认为是SpEL,它可以为Bean的属性进行动态赋值,下面罗列了几种SpEL的常用场景:
- 引用其他的对象,不需要用
ref
关键字,还是使用value,比如下面的:
<property name="car" ref="car"></property>
<!--可以改写为下面的样子-->
<property name="car" value="#{car}"></property>
- 引用其他对象的属性(这个属性在Bean中定义必须是权限修饰符必须为public或者才能在Bean的实体类中定义了对应属性的getter方法,才被SpEL表达式访问),就像普通对象引用的属性,比如下面的引用:
<property name="name" value="#{address.city}"></property>
在设置name的时候,用SpEL表达式引用了之前定义的address的Bean的中city的属性值,注意在Address中对于name的定义必须为public String name
这样的可以被大家访问的形式。
* 可以调用静态方法或者静态属性,通过T()
调用一个类的静态方法,返回一个Class Object,然后再调用相应的方法或属性,比如下面调用π的方法:
<property name="initValue" value="#{T(java.lang.Math).PI}"></property>
- 当然SpEL表达式也可以使用普通的字面量,注意的是字符串需要用单引号包裹:
<property name="city" value="#{'Nanjng'}"></property>
- 支持常规的算数和逻辑运算以及判断,下面是各种情况的栗子:
<!--判断-->
<property name="info" value="#{car.price>300000?'金领':'白领'}"></property>
6. 基于注解的方式配置Bean
上述的种种都是基于XML配置的方式,同样的Bean的配置也可以基于注解的方式,总体看下来,不论是什么,基于注解的方式几乎是总是十分方便,所以有必要进行学习一下。
6.1 配置Bean到IOC容器
这里配置BeanF分为两个部分,一个是配置Bean到IOC容器中,另一个配置Bean的属性,先进行前者的工作:配置Bean到IOC容器中,就需要组件扫描(component scanning),Spring可以从特定的classpath下自动扫描,发现和实例化具有特定注解的组件,这些组件包括:
- @Component–基本标识,表明该组件受Spring管理;
- @Respository–标识持久层组件,即Dao层;
- @Service–标识服务层,即Service层;
- @Controller–标识控制层,即Controller层;
在上述的标识中,往往会带上组件的命名,但是通常Spring有自己默认的命名策略,即使用该类的类名(对该类名做如下的处理:首字母小写),大部分都是不用自己配,比如下面:
@Component
public class User{
}
那么在注入的时候直接是user
,因为默认的大部分我们所要的一致,所以无须添加名称,但是在Service层往往涉及到接口的继承,这里我们需要进行手动添加命名了(用value属性标识,如果没有其他属性,可以省去value直接写名字),如:
@Service("logService")
public class LogServiceImpl implements logService {
}
这里我们手动命名为logService
方便后面的阅读和处理,如果不手动操作Spring将会自动为其命名为logServiceImpl
;
当然加了上面的一些注解,还需要在Spring的配置文件声明组件扫描<context:component-scan>
,用base-package
属性指定一个扫描的基类包(如果要扫描多个基类包,之间要用逗号分隔),那么Spring将会自动扫描包括这个包和该包下的所有子包下文件,如果文件上带有Spring组件标识符的将会自动被Spring所管理,比如下面的声明:
<context:component-scan base-package="com.hhu.spring.annotation"/>
【注】IDEA中命名空间在我们使用的时候自动帮我们导入,那么这个时候com.hhu.spring.annotation
包以及子包下所有带有上述组件标识符的Bean都会被Spring管理,都可以从Spring的IOC容器中取到。
6.1.1包的过滤
当然除了在<context:component-scan>
标签中用base-package
属性指定基类包,也可以使用resource-pattern
属性指定只扫描哪些类进行部分过滤,比如:
<context:component-scan base-package="com.hhu.spring.annotation" resource-pattern="repository/*.class"/>
那么此时只对com.hhu.spring.annotation.repository
目录下后缀为.class
的文件进行扫描。
除了上述的resource-pattern
属性进行指定类的扫描外,还可利用<context:exclude-filter ... />
和<context:include-filter ... />
子标签进行指定扫描要排除的类和要包含的目标类,在<context:component-scan>
标签下可以若干个上数的exclude-filter
和include-filter
节点,在这两个子标签下支持多种类型的过滤表达式(即type的类型):常用的有annotation(基于注解的类型)和assignable(基于类名的方式)两种类型。
类型 | 示例 | 说明 |
---|---|---|
annotation | org.springframework.stereotype.Repository | 在基类包下所有标注了@Repository 注解的类将会被扫描或者过滤 |
assignable | com.hhu.XxxService | 所有继承或扩展XxxService的类都会被扫描或过滤 |
1. 基于注解的类型过滤
比如对com.hhu.spring.annotation包下的类进行扫描,并且标注有@Repository
注解的类将会被过滤,不作扫描处理:
<context:component-scan base-package="com.hhu.spring.annotation">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
</context:component-scan>
如果只想对基类包下带有@Repository
注解类进行扫描,那么可以使用<context:include-filter>
子标签,还必须默认的过滤器关闭(这个时候Spring的包扫描将只使用我们定义的这个标签进行扫描):
<context:component-scan base-package="com.hhu.spring.annotation" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
</context:component-scan>
【注意】这里一定要关闭默认的过滤器use-default-filters="false"
,否则上述的配置含义是要对带有@Repository
注解的类进行扫描,再加上默认过滤器的扫描,这个注解的类被扫描两次!!而不是只对带有@Repository
注解的类进行扫描,注意区分!我们这里是希望只对(是只对!!其他不扫描)指定注解的类进行扫描。这里好好说明一下use-default-filters
这个属性吧,默认或缺省都为true
,根据Spring的官方文档,此时它会自动去扫描基类包下带有@Component
,@Repository
,@Service
,@Controller
和@Configuration
注解的类,一旦想如上面用include-filter
标签实现只对指定注解进行拦截扫描,必须关闭默认拦截器use-default-filters="false"
,但是如果仅仅是想排除基类包下某些注解的类,则不需要关闭默认拦截器,直接使用exclude-filter
即可,之前做项目的时候吃过亏。
- 基于类名的类型过滤
<context:component-scan base-package="com.hhu.spring.annotation">
<context:exclude-filter type="assignable" expression="com.hhu.spring.annotation.repository.UserRepository"/>
</context:component-scan>
那么在扫描基类包的时候,会将所有UserRepository类和继承或实现该类或接口的类排除在外,不进行扫描。同样的和基于注解的过滤方式一样,也可以实行只对UserRepository及其子类进行扫描(需要关闭默认的过滤器),配置如下:
<context:component-scan base-package="com.hhu.spring.annotation" use-default-filters="false">
<context:include-filter type="assignable" expression="com.hhu.spring.annotation.repository.UserRepository"/>
</context:component-scan>
6.2 装配Bean
通过上面的步骤已经实现了将Bean加载到IOC容器中了,那在需要的时候将实例化的Bean取出来进行装配,只需要在调用的类(这个类也是被Spring管理的)中定义一下这个Bean,然后将在定义的时候用@Autowired
注解一下即可,比如:
@Autowired
private UserService userService;
那么接下来可以直接使用userService
这个实例化对象了,另外基于注解配置的Bean在使用@Autowired
注解时,那么这个Bean必须是可以被Spring扫描到的,否则在IOC启动时会报错,当然可以在装配的时候指定这个Bean是否必须(在后面用required
关键字指定),如果非必须也不会报错,但会自动装配成null
,比如下面的:
@Autowired(required = false)
private UserService userService;
那么此时就算Spring没有扫描加载到userService
这个Bean,那么IOC也可以正常启动而不会报错。
7.AOP
这个东西是Spring的核心之一,早就闻其大名,对于之前的Spring-boot中也有涉及并学习到这个东西,由于平台受限,在学校完全是闭门造车,对于Spring这个特性一直停留在理论认知,即使理论认知也不能达到理解程度,着实可惜,现在回头重新来看AOP真的是感慨,这一部分可以结合基于Springboot的Spring AOP学习记录一起来看以加深理解。
下面是以计算器日志为业务需求,在每次计算前后加上相对的日志,处理过程如下:
1. 编写计算机抽象接口
public interface Counter {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int dev(int i, int j);
}
- 接口实现类并添加日志文字
public class CounterImpl implements Counter {
@Override
public int add(int i, int j) {
System.out.println("The mothod add begin with [" + i + ", " + j + "]");
int result = i + j;
System.out.println("The method add end with " + result);
System.out.println();
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("The mothod sub begin with [" + i + ", " + j + "]");
int result = i - j;
System.out.println("The method sub end with " + result);
System.out.println();
return result;
}
@Override
public int mul(int i, int j) {
System.out.println("The mothod mul begin with [" + i + ", " + j + "]");
int result = i * j;
System.out.println("The method mul end with " + result);
System.out.println();
return result;
}
@Override
public int dev(int i, int j) {
System.out.println("The method dev begin with [" + i + ", " + j + "]");
int result = i / j;
System.out.println("The method dev end with " + result);
System.out.println();
return result;
}
}
尽管上面的代码完成我们开始时提出的功能需求,但是不难发现,所谓的日志代码带来的问题是:量大、重复性工作多,并且从计算器本身来讲,它的核心功能是计算(即add、sub、mul和dev4个方法),我们的重点关注应该在上述的4个计算方法上,而现在就造成代码混乱,越来越多的非业务需求加入使得在关注核心逻辑的同时还必须兼顾其他关注点,而且日志代码较为分散不便于维护,AOP的出现就是为了解决上述的问题。在用AOP解决前,首先用动态代理(用一个代理将对象包裹起来,调用的时候不直接调用原始对象而是调用代理对象)来搞定一下,上述的带有日志的计算器接口实现回退指上个没有日志的版本(即只有计算的代码没有日志代码),主要代码如下,注意jdk动态代理基于接口(本栗子中就是基于计算器接口Counter):
//1.动态代理对象
public class CounterProxy implements InvocationHandler{
private Counter target;
public CounterProxy(Counter target) {
this.target = target;
}
/**
* 主要理解三个参数的含义
* @param proxy:正在返回的代理对象,一般情况下在invoke方法中不使用该对象,他是一个无限循环
* @param method:正在被调用的方法
* @param args:调用方法时传入的参数数组
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//前置日志
System.out.println("The method " + method.getName() + " begin with [" + args[0] + ", " + args[1] + "]");
Object result = null;
result = method.invoke(target,args);
System.out.println("The method " + method.getName() + " end with " + result);
return result;
}
}
然后调用的时候,调用代理对象即可:
public class Main {
public static void main(String[] args) {
Counter counter = (Counter) Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{Counter.class}, new CounterProxy(new CounterImpl()));
counter.add(9, 3);
counter.sub(9, 3);
counter.mul(9, 3);
counter.dev(9, 3);
}
}
动态代理是可以做这样的事儿,但是相对于AOP来说,AOP要比它更合适,面向切面编程存在就是为了实现这些非核心逻辑的功能(日志、验证等)。
AOP的中有两个概念:连接点–程序执行的某个特定位置(比如方法执行前、方法执行后),这里连接的含义我理解下来应该就是非核心逻辑和核心逻辑之间的连接(比如日志和方法之间);切点–它和连接点不是一一对应的,连接点按照上面的理解确确实实是存在的,而切点则不一样,它是定位连接点的条件,结合AOP的概念来看,切点就是核心逻辑和非核心逻辑的区别标准,按这个标准一刀下去,必定会有若干对的切块(每对切块又分为核心逻辑和非核心逻辑两部分),这种区分标准在代码中是没有表现的。
7.2 AOP的使用
Java社区中最完整最流行的AOP框架是AspectJ(仅限于Spring2.0+),Spring自身也提了AOP的实现,但是推荐AspectJ(需要导入相应的jar,IDEA中的maven在检验到对自动导入),下面就做这样的事情。
现在已经有了正常的Counter接口和不带日志的Counter接口实现类CounterImpl,下面利用AOP的实现方式:
1. 首先声明一个切面LoggingAspect
,切面的作用就是实现非核心逻辑(横切关注点)的代码。
@Component
@Aspect
public class LoggingAspect {
//声明一个前置通知,在目标方法之前执行
@Before("execution(public int com.hhu.spring.aop.impl.Counter.add(int, int))")
public void beforeMethod() {
System.out.println("The mothod begins");
}
@After("execution(public int com.hhu.spring.aop.impl.Counter.add(int, int))")
public void afterMethod() {
System.out.println("The method ends");
}
}
【注意】切面的声明要注意3个步骤:第一步,切面必须存在于IOC容器中,这里可以用@Component
注解进行注册;第二步,切面本身的声明用@Aspect
注册;第三步,Spring的配置文件中必须声明AOP的自动代理生效,自动为匹配AspectJ注解的Java类生成代理对象,所以在配置文件中需要加上:
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
前两点和SpringBoot中的配置是一样的,这样就完成了一个标准切面的声明并且注册到了IOC容器中,当然除此之外,切面内部的Advice设置也有单独的一套理论,AspectJ中主要5种通知:
- @Before,前置通知,在方法前执行前执行;
- @After,后置通知,在方法执行(不论是否发生异常)后执行;
- @AfterRunning,返回通知,在方法正常返回结果之后执行
- @AfterThrowing,异常通知,在方法抛出异常之后执行
- @Around,环绕通知,围绕方法执行
Adivice的使用,就是在切面逻辑方法前使用对应上述的几种注解进行声明,用AspectJ的表达式定位到具体的方法上(这里的方法就是指上述的方法执行前或执行后中的这个方法,简而言之是指核心关注点里面的功能实现方法),当然这里的定位需要我们去用表达式说明,比如上面切面的定位表达式就是:
execution(public int com.hhu.spring.aop.impl.Counter.add(int, int))
我们称之为PointCutExpression(切面表达式),用来描述去匹配的哪些类的哪些方法,基本格式为:execution(1.修饰符 + 2.返回值类型 + 3.方法名)
;这里面的需要主要的是第3个元素方法名的写法,通需要具体到包名+类名+形参类型
,比如上面的
com.hhu.spring.aop.impl.Counter.add(int, int)
对于Counter中的add方法的定位就是如此,包名为com.hhu.spring.aop.impl
,然后定位到Counter
类,由于是对其中的int add(int i, int j)
这个接口方法定位,所以还需要将add方法和其中的两个参数的类型int
写出来,才能定位到add方法,当然PointCutExpression也是支持通配符的,通常情况下,上面的表达式可以使用通配符写成如下的形式:
execution(* com.hhu.spring.aop.impl.*.*(int, int))
这么这样的就可表示去匹配任意修饰符和返回类型,在com.hhu.spring.aop.impl包下面的任意方法(只要形参类型为2个且都为int)。如果希望在切面中可以对横切关注的细节进行访问(比如方法名和参数信息),可以在通知方法中加入JoinPoint
对象,比如:
@Before("execution(* com.hhu.spring.aop.impl.*.*(int, int))")
public void beforeMethod(JoinPoint joinPoint) {
//通过连接点JoinPoint可以获取方法名和参数
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("The mothod " + methodName + " begins");
}
上面主要是针对前置通知,针对后置通知我们也可以通过JoinPoint对象获取方法名,但是无法获取返回值,必须使用返回通知才可以,比如下面的:
//用PointCutExpression指明方法后,需要用returning绑定返回对象
@AfterReturning(value = "execution(* com.hhu.spring.aop.impl.*.*(int, int))", returning = "result")
public void afterRurning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " end with " + result);
}
异常通知和上面的返回通知有些类似,但是在返回通知中如果出现异常,程序将终止,异常后面的进程将不会继续执行,而这个时候异常返回通知就会执行:
//返回异常需要用throwing进行绑定,这里在参数定义的是Exception ex
@AfterThrowing(value = "execution(* com.hhu.spring.aop.impl.*.*(int, int))",throwing = "ex")
public void afterThrow(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " occurs Exception:" + ex);
}
【注意】上面异常返回通知形参定义的是Exception ex
,所以在横切方法发生任何异常都会返回,当然也可以定义为其他具体的异常如NullPointerException
,那么只有方法在放生空指针异常的时候才会返回通知,其他异常不返回。
最后一种环绕通知是功能最强的,但不是最常用的,环绕通知的注解和前面的类似,这里注意环绕通知必须要携带ProceedingJoinPoint
对象,而且必须要有返回值,下面是一个栗子:
/**
* 环绕通知必须携带ProceedingJoinPoint对象,类似于动态代理的全过程,
* 这个对象甚至可以决定是否执行目标方法,且环绕通知必须要有返回值,这个
* 返回值也是目标方法的返回值
*
* @param pjp
*/
@Around("execution(* com.hhu.spring.aop.impl.*.*(int, int))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//利用ProceedingJoinPoint可以获取方法名和参数
String methodName = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
//这里类似于前置通知
System.out.println("The method " + methodName + " begin with [" + args[0] + ", " + args[1] + "]");
//定义返回值,这个是必须要有的
Object result = null;
//执行目标方法,并获取返回值
result = pjp.proceed();
//这个类似与后置通知
System.out.println("The method " + methodName + " end with " + result);
return result;
}
从上面可以发现环绕通知很强大,它可以决定是否让目标方法执行,甚至可以修改目标方法的返回值,在一定环境下甚至可以取代前面几种通知。
对于一个同一个横切关注点,可能同时具有多个切面,这个时候可以在定义切面的时候用@Order(数字)
的方式指定优先级别,数字越小优先级别越高,比如下面的两个切面:
@Order(1)
@Component
@Aspect
public class LoggingAspect {
@Around("execution(* com.hhu.spring.aop.impl.*.*(int, int))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//获取方法名
String methodName = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
System.out.println("The method " + methodName + " begin with [" + args[0] + ", " + args[1] + "]");
Object result = null;
//执行目标方法,并获取返回值
result = pjp.proceed();
System.out.println("The method " + methodName + " end with " + result);
return result;
}
}
另一个切面:
/**
* 第二个切面
* Created by WeiguoLiu on 2018/2/28.
*/
@Order(2)
@Aspect
@Component
public class Aspect2 {
@Around("execution(* com.hhu.spring.aop.impl.Counter.*(int, int))")
public Object round(ProceedingJoinPoint pjp) throws Throwable {
String menthodName = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
Object result = null;
System.out.println("Aspect2:The method " + menthodName + " begin with[" +
args[0] + "," + args[1] + "]");
result = pjp.proceed();
System.out.println("Aspect2: The method end with " + result);
return result;
}
}
那么在执行Counter里面的方法时,必定LoggingAspect的切面里的通知会在Aspect2里的通知先触发。
PointCutExpression的表达式也是可以实现重用的,因为在一个切面或者其他切面,前置通知、后置通知、异常通知的切入点表达式很多都是一样的,比如下面的:
@Order(0)
@Component
@Aspect
public class LoggingAspect {
//声明一个前置通知,在目标方法之前执行,
@Before("execution(* com.hhu.spring.aop.impl.*.*(int, int))")
public void beforeMethod(JoinPoint joinPoint) {
//通过连接点JoinPoint可以获取方法名和参数
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("The mothod " + methodName + " begins with[" + args.get(0) + ", " + args.get(1) + "]");
}
@After("execution(* com.hhu.spring.aop.impl.*.*(int, int))")
public void afterMethod(JoinPoint joinPoint) {
System.out.println("The method " + joinPoint.getSignature().getName() + " ends");
}
@AfterReturning(value = "execution(* com.hhu.spring.aop.impl.*.*(int, int))", returning = "result")
public void afterRurning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " end with " + result);
}
@AfterThrowing(value = "execution(* com.hhu.spring.aop.impl.*.*(int, int))", throwing = "ex")
public void afterThrow(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " occurs Exception:" + ex);
}
}
可以发现关于切入点的表达式几乎都一样,后面的都是不断的重写,那么这里可以在前面定义一个逻辑无关的方法(通常这个方法为空),定义一个切面点表达式用@Pointcut
注解,里面写一个切面表达式,后面其他通知如果是同样的表达式可以直接写这个空方法的方法名即可:
@Order(0)
@Component
@Aspect
public class LoggingAspect {
//定义切点表达式模板
@Pointcut("execution(* com.hhu.spring.aop.impl.*.*(int, int))")
public void pointCut() {
}
//直接写模板的方法名即可
@Before("pointCut()")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("The mothod " + methodName + " begins with[" + args.get(0) + ", " + args.get(1) + "]");
}
@After("pointCut()")
public void afterMethod(JoinPoint joinPoint) {
System.out.println("The method " + joinPoint.getSignature().getName() + " ends");
}
@AfterReturning(value = "pointCut()", returning = "result")
public void afterRurning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " end with " + result);
}
@AfterThrowing(value = "pointCut()", throwing = "ex")
public void afterThrow(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " occurs Exception:" + ex);
}
}
而且这个模板在其他的切面中也可以用,只要定位到模板方法即可,如:
@Aspect
@Component
public class Aspect2 {
//直接使用上面切面中的模板
@Around("com.hhu.spring.aop.impl.LoggingAspect.pointCut()")
public Object round(ProceedingJoinPoint pjp) throws Throwable {
String menthodName = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
Object result = null;
System.out.println("Aspect2:The method " + menthodName + " begin with[" +
args[0] + "," + args[1] + "]");
result = pjp.proceed();
System.out.println("Aspect2: The method end with " + result);
return result;
}
}
8.Spring对JDBC的支持
这里有几种方式,首先Spring对JDBC的有自己的一套模板:SpringTemplate,这里对SpringTemplate有一点说明,它不支持级联属性(如:不能通过用户属性里面Car的id去查询Car对象的详细信息),所以说它是jdbc的一个小工具,而不是ORM框架。使用步骤如下:
1.创建数据库的属性文件
user = root
password = ***
jdbcUrl = jdbc:mysql://127.0.0.1:3306/db_studentinfo
driverClass = com.mysql.jdbc.Driver
initialPoolSize = 5
maxPoolSize = 10
2.创建Spring的配置文件
<?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 http://www.springframework.org/schema/context/spring-context.xsd">
<!--导入数据库的而配置文件-->
<context:property-placeholder location="db.properties"/>
<!--配置数据库的数据源-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${user}"/>
<property name="password" value="${password}"/>
<property name="jdbcUrl" value="${jdbcUrl}"/>
<property name="driverClass" value="${driverClass}"/>
<property name="initialPoolSize" value="${initialPoolSize}"/>
<property name="maxPoolSize" value="${maxPoolSize}"/>
</bean>
<context:component-scan base-package="com.hhu"/>
<!--配置Spring的jdbcTemplate,里面最基本的属性就是dataSource-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
3.使用JbdcTemplate
package com.hhu.springjdbc;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* Created by WeiguoLiu on 2018/2/28.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class JDBCTest {
@Autowired
private DataSource dataSource;
@Autowired
private JdbcTemplate jdbcTemplate;
//测试连接
@Test
public void testConnection() {
try {
System.out.println(dataSource.getConnection());
} catch (SQLException e) {
e.printStackTrace();
}
}
//测试增删改查
@Test
public void testInsert() {
String sql = "insert into t_user(userName, password)values(?,?)";
jdbcTemplate.update(sql, "insert", "insert");
}
@Test
public void testDelete() {
String sql = "delete from t_user where id = ?";
jdbcTemplate.update(sql, 2);
}
@Test
public void testUpdate() {
String sql = "update t_user set userName = ?,password = ? where id = ?";
jdbcTemplate.update(sql, "Spring", "Spring", 1);
}
/**
* 批量更新:包括增、删、改
*/
@Test
public void testBatchUpdate() {
String sql = "insert into t_user(userName, password) values(?, ?)";
List<Object[]> batchArgs = new ArrayList<>();
batchArgs.add(new Object[]{"insert1", "insert1"});
batchArgs.add(new Object[]{"insert2", "insert2"});
batchArgs.add(new Object[]{"insert3", "insert3"});
jdbcTemplate.batchUpdate(sql, batchArgs);
}
/**
* 从数据库中获取一条记录,而实际需要得到一个对象,
* 但是SpringTemplate不支持级联属性,即不能通过用户的car的id查询到car的具体属性
* 所以它是JDBC的小工具而不是ORM框架
*/
@Test
public void testQueryForObject() {
String sql = "select * from t_user where id = ?";
//指定映射的而类型
RowMapper<User> rowMapper = new BeanPropertyRowMapper<>(User.class);
//三个参数:sql,映射类型,待传入的参数id
User user = jdbcTemplate.queryForObject(sql, rowMapper, 3);
System.out.println(user);
}
/**
* 查询一组记录并映射到现有的对象上
*/
@Test
public void testQueryForList() {
String sql = "select * from t_user where id > ?";
RowMapper<User> rowMapper = new BeanPropertyRowMapper<>(User.class);
List<User> list = jdbcTemplate.query(sql, rowMapper, 2);
for (User u : list) {
System.out.println(u);
}
}
}
9.Spring的测试
这一在章应该放在前面ng项目中说的,疏忽了,也是自己有这个需求做了,进行了总结,测试的时候,不论如何,都需要让IOC运行起来才能获取其中的bean实例,这里提供两种测试的方法:
1.创建可运行类进行测试
package com.hhu.springjdbc;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import javax.sql.DataSource;
/**
* Created by WeiguoLiu on 2018/2/28.
*/
public class SpringTest {
public static void main(String[] args) {
//创建IOC容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
//从IOC容器中获取bean实例
DataSource dataSource = (DataSource) ctx.getBean("dataSource");
System.out.println(dataSource);
}
}
2.利用Junist4进行测试导入配置文件(推荐)
package com.hhu.springjdbc;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.sql.DataSource;
/**
* Created by WeiguoLiu on 2018/2/28.
*/
//让Spring运行在虚拟机中
@RunWith(SpringJUnit4ClassRunner.class)
//加载配置文件
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringTest {
@Autowired
private DataSource dataSource;
@Test
public void testSpring() {
System.out.println(dataSource);
}
}
10.Spring的事务管理
事务是一系列的动作被捆绑为一个执行单元,这一系列的动作要么都完成,要么都不完成。在Spring中依靠事务管理器来实现事务的管理,不同的方式有不同的事务管理器(其他的类似):
- 通过jdbc管理的事务使用
DataSourceTransactionManager
; - 通过JTA管理的事务使用
JtaTransactionManager
; - 通过Hibernate管理的事务使用
HibernateTransactionManager
;
10.1 Spring中事务的管理
在Spring中基于注解的事务管理配置方式分为两步:
1.Spring配置文件中配置事务管理器,打开事务注解驱动:
<!--配置事务管理器,事务管理器是针对数据库的操作的,所以需要配置相应的数据源-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--打开事务注解驱动-->
<tx:annotation-driven transaction-manager="transactionManager"/>
2.在事务行为上加上@Transactional
注解捆绑事务(通常在服务层进行捆绑)
@Transactional
public void purchase(String userName, String isbn) {
int price = bookShopDao.findBookPriceByIsbn(isbn);
bookShopDao.updateBookStock(isbn);
bookShopDao.updateUserAccount(userName, price);
}
10.2 Spring中事务的传播行为和隔离级别
事务的传播行为,就是指已有一个事务,然后新的事务调用已有的事务时,就需要指定这个已有事务在新的事务中是如何进行工作的,几种事务的传播行为见事务传播行为中的一.4,在具体使用的时候在注解@Transactional
后面追加事务的传播行为propagation
属性即可。
事务的隔离级别主要针对事务并发场景下提出的,因为在并发场景下容易出现脏读、幻读和不可重复读,事务的隔离级别的存在在一定程度上可以避免这些数据的读取问题,和传播行为类似,我们需要在@Transactional
注解后面通过isolation
属性来指定事务的隔离级别,具体在上面连接的事务传播行为中的一.5中有详细介绍,其中最常用的取值为Isolation.READ_COMMITTED
,同样的也可以使用rollbackFor
属性和noRollbackFor
指定对哪些异常进行回滚和不回滚,这里通常写的异常类型(如NullPointException.class),在@Transactional
注解中默认对所有的运行时异常进行回滚;还可以利用readOnly
来设置事务的只读属性,当其设置为true
时,事务不会加锁,帮助数据库对事务操作进行一度程度的人工优化,加快事务的执行速度,所以在我们确认一个事务确实仅仅是对数据库进行读的操作的话,建议加上readOnly = true
的设置;最后还可以利用timeout
指定在强制回滚前事务前(默认对事务不进行强制回滚)事务的执行可以持续多长时间,单位为秒,可以防止某个事务占用队列过长的时间,影响整个程序的执行。结合以上所有的配置,完整的事务注解像下面的:
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED
, rollbackFor = {NullPointerException.class}, noRollbackFor = {AccountException.class},
readOnly = true, timeout = 5)
public void purchase(String userName, String isbn) {
int price = bookShopDao.findBookPriceByIsbn(isbn);
bookShopDao.updateBookStock(isbn);
bookShopDao.updateUserAccount(userName, price);
}
11. Spring整合Hibernate
这里首先对两者的整合做一个目标,就是Spring到底整合什么,这里是整合Hibernate,那具体是整合Hibernate什么呢?两个东西:
1. 由Spring的IOC容器来管理Hibernate的SessionFactory;
2. 利用Spring的声明式事务为Hibernate所用;
两者的整合首要加入Spring和Hibernate各自需要的jar,这里IDEA直接加入这两个框架的依赖即可(Ctrl+shift+Alt+S,然后选择Modules里面对应的module添加对应的依赖即可),除了上面两个框架必须的依赖以外,还需要两个依赖,一个是jdbc,一个是c3p0,很容易理解这些是连接数据库做必须的jar包。
还是分为两个部分,一个Hibernate,一个是Spring。
在Hibernate的部分,需要做的有两个部分的工作,一个是核心配置
hibernate.cfg.xml
,但是只让它配置一些基础的属性:包括了方言、SQL显示以及是否格式化,生成数据表的策略和二级缓存等基本配置,但是其中关于和数据源的配置将会移到Spring的IOC容器中,在实际开发中,此配置文件可以视情况是否创建;另外,关于Hibernate的实体类和其映射文件还是需要正常配置,那Hibernate这一部分的工作就已经完成了。在Spring的部分,需要的东西:Spring的配置文件,这个配置文件需要做三件事情:1.配置数据源(数据源也是引用外部文件,所以需要导入外部的配置文件);2.配置Hibernate的SessionFactory;3.配置Spring的声明式事务。
下面是一个简单的小栗子:
<!--1. Hibernate的配置文件-->
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!--配置Hibernate的基本配置,
数据源直接放在Spring的IOC容器中配置,关联的Hibernate的
映射文件*.hbm.xml也放到IOC容器在配置SessionFactory的实例时进行配置,
这里的Hibernate的基本属性的配置有:
方言、SQL显示以及是否格式化,生成数据表的策略和二级缓存等基本配置,
虽然这样说,但是在实际开发中,这个文件实际上都可以拿掉,但是建议保留
-->
<!-- DB schema will be updated if needed -->
<!--显示SQL并且格式化-->
<property name="show_sql">true</property>
<property name="format_sql">true</property>
<!--配置数据的引擎,InnoDB,支持事务的数据库引擎-->
<property name="dialect">org.hibernate.dialect.MySQL5Dialect</property>
<!--数据库的生成策略-->
<property name="hbm2ddl.auto">update</property>
<!--二级缓存-->
</session-factory>
</hibernate-configuration>
然后是Spring的配置文件:
<?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:tx="http://www.springframework.org/schema/tx"
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 http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置自动扫描包-->
<context:component-scan base-package="com.hhu"></context:component-scan>
<!--配置数据源-->
<context:property-placeholder location="db.properties"/>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.userName}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
<property name="initialPoolSize" value="${jdbc.initialPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
</bean>
<!--配置Hibernate的SessionFactory,这里一定要注意绑定的到底是Hibernate的版本,要和下面的事务管理器的版本一致
否则报错,这里的版本是5
-->
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<!--配置数据源-->
<property name="dataSource" ref="dataSource"/>
<!--配置Hibernate的配置文件-->
<property name="configLocation" value="hibernate.cfg.xml"/>
<!--配置Hibernate的映射文件-->
<property name="mappingLocations">
<value>classpath:com/hhu/entity/*.hbm.xml</value>
</property>
</bean>
<!--配置Spring的声明式事务,这里是整合Hibernate,所以使用Hibernate的事务管理器-->
<bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"></property>
</bean>
<!-- <!–1.开启事务注解驱动,这里使用ASpect注解驱动–>
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
<!–配置自动Aspect自动生成代理对象–>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>-->
<!--2.基于注解实现的事务-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="purchase" propagation="REQUIRES_NEW"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!--配置事务切点-->
<aop:config>
<aop:pointcut id="txPointcut" expression="execution(* com.hhu.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
</beans>
当然其实在上述的Spring的配文件中配置Hibernate的核心cfg.xml配置文件,当然也可以直接不要了,关于Hibernate的核心配置文件直接放到Spring的配置文件中去配置,所以上面的Hibernate.cfg.xml文件和Spring里面的,上述相关事务的配置存在一个疑问,利用的Spring的注解的事务配置不能进行事务的回滚。
<property name="configLocation" value="hibernate.cfg.xml"/>
可以用下面的配置来取代(在sessionFactory
的bean标签下):
<property name="hibernateProperties">
<props>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.format_sql">true</prop>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>
【注意】有关在Spring配置文件中配置Hibernate的相关属性的时候已经在前面提过这个注意点,就是在这里一定要加上hibernate的前缀,否则不能生效(IDEA中可以直接写属性名,所以特别容易漏掉这个前缀)
12. Spring整合WEB应用
在Spring中整合WEB项目时,理所当然创建的Module应当是WEB类型,当然引入的jar包需要额外的spring-web和spring-webmvc(这两个jar包在整合WEB的时候这两个jar必须高搞进来),不过在IDEA不用担心,只要勾选Spring的架构就可以了,用到的时候自动导入。主要的问题在WEB应用中如何创建IOC容器,在非WEB应用中,可以在main方法中使用
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
来创建IOC容器和实例化Bean,在WEB应用中,应该在服务器加载该应用的时候创建IOC容器是最合适的,服务器被加载的时候可以通过ServletContextListener
来监听到状态(需要导入javax.servlet-api),再通过里面的contextInitialized(ServletContextEvent sec)
方法创建IOC容器,在WEB应用已启动就可以使用这个容器,现在是有这个IOC容器了,但是如果使用呢,在WEB应用中,可以将这个容器放到ServletContext(即application域)的一个属性中,这样以后在WEB应用中就可以使用这个IOC容器了。这里需要注意的是Spring的配置文件位置,一般非WEB应用会放在src目录下面,然后在主函数中通过下面的形式配置:
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
WEB应用完全可以将这个配置文件的配置写到WEB应用中初始化参数中,即web.xml中:
<context-param>
<param-name>configLocation</param-name>
<param-value>applicationContext.xml</param-value>
</context-param>
所以依据上面所述,对于Spring整合WEB的步骤很清晰,这里有一个最初的小栗子:
1. 创建一个实体类的Bean–Person(具体创建什么Bean,随你怎么搞)
public class Person {
private String userName;
public void setUserName(String userName) {
this.userName = userName;
}
public void hello () {
System.out.println("hello, my name is " + userName);
}
}
2. 创建监听器来监测WEB应用的生命周期,必须继承ServletContextListener接口
/**
* 监听WEB应用程序的生命周期
* Created by WeiguoLiu on 2018/3/4.
*/
public class SpringServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("应用初始化");
//1.获取Spring的配置文件名
ServletContext servletContext = sce.getServletContext();
String config = servletContext.getInitParameter("configLocation");
//在web应用初始化的时候,就创建IOC容器,所以将这一步放在contextInitialized方法中
//1.创建IOC容器
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
//2.把IOC容器放在ServletContext的一个属性中,后续的应用中可以使用这个属性
servletContext.setAttribute("ApplicationContext", ctx);
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
这里可以在启动WEB项目的时候,注意查看一下控制台的输出信息,Spring的IOC容器相关信息的生成时间位置。
3. 创建测试的Servlet
public class TestServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
System.out.println("WEB App has come to TestServlet");
//1.从application域对象中得到IOC容器的引用
ServletContext servletContext = getServletContext();
ApplicationContext ctx = (ApplicationContext) servletContext.getAttribute("ApplicationContext");
Person person = (Person) ctx.getBean("person");
person.hello();
}
}
4. 创建Spring的核心配置文件applicationContext.xml
因为在这个Spring整合WEB中没有涉及到其他框架,这里就简单的对实体类的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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="person" class="com.hhu.bean.Person">
<property name="userName" value="SpringWEB"/>
</bean>
</beans>
5. 在web.xml中注册监听器和Servlet
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<!--配置Servlet,如果是3.0也可以直接利用注解进行配置即可-->
<servlet>
<servlet-name>TestServlet</servlet-name>
<servlet-class>com.hhu.servlets.TestServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>TestServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--配置Spring配置文件的名字和位置-->
<context-param>
<param-name>configLocation</param-name>
<param-value>applicationContext.xml</param-value>
</context-param>
<!--启动IOC容器的ServletContextListenser-->
<listener>
<listener-class>com.hhu.listener.SpringServletContextListener</listener-class>
</listener>
</web-app>
6. 创建测试页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>测试Servlet页面</title>
</head>
<body>
<a href="TestServlet">TestServlet</a>
</body>
</html>
在Spring整合WEB时,Spring已经集成了监听WEB应用生命周期的ServletContextListener接口,所以实际上,上面的第2步可以直接省略,在配置的时候直接使用Spring-web自带的监听器org.springframework.web.context.ContextLoaderListener
就可以了,下面对上面的栗子进行改造,Spring的配置文件是同样的配置,去除了原来自己实现的监听器和Servlet(直接写入到jsp中),web.xml配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!--配置Spring的配置文件的名称和位置-->
<context-param>
<param-name>contextConfigLocation</param-name>
<!--当前类路径下的applicationContext.xml,其中classpath前缀Spring可以自动解析-->
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!--启动IOC容器的ServletContextListener,这个是Spring自带的-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
然后在测试页面中直接测试即可:
<%@ page import="org.springframework.web.context.support.WebApplicationContextUtils" %>
<%@ page import="org.springframework.context.ApplicationContext" %>
<%@ page import="com.hhu.beans.Person" %><%--
Created by IntelliJ IDEA.
User: WeiguoLiu
Date: 2018/3/5
Time: 9:10
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>测试Spring</title>
</head>
<body>
<%
//1.从application域对象中获取IOC容器,这里是通过Spring提供工具类获取,实际获取很麻烦,默认的名字很长
ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(application);
Person person = (Person) ctx.getBean("person");
System.out.println("has come to jsp");
person.hello();
%>
</body>
</html>
12.1 Spring整合Struts2
Spring整合Struts2主要是要实现让Spring的IOC容器来管理Struts2的Action(即是实现Spring的IOC容器中配置Struts的Action),这里在配置Struts2中的Action的Bean时,有一点非常重要,在Struts2中的Action是非单例的,而在Spring配置Bean的时候,默认是单例的,所以必须要加上scope
这个属性并赋值为原型。具体步骤如下:
1. 添加正常Struts2和Spring所需要的jar包,需要额外的struts2-spring-plugin的jar
2. 配置web.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<!--配置Spring的配置文件位置-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!--配置Spring的Listenser-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--配置Struts2的过滤器-->
<filter>
<filter-name>struts2</filter-name>
<!--2.5版本中的过滤器包名中没有ng-->
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
3. 配置Struts的配置文件struts.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
"http://struts.apache.org/dtds/struts-2.5.dtd">
<struts>
<constant name="struts.enable.DynamicMethodInvocation" value="false"/>
<constant name="struts.devMode" value="true"/>
<package name="default" namespace="/" extends="struts-default">
<!--以前只用Struts的时候,每次都是自己创建,class属性直接写为com.hhu.services.PersonService,
现在需要从IOC中获取,这里就是直接指向在Spring配置文件中配置的Bean的id-->
<action name="person-save" class="personAction">
<result>/success.jsp</result>
</action>
</package>
</struts>
【注意】这里在写Action的配置的时候,class属性属性就不要再写全部类名了,因为整合之后的Action都有Spring的IOC容器管理,所以我们直接从IOC容器中获取即可,这里是通过在Spring的核心配置文件applicationContext.xml中配置的Bean的id获取的Action;
4. Spring的核心配置文件applicationContext.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="person" class="com.hhu.entities.Person">
<property name="name" value="Spring_Struts2"/>
</bean>
<!--注意原来的Struts2中的Action不是单例的,而Spring中配置Bean默认是单例的,所以这里要将默认的单例改为原型的-->
<bean id="personAction" class="com.hhu.actions.PersonAction" scope="prototype">
<property name="personService" ref="personService"/>
</bean>
<bean id="personService" class="com.hhu.services.PersonService">
</bean>
</beans>
【注意】在Spring中配置的Bean默认是单例的,而在Struts2中,每次获取的Action都是新的(非单例),所以这里需要对Bean的Scope属性进行指定为原型即可。
5. 测试页面
<!--页面1:index.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>整合Struts</title>
</head>
<body>
<a href="person-save">Person Save</a>
</body>
</html>
<!--页面2:success.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>SUCCESS</title>
</head>
<body>
<h4>Success Page</h4>
</body>
</html>
点击后后台会打印相关的方法输出语句。
===================待续======================