Spring与Dao部分,是Spring的两大核心技术IoC与AOP的典型应用体现:对于JDBC模板的使用,是IoC的应用,是将JDBC模板对象注入给了Dao层的实现类。
为了避免直接使用JDBC而带来的复杂且冗长的代码,Spring提供了一个强有力的模板类—JdbcTemplate
来简化JDBC操作。并且,数据源DataSource
对象与模板JdbcTemplate
对象均可通过Bean的形式定义在配置文件中,充分发挥了依赖注入的威力。
代码举例: 1.首先除了Spring的基本包以及MySql驱动以外,还需要导入Spring-jdbc.jar和Spring-tx.jar两个jar包。
2.定于实体类Student
public class Student {
private Integer id;
private String name;
private int age;
public Student() {
super();
}
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
// getter and setter
// toString()
2.定义数据库及表
3.定义IStudentDao
public interface IStudentDao {
void insertStudent(Student student);
void deleteById(int id);
void updateStudent(Student student);
List<String> selectAllStudentsNames();
String selectStudentNameById(int id);
List<Student> selectAllStudents();
Student selectStudentById(int id);
}
4.初步定义StudentDaoImpl 这里仅仅定义一个StudentDaoImpl类实现了IStudentDao接口,但不具体写每个方法的方法实现。保持默认即可。后面会逐个通过Jdbc模板来实现。
public class StudentDaoImpl implements IStudentDao{
@Override
public void insertStudent(Student student) {
// TODO Auto-generated method stub
}
@Override
public void deleteById(int id) {
// TODO Auto-generated method stub
}
// .....
}
5.定义IStudentService
public interface IStudentService {
void addStudent(Student student);
void removeById(int id);
void modifyStudent(Student student);
List<String> findAllStudentsNames();
String findStudentNameById(int id);
List<Student> findAllStudents();
Student findStudentById(int id);
}
6.定义UserService
public class StudentServiceImpl implements IStudentService{
private IStudentDao dao;
public void setDao(IStudentDao dao) {
this.dao = dao;
}
@Override
public void addStudent(Student student) {
dao.insertStudent(student);
}
@Override
public void removeById(int id) {
dao.deleteById(id);
}
// .....
}
7.定义测试类MyTest
public class MyTest {
private IStudentService service;
@Before
public void init() {
// 初始化,创建容器对象并创建service对象
String resource = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
service = (IStudentService) ac.getBean("");
}
@Test
public void test01() {
Student student = new Student("小斌", 21);
service.addStudent(student);
}
//...
简单的测试框架搭建好后就可以进行数据源的配置,数据源直接以Bean的形式配置在Spring配置文件中。根据数据源的不同,其配置方式不同。下面主要讲解三种常用数据源的配置方式。
Spring默认的数据源为DriverManagerDataSource,其有一个属性DriverClassName,用于接收DB驱动,相关xml文件配置如下:
<bean id="myDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/spring_jdbc_test"></property>
<property name="username" value="root"></property>
<property name="password" value="***"></property>
</bean>
DBCP,DataBase Connection Pool,是apache下的项目,使用该数据源,需要导入org.apache.commons目录中dbcp与pool两个jar包。
<!-- 注册数据源:DBCP -->
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/spring_jdbc_test"></property>
<property name="username" value="root"></property>
<property name="password" value="***"></property>
</bean>
使用C3P0数据源,需要导入com.mchange.c3p0的jar包。
相关配置:
<!-- 注册数据源:c3p0-->
<bean id="myDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/spring_jdbc_test"></property>
<property name="user value="root"></property>
<property name="password" value="***"></property>
</bean>
注意,如果导入c3p0包版本为0.9.1.2,不会发生问题,但是如果是较其新的版本则要再导入mchange-commons包,不然会报错java.lang.NoClassDefFoundError: com/mchange/v2/ser/Indirector
为了方便数据库的配置,一般将数据库配置文件放在src下面
jdbc.driver=com.mysql.jdbc.mysql
jdbc.url=jdbc:mysql://127.0.0.1:3306/spring_jdbc_test
jdbc.user=root
jdbc.password=***
该属性文件若要被Spring配置文件读取,其必须在配置文件中进行注册。注册方式有两种:<bean/>
方式<context>
方式,如果使用context方式,需要在Spring配置文件头部加入context的约束
<!-- 注册属性方式一 -->
<!-- <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:jdbc.properties"></property>
</bean> -->
<!-- 注册属性方式二 -->
<context:property-placeholder location="classpath:jdbc.properties" />
整体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 http://www.springframework.org/schema/context/spring-context.xsd">
<!-- bean definitions here -->
<!-- 注册属性方式一 -->
<!-- <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:jdbc.properties"></property>
</bean> -->
<!-- 注册属性方式二 -->
<context:property-placeholder location="classpath:jdbc.properties" />
<!-- 注册数据源:c3p0-->
<!-- <bean id="myDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcurl" value="jdbc:mysql://127.0.0.1:3306/spring_jdbc_test"></property>
<property name="user" value="root"></property>
<property name="password" value="***"></property>
</bean> -->
<bean id="myDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 注册数据源:DBCP -->
<!-- <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/spring_jdbc_test"></property>
<property name="username" value="root"></property>
<property name="password" value="***"></property>
</bean> -->
<!-- 注册数据源:Spring默认 -->
<!-- <bean id="myDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/spring_jdbc_test"></property>
<property name="username" value="root"></property>
<property name="password" value="***"></property>
</bean> -->
<!-- 注册jdbcTemplate -->
<bean id="myJdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="myDataSource"></property>
</bean>
<!-- 注册dao -->
<bean id="studentDao" class="love.minmin.dao.StudentDaoImpl">
<property name="jdbcTemplate" ref="myJdbcTemplate"></property>
</bean>
<!-- 注册service -->
<bean id="studentService" class="love.minmin.services.StudentServiceImpl">
<property name="dao" ref="studentDao"></property>
</bean>
</beans>
注意: Dao实现类继承自JdbcDaoSupport类,而JdbcDaoSupport类中有一个属性JdbcTemplate,用于接收JDBC模板。所以Dao实现类继承了JdbcDaoSupport类后,也就具有了JDBC模板属性。在配置文件中,只要将模板对象注入即可,即上面关于dao的配置。 但是再仔细查看JdbcDaoSupport类,发现其有一个dataSource属性,查看setDataSource()方法体可知,若JDBC模板为null,则会自动创建一个模板对象。
/**
* Set the JDBC DataSource to be used by this DAO.
*/
public final void setDataSource(DataSource dataSource) {
if (this.jdbcTemplate == null || dataSource != this.jdbcTemplate.getDataSource()) {
this.jdbcTemplate = createJdbcTemplate(dataSource);
initTemplateConfig();
}
}
故,在Spring配置文件中,对于JDBC模板对象的配置完全可以省去,而是在Dao实现类中直接注入数据源对象。这样会让系统自动创建JDBC模板对象。 即dao的配置文件可改为:
<!-- 注册dao二 -->
<bean id="studentDao" class="love.minmin.dao.StudentDaoImpl">
<property name="dataSource" ref="myDataSource"></property>
</bean>
JdbcTemplate类中提供了对DB进行修改、查询的方法。Dao实现类使用继承自JdbcDaoSupport的getTemplate()方法,可以获取到JDBC模板对象。
对DB的增、删、改都是通过update()方法实现的。该方法常用的重载方法有两个: public int update ( String sql) public int update ( String sql, Object… args)
@Override
public void insertStudent(Student student) {
String sql = "insert into student(name, age) values (?, ?)";
this.getJdbcTemplate().update(sql, student.getName(), student.getAge());
}
@Override
public void deleteById(int id) {
String sql = "delete from student where id=?";
this.getJdbcTemplate().update(sql, id);
}
@Override
public void updateStudent(Student student) {
String sql = "update student set name=?, age=? where id=?";
this.getJdbcTemplate().update(sql, student.getName(), student.getAge(), student.getId());
}
JDBC模板的查询结果均是以对象的形式返回。根据返回对象类型的不同,可以将查询分为两类:简单对象查询,与自定义对象查询。
常用的简单对象查询方法有:查询结果为单个对象的queryForObject()与查询结果为List的queryForList()。
pubic T queryForObject (String sql, Class
@Override
public List<String> selectAllStudentsNames() {
String sql = "select name from student";
return this.getJdbcTemplate().queryForList(sql, String.class);
}
@Override
public String selectStudentNameById(int id) {
String sql = "select name from student where id=?";
return this.getJdbcTemplate().queryForObject(sql, String.class, id);
}
常用的自定义对象查询方法有:查询结果为单个对象的queryForObject()与查询结果为List的query()。
pubic T queryForObject (String sql, RowMapper
实现RowMapper接口:
public class StudentRowMapper implements RowMapper<Student>{
// rs:当查询出总的结果集时,框架回自动遍历这个结果集,每一次遍历一次数据,
// 都会存放在rs中,这里rs代表的时一行的数据,并非所有查询结果,换个角度来说,只要能执行到
// 这个方法,rs就不会为空
// 实现将查询结果中当前行的数据包装为一个指定对象。
@Override
public Student mapRow(ResultSet rs, int rowNum) throws SQLException {
Student student = new Student();
student.setName(rs.getString("name"));
student.setAge(rs.getInt("age"));
student.setId(rs.getInt("id"));
return student;
}
}
实现service中的方法:
@Override
public List<Student> selectAllStudents() {
String sql = "select * from student";
return this.getJdbcTemplate().query(sql, new StudentRowMapper());
}
@Override
public Student selectStudentById(int id) {
String sql = "select * from student where id=?";
return this.getJdbcTemplate().queryForObject(sql, new StudentRowMapper(), id);
}
注意:JdbcTemplate对象是多例的,即系统会为每一个使用模板对象的线程(方法)创建一个JdbcTemplate实例,并且在该线程(方法)结束时,自动释放JdbcTemplate实例。所以在每次使用JdbcTemplate对象时,都需要通过getJdbcTemplate()方法获取,所以代码不能时一下形式:
public class StudentDaoImpl extends JdbcDaoSupport implements IStudentDao{
private JdbcTemplate jt;
public StudentDaoImpl() {
jt = new JdbcTemplate();
}
@Override
public void insertStudent(Student student) {
String sql = "insert into student(name, age) values (?, ?)";
jt.update(sql, student.getName(), student.getAge());
}
Bean实例在调用无参构造器创建了空值对象后,就要对Bean对象的属性进行初始化。初始化是由容器自动完成的,称为注入。根据注入方式的不同,常用的有两类:设值注入、构造注入。
设值注入是指,通过setter方法传入被调用者的实例。这种注入方式简单、直观,因而在Spring的依赖注入中大量使用。
代码:
// 定义学生类
public class Student {
private int age;
private String name;
private School school; // 对象属性,域属性
public void setName(String name) {
System.out.println("执行setName");
this.name = name;
}
public void setAge(int age) {
System.out.println("执行setAge");
this.age = age;
}
public void setSchool(School school) {
this.school = school;
}
@Override
public String toString() {
return "Student [age=" + age + ", name=" + name + ", school=" + school + "]";
}
}
// 定义学校类
public class School {
private String name;
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "School [name=" + name + "]";
}
}
// 注册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 definitions here -->
<!-- 注册student -->
<bean id="myStudent" class="love.minmin.di01.Student">
<property name = "name" value="minmin"></property>
<property name = "age" value="18"></property>
<property name = "school" ref="mySchool"></property>
</bean>
<!-- 注册school -->
<bean id="mySchool" class="love.minmin.di01.School">
<property name = "name" value="NNU"></property>
</bean>
</beans>
// 测试
public class MyTest {
@Test
public void test01() {
String resource = "/love/minmin/di01/applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
Student student = (Student) ac.getBean("myStudent");
System.out.println(student);
((ClassPathXmlApplicationContext) ac).close();
}
}
构造注入是指,在构造调用者实例的同时,完成被调用者的实例化。即使用构造器设置依赖关系。
代码:
// 定义学生类
public class Student {
private int age;
private String name;
private School school; // 对象属性,域属性
public Student(int age, String name, School school) {
super();
this.age = age;
this.name = name;
this.school = school;
}
@Override
public String toString() {
return "Student [age=" + age + ", name=" + name + ", school=" + school + "]";
}
}
// 定义school类
public class School {
private String name;
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "School [name=" + name + "]";
}
}
// 定义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 definitions here -->
<!-- 注册student -->
<bean id="myStudent" class="love.minmin.di02.Student">
<constructor-arg name="name" value="minmin"></constructor-arg>
<constructor-arg name="age" value="18"></constructor-arg>
<constructor-arg name="school" ref="mySchool"></constructor-arg>
</bean>
<!-- 注册school -->
<bean id="mySchool" class="love.minmin.di02.School">
<property name = "name" value="NNU"></property>
</bean>
</beans>
注意在Student中可以不用定义无参构造器,因为构造注入底层直接调用了带参构造器,而且也不用setter方法。但在实际情况中这种方法很少用,因为一个类的成员变量会有很多个,这个时候带参构造器就很难写。
AOP(Aspect Orient Programming),面向切面编程,是面向对象编程OOP的一种补充。面向对象编程是从静态角度考虑程序的结构,而面向切面编程是从动态角度考虑程序运行过程。
AOP底层,就是采用动态代理模式实现的。采用了两种代理:JDK的动态代理,与CGLIB的动态代理。 关于代理模式可参考之前写的博客:Java动态代理
面向切面编程,就是将交叉业务逻辑封装成切面,利用AOP容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志等。
AOP是由AOP联盟提出的一种编程思想,提出的一套编程规范。而Spring是AOP这套规范的一种实现.所以,需要导入AOP联盟的规范(接口)包:aopalliance-1.0.jar
及Spring对其的实现包:spring-aop-4.2.1.RELEASE.jar
。
通知(Advice),切面的一种实现,可以完成简单织入功能(织入功能就是在这里完成的)。常用通知有:前置通知、后置通知、环绕通知、异常处理通知。
定义前置通知,需要实现MethodBeforeAdvice
接口。该接口中有一个方法before()
,会在目标方法执行之前执行。
前置通知的特点:
通知的用法举例: 1.定义目标类
// 目标类
public class SomeServiceImpl implements ISomeService{
// 目标方法
@Override
public void doFirst() {
System.out.println("do first");
}
// 目标方法
@Override
public void doSecond() {
System.out.println("do second");
}
}
2.通知类是指,实现了相应通知类型接口的类。当前,实现了这些接口,就要实现这些接口中的方法,而这些方法的执行,则是根据不同类型的通知,其执行时机不同。 前置通知:在目标方法执行之前执行 后置通知:在目标方法执行之后执行 环绕通知:在目标方法执行之前与之后均执行 异常处理通知:在目标方法执行过程中,若发生指定异常,则执行通知中的方法。 这里定义前置通知类。
// 定义前置通知
public class MyMethodBeforeAdvice implements MethodBeforeAdvice{
// 织入的具体方法在这里完成
/*
* method:业务方法
* args[]: 业务方法参数
* target:目标对象
*/
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
// 目标方法的增强代码
System.out.println("执行前置通知方法");
}
}
3.注册目标类,在Spring配置文件中注册目标对象Bean
<!-- 配置目标对象 -->
<bean id="someService" class="love.minmin.services.SomeServiceImpl" />
4.注册通知切面,即在Spring配置文件中注册定义的通知对象Bean。
<!-- 配置切面:通知 -->
<bean id="MyMethodBeforeAdvice" class="love.minmin.services.MyMethodBeforeAdvice" />
5.注册代理工厂Bean类对象ProxyFactoryBean
<!-- 配置代理 -->
<bean id="serviceProxy"
class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 目标类 -->
<property name="target" ref="someService"></property>
<!-- <property name="targetName" value="someService"></property> -->
<!-- 接口 -->
<!-- 设置目标对象所实现的业务接口,要求给出接口的全既定性类名。
此属性可以不进行设置,因为打开ProxyFactoryBean的源码,
可以看到其有个自动检测目标类的所有接口属性autodetectInterfaces, 默认值为true。
即不设置也可以自动检测到。 当然,此时使用的是jdk的Proxy动态代理。 -->
<property name="interfaces"
value="love.minmin.services.ISomeService"></property>
<!-- 切面 -->
<property name="interceptorNames"
value="MyMethodBeforeAdvice"></property>
</bean>
6.客户端访问动态代理对象
@Test
public void test01() {
String resource = "/love/minmin/services/applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
// 客户端访问的是动态代理对象,而非原目标对象。因为代理对象可以将交叉业务逻辑按照通知类型,动态的织入到目标对象的执行中。
ISomeService service = (ISomeService) ac.getBean("serviceProxy");
service.doFirst();
System.out.println("===============");
service.doSecond();
}
对于测试类,需要注意,从容器中获取的是代理对象,而非目标对象。
定义后置通知,需要实现接口AfterReturningAdvice
。该接口中有一个方法afterReturning()
,会在目标方法执行之后执行。
后置通知的特点:
代码举例: 1.将doSecond()方法返回值改为String类型
// 目标类
public class SomeServiceImpl implements ISomeService{
// 目标方法
@Override
public void doFirst() {
System.out.println("do first");
}
// 目标方法
@Override
public String doSecond() {
System.out.println("do second");
return "abc";
}
}
2.定义后置通知切面
// 后置通知,可以获取到目标方法的返回结果,但是无法改变目标方法的结果
public class MyAfterReturningAdvice implements AfterReturningAdvice{
// 在目标方法执行之后执行
// returnValue:目标方法执行结果
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
if(returnValue != null) {
returnValue = returnValue.toString().toUpperCase();
}
System.out.println("执行后置通知: " + returnValue);
}
}
3.修改xml文件相关配置
定义环绕通知,需要实现MethodInterceptor接口。环绕通知,也叫方法拦截器,可以在目标方法调用之前及之后做处理,可以改变目标方法的返回值,也可以改变程序执行流程。
部分代码:
public class MyMethodInterceptor implements MethodInterceptor{
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("环绕执行通知: 目标执行方法之前");
// 执行目标方法
System.out.println("环绕执行通知: 目标执行方法之后");
Object result = invocation.proceed();
if(result != null) {
result = result.toString().toUpperCase();
}
return result;
}
}
定义异常通知,需要实现ThrowsAdvice接口。该接口的主要作用是,在目标方法抛出异常后,根据异常的不同做出相应的处理。当该接口处理完异常后,会简单地将异常再次抛出给目标方法。
不过,这个接口较为特殊,从形式上看,该接口中没有必须要实现的方法。但,这个接口却确实有必须要实现的方法afterThrowing()。这个方法重载了四种形式。由于使用时,一般只使用其中一种,若要都定义到接口中,则势必要使程序员在使用时必须要实现这四个方法。这是很麻烦的。所以就将该接口定义为了标识接口(没有方法的接口)。
在打开ThrowsAdvice源码后,上侧的注释部分可以看到四个方法, 但是最常用的时这个方法:
public void afterThrowing(自定义的异常类 e)
这里的参数e为,与具体业务相关的用户自定义的异常类对象。容器会根据异常类型的不同,自动选择不同的该方法执行。这些方法的执行是在目标方法执行结束后执行的。
代码举例: 1.定义异常类的父类
/*
* 异常分类:
* 1.运行时异常, 不进行处理也可以通过编译
* 继承自RuntimeException
* 2.编译时异常: checked exception 不进行处理,无法通过编译
* 继承自Exception
*/
public class UserException extends Exception{
public UserException() {
super();
}
public UserException(String message) {
super(message);
}
}
2.定义异常类的子类
/*
* 异常分类:
* 1.运行时异常, 不进行处理也可以通过编译
* 继承自RuntimeException
* 2.编译时异常: checked exception 不进行处理,无法通过编译
* 继承自Exception
*/
public class UserException extends Exception{
public UserException() {
super();
}
public UserException(String message) {
super(message);
}
}
public class PasswordException extends UserException{
public PasswordException() {
super();
}
public PasswordException(String message) {
super(message);
}
}
3.定义业务接口。要抛出异常父类。
// 业务接口
public interface ISomeService {
// 业务方法
boolean login(String name, String password) throws UserException;
}
4.定义目标类。
// 目标类
public class SomeServiceImpl implements ISomeService{
@Override
public boolean login(String name, String password) throws UserException {
if(! "minmin".equals(name)) {
throw new UserNameException("name wrong");
}
if(! "love".equals(password)) {
throw new PasswordException("password wrong");
}
double a = 3/0;
return true;
}
}
5.定义异常通知
public class MyThrowsAdvice implements ThrowsAdvice{
// 当目标方法抛出与指定类型的异常具有"is a"关系的异常时执行。
public void afterThrowing(UserNameException ex) {
System.out.println("name exception: " + ex.getMessage());
}
// 当目标方法抛出与指定类型的异常具有"is a"关系的异常时执行。
public void afterThrowing(PasswordException ex) {
System.out.println("password exception: " + ex.getMessage());
}
// 当目标方法抛出与指定类型的异常具有"is a"关系的异常时执行。
public void afterThrowing(Exception ex) {
System.out.println("exception: " + ex.getMessage());
}
}
6.配置xml文件并测试
public class MyTest {
@Test
public void test01() throws UserException {
String resource = "/love/minmin/aop04/applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
ISomeService service = (ISomeService) ac.getBean("serviceProxy");
service.login("minmin", "love");
((ClassPathXmlApplicationContext) ac).close();
}
}
1.若要给目标方法织入多个切面,则需要在配置代理对象的切面(interceptorNames)属性时,设定为list或者array,也可以使用简便写法,如下:
<property name="interceptorNames" value="MyAfterReturningAdvice, MyMethodBeforeAdvice">
<!-- <list>
<value>MyAfterReturningAdvice</value>
<value>MyMethodBeforeAdvice</value>
</list> -->
</property>
2.若ProxyFactoryBean配置中target没有实现接口,则会自动采用cglib的方式进行动态代理
3 若target实现了接口,但是想要使用cglib进行动态代理,可以在ProxyFactoryBean的配置中添加proxyTargetClass
属性,将其设为true,或者指定optimize
属性为true。
通知(Advice)是Spring提供的一种切面(Aspect)。但其功能过于简单:只能将切面织入到目标类的所有目标方法中,无法完成将切面织入到指定目标方法中。
顾问(Advisor)是Spring提供的另一种切面。其可以完成更为复杂的切面织入功能。PointcutAdvisor是顾问的一种,可以指定具体的切入点。顾问将通知进行了包装,会根据不同的通知类型,在不同的时间点,将切面织入到不同的切入点。
NameMatchMethodPointcutAdvisor,即名称匹配方法切入点顾问。容器可根据配置文件中指定的方法名来设置切入点。 其中对于切入点的指定可以使用通配符。
RegexpMethodPointcutAdvisor,即正则表达式方法顾问。容器可根据正则表达式来设置切入点。注意,与正则表达式进行匹配的对象是接口中的方法名,而非目标类(接口的实现类)的方法名,并且为全限定方法名。
前面代码中所使用的代理对象,均是由ProxyFactoryBean
代理工具类生成的。而该代理工具类存在着如下缺点:
Spring提供了自动代理生成器,用于解决ProxyFactoryBean
的问题。常用的自动代理生成器有两个:
需要注意的是,自动代理生成器均继承自Bean后处理器BeanPostProcessor
。容器中所有Bean在初始化时均会自动执行Bean后处理器中的方法,故其无需id属性。所以自动代理生成器的Bean也没有id属性,客户类直接使用目标对象bean的id。
DefaultAdvisorAutoProxyCreator代理的生成方式是,将所有的目标对象与Advisor
自动结合,生成代理对象。无需给生成器做任何的注入配置。注意,只能与Advisor
配合使用。
相关xml配置:
<!-- 配置目标对象 -->
<bean id="someService"
class="love.minmin.aop02.SomeServiceImpl" />
<!-- 配置切面:通知 -->
<bean id="MyMethodBeforeAdvice"
class="love.minmin.aop02.MyMethodBeforeAdvice" />
<!-- 配置切面:顾问 -->
<bean id="myAdvisor"
class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
<property name="advice" ref="MyMethodBeforeAdvice"></property>
<property name="mappedNames" value="doFirst"></property>
</bean>
<!-- 配置自动代理 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
DefaultAdvisorAutoProxyCreator存在两个问题
BeanNameAutoProxyCreator的代理生成方式是,根据bean的id,来为符合相应名称的类生成相应代理对象,且切面既可以是顾问Advisor又可以是通知Advice。
相关xml配置:
<!-- 配置自动代理 -->
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames" value="someService"></property>
<property name="interceptorNames" value="myAdvisor"></property>
</bean>
beanNames:指定要增强的目标类的id interceptorNames:指定切面。可以是顾问Advisor,也可以是通知Advice。
对于AOP这种编程思想,很多框架都进行了实现。Spring就是其中之一,可以完成面向切面编程。然而,AspectJ是另一个框架,也实现了AOP的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring又将AspectJ的对于AOP的实现也引入到了自己的框架中。
AspectJ中常用的通知有五种类型: (1)前置通知 (2)后置通知 (3)环绕通知 (4)异常通知 (5)最终通知 其中最终通知是指,无论程序执行是否正常,该通知都会执行。类似于try..catch中的finally代码块。
实现步骤: 1.定义业务接口与实现类
// 业务接口
public interface ISomeService {
// 业务方法
void doFirst();
// 业务方法
String doSecond();
// 业务方法
String doThird();
// 业务方法
String doForth();
}
// 目标类
public class SomeServiceImpl implements ISomeService{
// 目标方法
@Override
public void doFirst() {
System.out.println("do first");
}
// 目标方法
@Override
public String doSecond() {
System.out.println("do second");
return "adc";
}
// 目标方法
@Override
public String doThird() {
return "abc";
}
@Override
public String doForth() {
int a = 3/0;
return null;
}
}
2.定义切面POJO类,其中定义了若干普通方法,将作为不同的通知方法,作为切面出现。
@Aspect
public class MyAspect {
@Before("execution(* *..ISomeService.doFirst())")
public void beforeSome() {
System.out.println("前置增强");
}
@Before("execution(* *..ISomeService.doFirst())")
public void beforeSome(JoinPoint jp) {
System.out.println("前置增强 切入点表达式: " + jp);
System.out.println("前置增强 方法签名: " + jp.getSignature());
System.out.println("前置增强 目标对象: " + jp.getTarget());
}
@AfterReturning(value="execution(* *..ISomeService.doSecond())", returning = "result")
public void AfterSome(Object result) {
System.out.println("后置增强");
System.out.println(result);
}
@Around("execution(* *..ISomeService.doThird())")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕通知之前");
Object result = pjp.proceed();
System.out.println("环绕通知之后");
System.out.println(result);
if(result != null) {
result = result.toString().toUpperCase();
}
return result;
}
@AfterThrowing(value="execution(* *..ISomeService.doForth())", throwing = "e")
public void afterAThrowing(Throwable e) {
System.out.println("异常通知: " + e.getMessage());
}
@After("myPointcut()")
public void after() {
System.out.println("最终方法");
}
// 定义一个PointCut,代表方法myPointcut
@Pointcut("execution(* *..ISomeService.doForth())")
private void myPointcut() {}
}
3.注册目标对象与POJO切面类
<!-- 配置目标对象 -->
<bean id="someService" class="love.minmin.aop01.SomeServiceImpl" />
<!-- 配置切面 -->
<bean id="myAspect" class="love.minmin.aop01.MyAspect" />
4.注册AspectJ的自动代理 在定义好切面Aspect后,需要通知Spring容器,让容器生成“目标类 + 切面”的代理对象。这个代理是由容器自动生成的。只需要在Spring配置文件中注册一个基于aspectj的自动代理生成器,其就会自动扫描到@Aspect注解,并按通知类型与切入点,将其织入,并生成代理。
<!-- 配置aspectj自动代理 -->
<aop:aspectj-autoproxy />
<aop:aspectj-autoproxy/>
的底层是由AnnotationAwareAspectJAutoProxyCreator实现的。从其类名就可看出,是基于AspectJ的注解适配自动代理生成器。
其工作原理是,
切面就是一个POJO类,而用于增强的方法就是普通的方法。通过配置文件,将切面中的功能增强织入到了目标类的目标方法中。
基于xml的AOP实现,去除了POJO类的注解,改而在xml文件中进行配置,这里记录xml配置的改变,其他文件同上文一样。
<!-- 配置目标对象 -->
<bean id="someService" class="love.minmin.aop02.xml.SomeServiceImpl" />
<!-- 配置切面 -->
<bean id="myAspect" class="love.minmin.aop02.xml.MyAspect" />
<!-- 配置aop-->
<aop:config>
<!-- 定义切入点 -->
<aop:pointcut expression="execution(* *..ISomeService.doFirst())" id="mydoFirstPointCut"/>
<aop:pointcut expression="execution(* *..ISomeService.doSecond())" id="mydoSecondPointCut"/>
<aop:pointcut expression="execution(* *..ISomeService.doThird())" id="mydoThirdPointCut"/>
<aop:pointcut expression="execution(* *..ISomeService.doForth())" id="mydoForthPointCut"/>
<!-- 定义切面 -->
<aop:aspect ref="myAspect">
<aop:before method="beforeSome" pointcut-ref="mydoFirstPointCut"/>
<aop:before method="beforeSome(org.aspectj.lang.JoinPoint)" pointcut-ref="mydoFirstPointCut"/>
<aop:after-returning method="AfterSome" pointcut-ref="mydoSecondPointCut" returning="result"/>
<aop:around method="myAround" pointcut-ref="mydoThirdPointCut"/>
<aop:after-throwing method="afterAThrowing" pointcut-ref="mydoForthPointCut" throwing="e"/>
<aop:after method="after" pointcut-ref="mydoForthPointCut"/>
</aop:aspect>
</aop:config>
Java是强类型的编程语言,定义变量时必须声明变量的类型,数据类型分为两种,基本数据类型和引用数据类型。
1.整数类型
2.浮点数类型
注意:
(1) 在long类型后面需要加l或者L,在float类型下需要加f或者F。
(2) 精度损失时需要进行强转
代码举例:
double d = 12.345;
float f = (float) d;
// 将double类型的12.345强转为float类型
float f1 = (float) 12.345;
// f2直接就是float类型, 建议采用这种方式。
float f2 = 12.345F;
3.字符数据类型:占用空间2个字节,可以用0-65535范围的整数进行赋值。所以可以存储汉字
4.布尔类型:布尔数据类型只有两个可能的值:真和假。使用此数据类型为跟踪真/假条件的简单标记。这种数据类型就表示这一点信息,但是它的“大小”并不是精确定义的。
可以看出,boolean类型没有给出精确的定义,《Java虚拟机规范》给出了4个字节,和boolean数组1个字节的定义,具体还要看虚拟机实现是否按照规范来,所以1个字节、4个字节都是有可能的。这其实是运算效率和存储空间之间的博弈,两者都非常的重要。
这里涉及到long可以转换为float,long为64位,float32位,但是为什么还是可以转换?原因在于float的数值表示范围大于long的表示范围,并且底层存储结构不同, 具体可参考float存储原理
另外注意:
代码举例:
{
byte a = 1;
byte b = 2;
// 编译出错,需要进行强转:a = (byte) (a + b), 而且byte优先级大于+,所以a = (byte) a + b这种写法是错误的。
a = a + b;
// 编译正确,+=直接隐式将右边的变量转换为左边的数据类型,在这里转为了byte类型,等价于 a = (byte) (a + b)
//而且+= 使用的位运算,速度比上面的表达式快。
a += b;
}
// 几个常见byte值:
byte b1 = (byte) 128 // -128
byte b2 = (byte) 129 // -127
byte b3 = (byte) 130 // -126
// byte short定义的时候接收的式int值,然后自己做数据判断,看在不在范围内。
在Java中,很多方法需要接收引用类型的对象,因此JDK提供了乙烯类的包装类,通过这些包装类可以把基本数据类型的值包装为引用类型的对象。
// int -- String
int number = 100;
String str = String.valueOf(number);
String str2 = Integer.toString(number);
// String -- int
String str = "100";
Integer number = new Integer(str);
int num = number.intValue();
// 注意,参数不能为null
int num2 = Integer.parseInt(str);
// 十进制到其他进制,进制范围:2-36
// 注意,参数不能为null
Integer.toString(100, 2);// 1100100
// 其他进制转为十进制
Integer.parseInt("100", 2); // 4
JDK5新特性,支持自动拆装箱 装箱:基本类型转为引用类型 拆箱:引用类型转为基本类型
// 自动装箱
Integer i = 100;
// 注意判断不能为null
if(i != null) {
i += 100;
}
// 自动拆箱
int y = i;
// 通过反编译工具可以查看上一段代码
// 自动装箱
Integer i = Integer.valueOf(100);
// 先自动拆箱然后自动装箱
i = Integer.valueOf(i.intValue() + 100);
先看一段测试代码
Integer i1 = new Integer(127);
Integer i2 = new Integer(127);
System.out.println(i1 == i2); // false
System.out.println(i1.equals(i2)); // true
Integer i3 = new Integer(128); Integer i4 = new Integer(128);
System.out.println(i3 == i4); // false
System.out.println(i3.equals(i4)); // true
Integer i5 = 127;
Integer i6 = 127;
System.out.println(i5 == i6); //true
System.out.println(i5.equals(i6)); //true
Integer i7 = 128;
Integer i8 = 128;
System.out.println(i7 == i8); //false
System.out.println(i7.equals(i8)); //true
}
通过反编译工具可以看到Integer i = 127
;在执行时的语句为Integer i = Integer.valueof(127)
, 而new Integer(127) 与 Integer.valueof(127)区别在于
valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容,不在则使用new Integer(i)进行创建对象。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
在java8中,Integer缓存吃的大小默认为-128-127
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
基本类型对应的缓冲池如下:
Bean的装配,即Bean对象的创建。容器根据代码要求创建Bean对象后再传递给代码的过程,称为Bean的装配。
代码通过getBean()方式从容器获取指定的Bean实例,容器首先会调用Bean类的无参构造器,因为底层使用的是反射机制,反射中newInstance调用了无参构造器,若类中没有无参构造器则会报错。创建空值的实例对象。 代码:
public interface ISomeService {
void doSome();
}
public class SomeServiceImp implements ISomeService {
private int age = 1;
public SomeServiceImp() {
System.out.println("constructor age: " + age);
}
public SomeServiceImp(int age) {
super();
this.age = age;
}
@Override
public void doSome() {
System.out.println("do some");
}
}
public class MyTest {
@Test
public void test01() {
String resource = "/love/minmin/beanAssmble01/applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
ISomeService service = (ISomeService) ac.getBean("myService");
service.doSome();
}
}
有些时候,项目中需要通过工厂类来创建Bean实例,而不能像前面例子中似的,直接由Spring容器来装配Bean实例。使用工厂模式创建Bean实例,就会使工厂类与要创建的Bean类耦合到一起。
将动态工厂Bean作为普通Bean来使用是指,在配置文件中注册过动态工厂Bean后,测试类直接通过getBean()获取到工厂对象,再由工厂对象调用其相应方法创建相应的目标对象。配置文件中无需注册目标对象的Bean。因为目标对象的创建不由Spring容器来管理。
代码:
// 定义工厂类
public class ServiceFactory {
public ISomeService getSomeService() {
return new SomeServiceImp();
}
}
// 在applicationContext中配置工厂类
<?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 definitions here -->
<!-- 注册factory -->
<bean id="myFactory" class="love.minmin.beanAssmble02.ServiceFactory" />
</beans>
// 测试
public class MyTest {
@Test
public void test01() {
String resource = "/love/minmin/beanAssmble02/applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
// 从Spring容器中获取factory
ServiceFactory factory = (ServiceFactory) ac.getBean("myFactory");
// 通过factory获取service
ISomeService service = factory.getSomeService();
service.doSome();
}
}
这样做的缺点是,不仅工厂类与目标类耦合到了一起,测试类与工厂类也耦合到了一起。
Spring对于使用动态工厂来创建的Bean,有专门的属性定义。factory-bean
指定相应的工厂Bean,由factory-method
指定创建所用方法。此时配置文件中至少会有两个Bean的定义:工厂类的Bean,与工厂类所要创建的目标类Bean。而测试类中不再需要获取工厂Bean对象了,可以直接获取目标Bean对象。实现测试类与工厂类间的解耦。
代码实现:
// 添加xml配置
<!-- 注册factory -->
<bean id="myFactory" class="love.minmin.beanAssmble02.ServiceFactory" />
<!-- 注册service -->
<bean id="myService" factory-bean="myFactory" factory-method="getSomeService" />
// 修改测试类
@Test
public void test2() {
String resource = "/love/minmin/beanAssmble02/applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
SomeServiceImp service = (SomeServiceImp) ac.getBean("myService");
service.doSome();
}
静态工厂无需工厂实例,所以不再需要定义静态工厂<bean/>
。而对于工厂所要创建的Bean,其不是由自己的类创建的,所以无需指定自己的类。但其是由工厂类创建的,所以需要指定所用工厂类。故class属性指定的是工厂类而非自己的类。当然,还需要通过factory-method属性指定工厂方法。
代码:
// 定义静态工厂
public class ServiceFactory {
public static ISomeService getSomeService() {
return new SomeServiceImp();
}
}
// 定义xmml文件
<?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 definitions here -->
<!-- 注册service -->
<bean id="myService" class="love.minmin.beanAssmble03.ServiceFactory" factory-method="getSomeService" />
</beans>
// 定义测试类
public class MyTest {
@Test
public void test() {
String resource = "/love/minmin/beanAssmble03/applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
ISomeService service = (ISomeService) ac.getBean("myService");
service.doSome();
}
}
当通过Spring容器创建一个Bean实例时,不仅可以完成Bean的实例化,还可以通过scope属性,为Bean指定特定的作用域。Spring支持5种作用域。
<bean />
的实例都是一个新的实例,容器初始化是不生成对象,在执行getBean时才创建对象,效果等同于BeanFactory。在软件开发过程中存在这样的情况,某类的某个方法的实现需要几个固定的步骤。在这些固定的步骤中,对于该类的不同对象,有些步骤的实现是固定不变的,有些步骤的实现是大相径庭的,有些是可变可不变的,对于这种情况就适用于模板方法设计模式。
模板方法设计模式的定义为:定义一个操作中某个算法的框架,然后将一些步骤延迟到子类中。模板方法模式可以使子类在不改变一个算法结构的前提下,对某些步骤实现个性化定义。
在模板方法设计模式中,存在一个父类,其中包含两类方法。
实现某个算法的方法步骤,这些步骤都是调用步骤方法完成的。
1.定义父类
public abstract class ShoppingService {
// 模板方法
public final void shopping() {
login(); // 登录网站
choose(); // 挑选商品
pay(); // 付款
}
// 子类不能重写
public final void login() {
System.out.println("login");
}
// 子类必须实现
public abstract void choose();
// 钩子方法:子类可以重写
public void pay() {
System.out.println("alipay");
}
}
2.定义两个子类
public class DressShoopingService extends ShoppingService{
@Override
public void choose() {
System.out.println("choose Goach");
}
@Override
public void pay() {
System.out.println("choose wechat");
}
}
public class ShoseShoopingService extends ShoppingService{
@Override
public void choose() {
System.out.println("choose Nike");
}
}
3.定义测试类
public class MyTest {
public static void main(String[] args) {
DressShoopingService dress = new DressShoopingService();
ShoseShoopingService shose = new ShoseShoopingService();
dress.shopping();
shose.shopping();
}
}