系列文章目录
保姆级别的介绍Spring实现JDBC原理的封装
文章目录
一、Spring配置数据源连接池
由Spring来管理数据源
- 导入spring-jdbc-x.x.x.Release.jar包和相关数据库驱动包(此处用MySQL为例)
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.6.RELEASE</version>
</dependency>
- Spring jdbcd不带连接池的数据源配置
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF8&serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
- 数据源的使用
DataSource ds = (DataSource)context.getBean("dataSource");
Connection conn=ds.getConnection();
使用阿里的Druid连接池
- 引入druid的包
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.27</version>
</dependency>
- 在Ioc中配置druid的Bean
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 基本属性 driver、url、user、password -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF8&autoReconnect=true&autoReconnectForPools=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="1"/>
<property name="minIdle" value="1"/>
<property name="maxActive" value="20"/>
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="60000"/>
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000"/>
<property name="validationQuery" value="SELECT 'x'"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
<property name="poolPreparedStatements" value="true"/>
<property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
<!-- 配置监控统计拦截的filters,去掉后监控界面sql无法统计 -->
<property name="filters" value="stat"/>
</bean>
- 可以将核心文件提取成单独的配置文件
# jdbc.properties 文件
jdbc.driver=com.mysql.jdbc.Driver•jdbc.url=jdbc:mysql:///test?useUnicode=true&characterEncoding=UTF8&autoReconnect=true&autoReconnectForPools=true
jdbc.username=root
jdbc.password=root
jdbc.initialSize=2
jdbc.minIdle=2
jdbc.maxActive=50
...
然后在Spring配置文件中获取
<!-- 配置用于加载属性文件的Bean,Spring一个容器仅允许定义一个PropertyPlaceholderConfigurer实例 -->
<context:property-placeholder location=“classpath:jdbc.properties,classpath:xxx.properties" ignore-unresolvable="true"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${jdbc.dirver}" />
<property name="url" value="${jdbc.url}" /><property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="initialSize" value="${jdbc.initialSize}" />
<property name="minIdle" value="${jdbc.minIdle}" />
<property name="maxActive" value="${jdbc.maxActive}" />
</bean>
二、Spring内置JDBC模板
引入Spring-jdbc的包
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
Spring引入两种JDBC操作模板
- JdbcTemplate:利用JDBC和简单的索引参数查询提供对数据库的简单访问
- NamedParameterJdbcTemplate:能够在查询时把值绑定在SQL里的命名参数,通过getJdbcOperations()可以获取jdbcTemplate实例
模板使用
- 在Spring的配置文件中配置JDBC模板类
- 把JDBC模板类装配到DAO中,用它来访问数据库
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--NamedParameterJdbcTemplate 没有无参构造需要构造器方式注入-->
<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource"/>
</bean>
<!--也可在Java中通过注解注入-->
<bean id="userDao" class="com.liqk.dao.impl.UserDaoJDBCImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
JDBCTemple常用方法
前置说明:
- 下面的例子都是以Student类为例,其实例类为:
public class Student {
private int id;
private String name;
private boolean gender;
private double score;
private Date registTime;
//此处省略 Get/Set 等常用方法
}
- JdbcTemplate的定义为
@Autowired
private JdbcTemplate jt;
修改方法
public int update(String sql,Object...args);//更新(插入/修改/删除)
jt.update("INSERT INTO student(name,score)VALUES(?,?)",stu.getName(),stu.getScore());
public int update(final PreparedStatementCreator psc, final KeyHolder generatedKeyHolder)//更新,适用于insert操作可获取自增的key
public Student addReturnKey(Student stu){
//需要使用KeyHolder 来接收自增id
KeyHolder kh = newGeneratedKeyHolder();
jt.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
PreparedStatement pstat=con .prepareStatement("INSERT INTOstudent(name,score,gender,registTime) VALUES(?,?,?,?)",Statement.RETURN_GENERATED_KEYS);//此处需要声明以Statement.RETURN_GENERATED_KEYS 方式来获取SQL执行器
pstat.setString(1,stu.getName());
pstat.setDouble(2,stu.getScore());
pstat.setBoolean(3,stu.isGender());
if(null!=stu.getRegistTime()){
pstat.setTimestamp(4,newTimestamp(stu.getRegistTime().getTime()));
}else{
pstat.setTimestamp(4,null);
}
return pstat;
}
},kh);
//新增方法执行完成之后
int id=kh.getKey().intValue();
stu.setId(id);
return stu;
}
public int[] batchUpdate(String sql,BatchPreparedStatementSetter p)//批量更新
//批量删除
public void deleteByIds(int... ids){
jt.batchUpdate("DELETE FROM student WHERE id = ?", new BatchPreparedStatementSetter() {
/*
进行占位符的赋值操作
ps 执行器
i 当前执行到第几次 index 从0开始
*/
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
ps.setInt(1,ids[i]);
}
//当前批评执行的语句数量执行的次数
@Override
public int getBatchSize() {
return ids.length;
}
});
}
查询方法
public Listquery(String sql, RowMapper rm, Object... args) //查询列表
- RowMapper把结果集中的行映射成对象的一个接口,常用子类BeanPropertyRowMapper
public List<Student> findAll() {
List<Student> stulist=null;//查询多行多列使用query
stulist=jt.query("SELECT * FROM student",newBeanPropertyRowMapper<>(Student.class));
return stulist;
}
- public TqueryForObject(String sql, RowMapper rm, Object… args) //查询单个对象/值。注意,如果这个方法查询不到数据时会抛出异常。不建议在查询对象时使用
//查询单个对象如果id不存在则会抛出异常
public Student findOne(int id){
Student stu=null;
stu=jt.queryForObject("SELECT id,name,gender,score,registTime FROMstudent WHERE id = ?",new BeanPropertyRowMapper<>(Student.class),id);
List<Student> stulist=jt.query("SELECT id,name,gender,score,registTime FROM studentWHERE id = ?",new BeanPropertyRowMapper<>(Student.class), id);
if(null!=stulist&&stulist.size()==1){
stu=stulist.get(0);
}
return stu;
}
建议以上改为query查询
//查询单个对象如果id不存在也无异常产生
public Student findOne(intid){
Student stu=null;
List<Student>stulist=jt.query("SELECT id,name,gender,score,registTime FROM studentWHERE id = ?", new BeanPropertyRowMapper<>(Student.class), id);
if(null!=stulist&&stulist.size()==1){
stu=stulist.get(0);
}
return stu;
}
- public Object queryForObject(String sql, Class requiredType, Object… args) //查询单个数据,如统计查询
public int count(){
//获取总记录数
int count = jt.queryForObject("SELECT count(id) FROM student",Integer.class);
return count;
}
执行储存语句或者DDL语句
public void execute(String sql); //执行DDL语句
返回字段名和类属性名不一致
当出现这样的情况时,可以自定义RowWrapper来进行映射
//根据返回类名自定义映射字段
private RowMapper rm=new RowMapper<Student>(){
@Override
public Student mapRow(ResultSetrs, introwNum) throwsSQLException {
Student stu=new Student();
stu.setId(rs.getInt("i"));
stu.setName(rs.getString("n"));
stu.setGender(rs.getBoolean("g"));
stu.setScore(rs.getDouble("s"));
stu.setRegistTime(rs.getTimestamp("r") ==null?null : newDate(rs.getTimestamp("r").getTime()));
return stu;
}
};
public Student findTest(int id) {
Student stu=null;
// 返回列名和类属性名不一致
String sql="SELECT id i,name n,gender g,score s,registTime r FROM studentWHERE id = ?";
List<Student> stulist=jt.query(sql,rm,id);
if(null!=stulist&&stulist.size() ==1){
stu=stulist.get(0);
}
return stu;
}
NamedParameterJdbcTemplate的使用区别
NamedParameterJdbcTemplate主要在JdbcTemplate的基础上增加了命名映射功能,使用方式将SQL占位符 ? 变为 :属性名的方式来声明
前置说明:
在代码中提前注入NamedParameterJdbcTemplate对象
@Autowired
private NamedParameterJdbcTemplate npt;
字段名匹配
新增示例:
//使用ParameterSource 的子类 BeanPropertySqlParameterSource来自动进行字段关联映射
public void add(Studentstu) {
npt.update("INSERT INTO student(name,gender,score,registTime)VALUES(:name,:gender,:score,:registTime)",new BeanPropertySqlParameterSource(stu));
}
字段名不匹配或者无对象映射关系
修改示例:
public void update(Student stu) {
/*
由于 :名称和类的属性名不一致需要自动以映射方式 eg. :s 在Student中没有相应属性
需要使用MapSqlParameterSource 来自己维护映射关系
MapSqlParameterSource 底层就是一个 Map<String,Object> 集合
所以也可以直接创建对应集合
*/
MapSqlParameterSource ms=new MapSqlParameterSource();
ms.addValue("name",stu.getName())
.addValue("gender",stu.isGender())
.addValue("s",stu.getScore())
.addValue("r",stu.getRegistTime())
.addValue("id",stu.getId());
npt.update("UPDATE student SET name = :name , gender = :gender , score = :s, registTIme = :r WHERE id =:id",ms);
}
三、Spring事务管理
Spring对声明式事务的管理:在企业分层架构中,实物的管理一般添加在业务层。
Spring不直接管理事务,它通过事务管理器将事务管理的责任委托给相应的持久性机制的某个特定平台事务实现。
事务管理实现 | 使用时机 |
---|---|
org.springframework.jdbc.datasource.DataSourceTransactionManager | 在单一的JDBCDataSource中管理事务 |
org.springframework.orm.hibernate5.HibernateTransactionManager | 当持久化机制是Hibernate时,用它来管理事务 |
org.springframework.orm.jpa.JpaTransactionManager | 当JPA用作持久化时,用它来管理事务 |
org.springframework.tranaction.jta.JtaTransactionManager | 使用一个JTA实现来管理事务。在一个事务跨越多个资源时必须使用(分布式事务) |
… | … |
基于XML配置的声明式事务管理
- 配置Spring提供的对应持久化技术的事务管理器(封装了底层数据库事务的操作)。 —>AOP中切面类
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
- 针对要进行事务控制的方法配置事务参数5个(配置通知)
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" isolation="READ_COMMITTED"/>
<tx:method name="get*" isolation="READ_COMMITTED" read-only="true"/>
<tx:method name="load*" isolation="READ_COMMITTED" read-only="true"/>
<tx:method name="find*" isolation="READ_COMMITTED" read-only="true"/>
<tx:method name="total*" isolation="READ_COMMITTED" read-only="true"/>
</tx:attributes>
</tx:advice>
- AOP的配置
<aop:config>
<!-- 通知者:把事务参数应用到切入点 -->
<aop:advisor advice-ref="txAdvice" pointcut="execution(*com.qiujy.service..*.*(..))"/>
</aop:config>
事务参数
-
传播行为(propagation):指定一个方法是否应该在事务中运行,在事务中怎样运行。Spring提供EJB CMT中熟悉的事务传播选项。
- REQUIRED
必须在一个事务中运行。spring中的默认值 - SUPPORTS
支持事务。 - MANDATORY
强制事务。如果有一个事务正在进行,该方法将会在那个事务中运行。如果没有一个活动的事务,则抛出异常 - REQUIRES_NEW
必须是一个新的事务。 - NOT_SUPPORTED
不支持事务。如果有一个事务正在运行,它将在这个方法的运行期间被挂起。 - NEVER
从来不需要事务。如果有一个事务正在运行,则抛出异常 - NESTED
嵌套事务。如果当前有一事务正在运行,则该方法运行在一个嵌套式事务中。如果没有事务,开启一个新的事务。
- REQUIRED
-
隔离级别(isolation):当前事务和其它事务的隔离程度。
- DEFAULT
使用数据库默认的事务隔离级别。
(Oracle、SQL Server默认的是READ_COMMITTED,MySQL默认的REPEATABLE_READ) - READ_UNCOMMITTED
读未提交。会产生脏读,不可重复读和幻读的问题。 - READ_COMMITTED
读已提交。解决了脏读,但还会出现不可重复读和幻读的问题。从隔离程序和并发策略考虑,一般都建议使用这个级别。 - REPEATABLE_READ
可重复读。解决了脏读,不可重复读,但还会出现幻读的问题。 - SERIALIZABLE
串行性。解决了所有了并发问题。但效率太低。
- DEFAULT
-
只读状态(read-only):只读事务只对后端数据库执行读操作,这样数据库可以采用一些优化措施。默认值为false
只读事务在某些情况下(如使用Hibernate时)是一种非常有用的优化。 -
事务超时(timeout):事务在超时前能运行多久(自动被底层的事务基础设施回滚)。以秒为单位。默认为-1,代表永不超时。
-
回滚规则(rollback-for,no-rollback-for):哪些异常引起回滚,哪些不引起。
默认情况下,只在出现RuntimeException时事务回滚,而受检异常不回滚。
基于注解的事务配置
- 在Spring的配置文件中配置事务管理器。
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
- 启用Spring对事务注解的支持:
<tx:annotation-driven transaction-manager="transactionManager"/>
- 使用@Transactional对相应的类或方法添加事务配置:事务参数的配置参看文档
属性 | 类型 | 描述 |
---|---|---|
传播性 | 枚举型:Propagation | 可选的传播性设置 |
隔离性 | 枚举型:Isolation | 可选的隔离性级别(默认值:ISOLATION_DEFAULT) |
只读性 | 布尔型 | 读写型事务vs只读型事务 |
超时 | int型(以秒为单位) | 事务超时 |
回滚异常类(rollbackFor) | 一组 Class 类的实例,必须是Throwable 的子类 | 一组异常类,遇到时 必须 进行回滚。默认情况下checked exceptions不进行回滚,仅unchecked exceptions(即RuntimeException的子类)才进行事务回滚。 |
回滚异常类名(rollbackForClassname) | 一组 Class 类的名字,必须是Throwable的子类 | 一组异常类名,遇到时 必须 进行回滚 |
不回滚异常类(noRollbackFor) | 一组 Class 类的实例,必须是Throwable 的子类 | 一组异常类,遇到时必须不回滚。 |
不回滚异常类名(noRollbackForClassname) | 一组 Class 类的名字,必须是Throwable 的子类 | 一组异常类,遇到时必须不回滚 |
常用方式:
@Transactional(isolation=Isolation.READ_COMMITTED)
@Transactional(isolation=Isolation.READ_COMMITTED,readOnly=true)
示例:
@Transactional(isolation=Isolation.READ_COMMITTED)
public class
UserServiceImpl implements UserService {
public void addUser(Useruser){ ...}
public void deleteUser(Useruser){ ...}
public void updateUser(Useruser){ ...}
@Transactional(isolation=Isolation.READ_COMMITTED,readOnly=true)
public User getUser(Serializable id){ ...}
}
四、Spring定时调度
Timer类的封装
使用ScheduledTimerTask来设置简单定时任务
Quartz框架的封装 (重要、常用)
- 添加Quartz的jar包:quartz-x.jar 它还依赖slf4j-api-1.x.x.jar
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.3</version>
<exclusions>
<exclusion>
<artifactId>c3p0</artifactId>
<groupId>c3p0</groupId>
</exclusion>
</exclusions>
</dependency>
除此之外,还要添加Spring对Quartz的支持包:spring-context-support-x.x.x.jar、spring-tx-x.x.x.jar
- 配置任务详细代码
<bean id="jobDetail" c class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="目标对象"/>
<property name="targetMethod" value="目标方法"/>
</bean>
- 定义触发规则
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail" ref="jobDetail"/>
<!-- cron时间表达式 -->
<property name="cronExpression" value="0/5 * * * * ?"/>
</bean>
- 安排任务
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTrigger"/>
</list>
</property>
</bean>
- 只需要保证Spring上下文实例存在,定时调度任务就会在指定的时间规则内得到触发并执行。
Quartz cron表达式
Quartz cron表达式支持七个域,域之间用空格分隔。其中“年”域可省略
- 格式:秒 分 时 日 月 周 年
- 域
名称 | 是否必须 | 允许值 | 特殊字符 |
---|---|---|---|
秒 | 是 | 0-59 | , - * / |
分 | 是 | 0-59 | , - * / |
时 | 是 | 0-23 | , - * / |
日 | 是 | 1-31 | , - * ? / L W C |
月 | 是 | 1-12 或 JAN-DEC | , - * / |
周 | 是 | 1-7 或 SUN-SAT (SUN 为 1) | , - * ? / L C # |
年 | 否 | 空或 1970-2099 | , - * / |
- 特殊字符
特殊字符 | 说明 | 示例 |
---|---|---|
* | 表示在这个域上可以包含所有合法的值 | 0 * 17 * *? |
? | 不为该域指定值只能用在日或周域上。你为这两域的其中一个指定了值,那就必须在另一个字值上放一个? | |
, | 给某个域上指定一个值列表 | 00,15,30,45* * * ? |
/ | 用于表示时间表的递增量 | 0/15 0/30* * * ?*/15 /30 * * ? |
- | 用于指定一个范围 | 0 45 3-8 ** * ? |
L | 指定为最后一个值只能用在日(本月最后一天)和周(本月的最后一个周几)域,例如周域为5L,则表示最后一个星期四触发 | 0 0 8 L * ? |
W | 代表平日(Mon-Fri、工作日)只能用于日域系统将在离指定日期的最近的有效工作日触发事件。例如:在日域使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 | |
# | 指定月份中的第几周的哪一天只能用在周域 | 0 15 10 ?* 6#3 第三周的周五 |
说明:LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。
- 示例:
- “0 0 12 * * ?”
每天中午12整点触发 - “0 15 10 ? * *”
每天上午10:15触发 - “0 15 10 * * ?”
每天上午10:15触发 - “0 15 10 * * ? *”
每天上午10:15触发 - “0 15 10 * * ? 2005”
2005年的每天上午10:15触发 - “0 * 14 * * ?”
在每天下午2点到下午2:59期间的每1分钟触发 - “0 0/5 14 * * ?”
在每天下午2点到下午2:55期间的每5分钟触发 - “0 0/5 14,18 * * ?”
在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 - “0 0-5 14 * * ?”
在每天下午2点到下午2:05期间的每1分钟触发 - “0 10,44 14 ? 3 WED”
每年三月的星期三的下午2:10和2:44触发 - “0 15 10 ? * MON-FRI”
周一至周五的上午10:15触发 - “0 15 10 15 * ?”
每月15日上午10:15触发 - “0 15 10 L * ?”
每月最后一日的上午10:15触发 - “0 15 10 ? * 6L”
每月的最后一个星期五上午10:15触发 - “0 15 10 ? * 6L 2002-2005”
2002年至2005年的每月的最后一个星期五上午10:15触发 - “0 15 10 ? * 6#3”
每月的第三个星期五上午10:15触发