springmvc、jpa(hibernate)、mybatis组合框架搭建
前言
为什么要搭建这么个组合呢?这个问题,其实我思考了很久。说实话,最干净的搭建,我个人还是倾向于搭建springmvc+mybatis的组合。但是呢,用mybatis去写各种细小的操作重复性质的sql,实体间关系,复杂对象的加载啥的,真的是很浪费时间。什么?代码生成器?我个人是抵制代码生成器这种东西的,于是,一点探索的经验都没有,严重抵制生成出来的一堆垃圾,没人改,因为省事,于是更倾向于生成啥的,生成后各种维护没人管啥的,我宁愿用hibernate。但是,因为之前经历过hibernate的自动维护数据库,不靠谱,这次只打算搭建让hibernate检查实体是否和数据库映射正确这件事。但是,hibernate的问题,也很大,其实主要出在复杂结果的查询。不管是否满足,业务对象我们习惯性延迟加载,这种对象即使满足复杂查询结果,我们也应该单独建立DO或者VO来应对复杂结果查询,因为两个业务变化独立性和延迟加载带来的查询效率低下。而且,hibernate写业务操作,真的挺快的。写的sql优化,后面再说吧,先让我用这个撑着。
搭建
创建项目
这事吧,就是个干净的maven项目,这里我甚至没有使用任何骨架,从头来吧。
-Dmaven.multiModuleProjectDirectory system propery is not set. Check $M2_HOME environment variable and mvn script match.
添加M2_HOME的环境变量
在运行maven的VM参数里添加-Dmaven.multiModuleProjectDirectory=$M2_HOME。idea的是在setting->maven->Runner中找到的。
springmvc搭建
版本
这里选用sprin4的最后一个版本4.3.10.RELEASE,其他组件也使用这个版本
依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${version.spring}</version>
</dependency>
文件结构
建立基础的文件结构,如上图所示:
- 一个controller
- spring的相关配置xml
- properties文件
- web.xml
先大概说一下这些都是干什么的。其实,最核心的,是这个web.xml。这个东西,是遵循啥规范来着,忘了,反正必须建在这么个目录下面,叫这个名字。应用服务器会解析里面的servlet配置,从而开始我们的web应用。我的里面的内容是酱紫的:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0"
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_3_0.xsd">
<display-name>sp</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
如上代码所见,我们引入了spring-context.xml,其实吧,可以直接引入sprimg-mvc.xml的,但是,因为我会有好多spring的配置xml,而且,我只想在这一个地方引入,就在这里引入所有的,所以我建立了一个spring-context.xml,用它来管理所有的spring的xml。
spring-context.xml如下:
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context"
xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-3.1.xsd ">
<context:component-scan base-package="nature.ntbrick.demo.smh" />
<context:property-placeholder location="classpath:applicatoin.properties"/>
<import resource="./spring-mvc.xml"/>
</beans>
可以看到,我在这里定位了spring扫描的基础包名,它包含了我所有的java文件,所以后面我所有的spring的xml都不用配置该项了。我们还引入了properties文件,和spring-mvc.xml
spring-mvc.xml文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
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/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<mvc:annotation-driven />
</beans>
其实吧,就是开启了注解驱动。酱紫我们就能用Controller呀,Service呀这些注解来配置项目了。然后我们来看看DemoController里是啥样的。
@Controller
@RequestMapping("/demo")
public class DemoController {
@ResponseBody
@RequestMapping("/ping")
public String ping(){
return "pong";
}
}
至此,我们已经搭建一起个基本的mvc框架,可以通过url访问controller中的内容。需要注意的是,我们这次搭建的是springmvc项目,所以,请在pom中将打包类型定义为war
添加mybatis
引入mybatis
添加mybatis,我们首先要做的是引入mybatis相关的依赖。在pom文件中添加如下依赖。mybatis我们这次用的版本是3.4.5的。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${version.spring}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${version.mybatis}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.7</version>
</dependency>
配置mybatis
首先,我们要加入mybatis的spring配置文件spring-mybatis.xml,然后再spring-context.xml里引入它,参照上次的spring-mvc.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:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<!-- 数据源 org.apache.commons.dbcp.BasicDataSource com.alibaba.druid.pool.DruidDataSource -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<!-- 初始化连接大小 -->
<property name="initialSize" value="0" />
<!-- 连接池最大使用连接数量 -->
<property name="maxActive" value="20" />
<!-- 连接池最小空闲 -->
<property name="minIdle" value="0" />
<!-- 获取连接最大等待时间 -->
<property name="maxWait" value="60000" />
<property name="validationQuery"><value>SELECT 1</value></property>
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="testWhileIdle" value="true" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="6000" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="25200000" />
<!-- 打开removeAbandoned功能 -->
<property name="removeAbandoned" value="true" />
<!-- 1800秒,也就是30分钟 -->
<property name="removeAbandonedTimeout" value="1800" />
<!-- 关闭abanded连接时输出错误日志 -->
<property name="logAbandoned" value="true" />
<!-- 监控数据库 -->
<!-- <property name="filters" value="stat" /> -->
<!--<property name="filters" value="mergeStat" />-->
</bean>
<!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!-- 自动扫描mapping.xml文件 -->
<property name="mapperLocations" value="classpath:mybatis/mapper/**/*.xml"></property>
</bean>
<!-- DAO接口所在包名,Spring会自动查找其下的类 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="nature.ntbrick.demo.smh.dao" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>
<!-- (事务管理)transaction manager, use JtaTransactionManager for global tx -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
可以看到的是,首先是对dataSource数据源的配置,这里我们用的是阿里的druid的数据源,原因就是网上盛传它最快,具体再测吧。这里有个很深的坑,坑了我很久,注意变量,尤其是${jdbc.username},这里我之前是${username},本来说贪省事,加多的配置再改,结果坑了很久,注意${username}这个变量拿的是当前系统的登录用户的用户名。所以,我都加了jdbc前缀,就都拿到我自己配的值了。
然后大家看下面的sqlSessionFactory的配置,里面mapperLocation的配置,指明了我扫描对应的mapper的配置文件的地方,所以我这里建立了resources/mybatis/mapper目录,里面放了demoMapper.xml文件。
后面org.mybatis.spring.mapper.MapperScannerConfigurer配置了我去哪里扫描dao接口文件,然后,用谁来解析dao接口
transactionManager是事务的配置,在这个demo里面没有用到。
为了支撑这段配置,我们需要建立对应的到接口,mapper.xml,maven依赖。我们建立了nature.demo.dao.DemoDao和demoMapper.xml
nature.ntbrick.demo.smh.dao.DemoDao如下:
package nature.ntbrick.demo.smh.dao;
import java.util.List;
import java.util.Map;
public interface DemoDao {
public List<Map<String,Object>> demoSelect();
}
demoMapper.xml文件如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="nature.ntbrick.demo.smh.dao.DemoDao">
<select id="demoSelect" resultType="java.util.HashMap">
select * from demo
</select>
</mapper>
nature.properties中加入的属性配置如下:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/test
jdbc.username=root
jdbc.password=123456
至此,我们已经可以访问数据库了。然后,我们在之前的DemoController中加入对应的方法来访问数据库,如下:
@Controller
@RequestMapping("/demo")
public class DemoController {
@Resource
private DemoDao demoDao;
@ResponseBody
@RequestMapping("/ping")
public String ping(){
return "pong";
}
@ResponseBody
@RequestMapping("/demoselect")
public List<Map<String,Object>> demoSelect(){
return demoDao.demoSelect();
}
}
如上面代码,我们加了demoSelect。如果这个时候,你访问http://localhost:8080/demo/demoselect ,会发现,数据查出来了,但是,无法返回,会报错。因为,无法将List<Map<String,Object>>转化成json。需要加入一段配置,让@ResponseBody可以正常的将结果实体转化成json。我们在spring-mvc.xml中加入如下配置:
<!--使用jsonconverter,避免IE执行AJAX时,返回JSON出现下载文件 -->
<bean id="mappingJackson2HttpMessageConverter"
class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<bean class="org.springframework.http.MediaType">
<constructor-arg index="0" value="text"/>
<constructor-arg index="1" value="plain"/>
<constructor-arg index="2" value="UTF-8"/>
</bean>
</list>
</property>
</bean>
<!--接口返回中文乱码问题-->
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>application/json;charset=UTF-8</value>
<value>text/html;charset=UTF-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
网上查到的很多资料用的配置Converter的方法,用到的类都是过期的,最后找到这个,有效,并且未过期,具体未研究,再说吧,后面对返回值格式控制的时候再说吧。
需要加入的依赖如下:
<!-- 使用Json所依赖的jar包 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.5.4</version>
</dependency>
至此,我们已经成功的添加了mybatis相关的操作,并可以将数据添加到http请求的返回值的body中传回了。
引入hibernate
添加依赖
我们需要将对应的依赖引入pom文件。
<!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-jpa -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.11.6.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-entitymanager -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>3.6.10.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate.javax.persistence/hibernate-jpa-2.0-api -->
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.0-api</artifactId>
<version>1.0.1.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-core -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>3.6.10.Final</version>
</dependency>
解释下,首先jpa,是由spring-data-jpa支持的,根据自己的spring版本选择对应的版本引入。然后可以在仓库的信息中看到hibernate-entitymanager和hibernate-jpa-2.0-api的依赖,其余的都是spring的依赖,引入后发现还依赖于hibernate-core,选择对应版本引入后,先这样。这是资料可以查到的最小依赖集合,后面缺,再补。
配置
加入spring-jpa.xml,然后把这个文件引入spring-context.xml中。spring-jpa.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:tx="http://www.springframework.org/schema/tx"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd"
default-lazy-init="true">
<!-- JPA实体管理器工厂 -->
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter" ref="hibernateJpaVendorAdapter" />
<!-- 加入定制化包路径 -->
<property name="packagesToScan" value="nature.ntbrick.demo.smh" />
<property name="jpaProperties">
<props>
<prop key="hibernate.current_session_context_class">thread</prop>
<prop key="hibernate.hbm2ddl.auto">validate</prop><!-- validate/update/create -->
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.format_sql">true</prop>
<!-- 建表的命名规则 -->
<prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
</props>
</property>
</bean>
<!-- 设置JPA实现厂商的特定属性 -->
<bean id="hibernateJpaVendorAdapter"
class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="databasePlatform" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
</bean>
<!-- Jpa 事务配置 -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<!-- Spring Data Jpa配置 -->
<jpa:repositories base-package="nature.ntbrick.demo.smh" transaction-manager-ref="transactionManager" entity-manager-factory-ref="entityManagerFactory"/>
<!-- 使用annotation定义事务 -->
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" />
</beans>
说实话,这里的配置我也是从网上抄来的,然后删改了一部分。首先,datasource部分,我们重用了mybatis的配置,这里就没有重新来了,今天我们先把jpa引进来,后续还要观察mybatis和hibernate在同一事务下混合使用的表现。这里有基础的事务配置,我还没验证。后续我还会重新整理配置的结构,把datasource和transaction进行集中,否则很多东西扯不清楚,今天就先这样。
需要注意的是hibernate的hibernate.hbm2ddl.auto,我这里配的是validate,不太信得过它的自动更新,之后会后续跟进对应的创建数据库的方法,至于维护,就团队自己想办法吧。
注意配置包的范围,在这里坑了一小会儿。
编码
我们这里引入了JpaDemoDao,TestJpa,DemoJpaService三个类来完成本次的样例,很简单,我们就写一个实体,然后在service里用dao插进去。
实体类TestJpa
package nature.ntbrick.demo.smh.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;
/**
* 测试实体类
*
* @author nature
* @create 2018-03-04 23:08
*/
@Entity
@Table(name="test_jpa")
public class TestJpa implements Serializable {
private static final long serialVersionUID = -3421148517857545609L;
@Id
@Column(name="id")
private Long id;
@Column(name="name")
private String name;
@Column(name="createtime")
private Date createtime;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getCreatetime() {
return createtime;
}
public void setCreatetime(Date createtime) {
this.createtime = createtime;
}
}
JpaDemoDao
package nature.ntbrick.demo.smh.dao.jpademo;
import nature.ntbrick.demo.smh.entity.TestJpa;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* @author nature
* @create 2018-03-04 23:09
*/
public interface JpaDemoDao extends JpaRepository<TestJpa,Long> {
}
DemoJpaService
package nature.ntbrick.demo.smh.service;
import nature.ntbrick.demo.smh.dao.jpademo.JpaDemoDao;
import nature.ntbrick.demo.smh.entity.TestJpa;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @author nature
* @create 2018-03-04 23:10
*/
@Service
public class DemoJpaService {
@Resource
private JpaDemoDao jpaDemoDao;
public void addRecord(TestJpa record){
jpaDemoDao.save(record);
}
}
至此,代码的编码就完结了。
单元测试
上面我们进行了编码,但是,如果每次写代码都需要写出对应的controller,然后再模拟http请求,才能够进行测试,那么,我们编码的效率将会是低下的。因为想证明我们是否正确,我们几乎需要等待一个模块编码完成。单元测试,可以帮我们解决这个问题。
添加依赖
我们使用junit进行单元测试。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${version.spring}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
编写测试
我们建立一个和service同名的包,但是是建在test文件夹下,然后建立测试类,代码如下。
package nature.ntbrick.demo.smh.service;
import nature.ntbrick.demo.smh.entity.TestJpa;
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 java.util.Date;
/**
* @author nature
* @create 2018-03-04 23:16
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value="classpath:spring/spring-context.xml")
public class DemoJpaServiceTest {
@Autowired
private DemoJpaService demoJpaService;
@Test
public void testAddRecord(){
TestJpa testJpa=new TestJpa();
testJpa.setId(1L);
testJpa.setName("test2");
testJpa.setCreatetime(new Date());
demoJpaService.addRecord(testJpa);
}
}
然后,你运行这个单元测试,他就会报错啦,原因呢,就是找不到servlet类,因为我们用tomcat启动的时候,它有,但是,我们自己做单元测试的时候没有,引入依赖:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
好啦,打完收工。兄弟些,你成功了。
附录
- demo的源码在https://gitee.com/naturetrible/ntbrick.demo