前言:
开发环境:IDEA+jdk1.8+windows10
目标:使用springboot整合druid数据源+mysql+mybatis+通用mapper插件+pagehelper插件+mybatis-generator+freemarker+layui
使用springboot开发web项目,尤其是web后台管理类项目,推荐使用上面的组合;
原因:
首先,druid数据源提供了方便的sql监控视图,而且性能也很好;
mysql开源且小巧强大,使用方便;
mybatis可以手动定制sql,十分方便;通用mapper插件可以让你免去基础的单表增删改查等基础代码的编写工作;pagehelper使用拦截器智能分页;mybatis-generator配置后可以全面自动生成持久层大部分代码;
freemarker模板效率较好,宏的使用十分方便;
layui更是当前java后端程序员的福音;
下面开始跟我一起整合,源代码在最后:
项目整合:
1、项目依赖
完整的pom.xml文件如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.zjt.learn</groupId> <artifactId>springboot-mybatis</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springboot-mybatis</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!--使用freemarker的starter,里面包含spring-boot-starter-web组件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <!--springboot测试组件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <!--springboot热部署组件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> <!-- 这个需要为 true 热部署才有效 --> </dependency> <!--mybatis依赖--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency> <!--通用mapper--> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>1.1.5</version> </dependency> <!--pagehelper 分页插件--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.3</version> </dependency> <!--mysql连接驱动包--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.44</version> </dependency> <!--druid连接池依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.6</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.31</version> </dependency> <!--aspectj组件--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.0</version> </dependency> <!--部署外部tomcat容器的时候再开启--> <!--<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </dependency>--> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <!--fork : 如果没有该项配置,这个devtools不会起作用,即应用不会restart --> <fork>true</fork> <!--支持静态文件热部署--> <addResources>true</addResources> </configuration> </plugin> <!--要打包了这个生成代码要禁止掉,本地开发开启--> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.5</version> <dependencies> <!--配置这个依赖主要是为了等下在配置mybatis-generator.xml的时候可以不用配置classPathEntry这样的一个属性,避免代码的耦合度太高--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.44</version> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper</artifactId> <version>3.4.0</version> </dependency> </dependencies> <executions> <execution> <id>Generate MyBatis Artifacts</id> <phase>package</phase> <goals> <goal>generate</goal> </goals> </execution> </executions> <configuration> <!--允许移动生成的文件 --> <verbose>true</verbose> <!-- 是否覆盖 --> <overwrite>true</overwrite> <!-- 自动生成的配置 --> <configurationFile>src/main/resources/mybatis-generator.xml</configurationFile> </configuration> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> </project>
解释:
spring-boot-starter-freemarker 依赖会包含springboot web组件,所以web相关其他的组件就不用引入了;
mybatis-spring-boot-starter引入mybatis相关依赖;
mapper-spring-boot-starter是mybatis的通用mapper;该插件可以极其方便的使用Mybatis单表的增删改查;(通用mapper原作者文档)
pagehelper-spring-boot-starter是pagehelper 分页插件,使用拦截器的原理;
druid-spring-boot-starter是druid连接池组件依赖;(druid连接池中文文档)
此外,在plugin中添加了mybatis-generator插件,配合mybatis-generator.xml来自动生成实体、mapper、xml;
2、项目包结构
3、配置数据持久层
3.1数据库表:
我们本文仅使用T_CLASS表,因为仅仅演示如何整合,故业务字段仅有1个name;
3.2在application-dev.properties配置文件中,配置数据源和连接池、mybatis和分页插件的配置:
## 数据库访问配置 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/springboot-mybatis?useUnicode=true&characterEncoding=utf-8 spring.datasource.username=root spring.datasource.password=root # 下面为连接池的补充设置,应用到上面所有数据源中 # 初始化大小,最小,最大 spring.datasource.initialSize=5 spring.datasource.minIdle=5 spring.datasource.maxActive=20 # 配置获取连接等待超时的时间 spring.datasource.maxWait=60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 spring.datasource.timeBetweenEvictionRunsMillis=60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 spring.datasource.minEvictableIdleTimeMillis=300000 spring.datasource.validationQuery=SELECT 1 FROM DUAL spring.datasource.testWhileIdle=true spring.datasource.testOnBorrow=false spring.datasource.testOnReturn=false # 打开PSCache,并且指定每个连接上PSCache的大小 spring.datasource.poolPreparedStatements=true spring.datasource.maxPoolPreparedStatementPerConnectionSize=20 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 spring.datasource.filters=stat,wall,log4j # 合并多个DruidDataSource的监控数据 #spring.datasource.useGlobalDataSourceStat=true #mybatis mybatis.type-aliases-package=com.zjt.entity mybatis.mapper-locations=classpath:mapper/*.xml #mappers 多个接口时逗号隔开 #mapper.mappers=tk.mybatis.mapper.common.Mapper mapper.mappers=com.zjt.util.MyMapper mapper.not-empty=false mapper.identity=MYSQL #pagehelper pagehelper.helperDialect=mysql pagehelper.reasonable=true pagehelper.supportMethodsArguments=true pagehelper.params=count=countSql
3.3在DruidConfig.java中使用druid数据源配置参数生产数据源,并配置druid监控相关配置:
package com.zjt.config; import com.alibaba.druid.filter.Filter; import com.alibaba.druid.filter.stat.StatFilter; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import com.alibaba.druid.wall.WallConfig; import com.alibaba.druid.wall.WallFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import javax.sql.DataSource; import java.util.ArrayList; import java.util.List; /** * Druid配置 * * @author zhaojiatao */ @Configuration public class DruidConfig { private Logger logger = LoggerFactory.getLogger(DruidConfig.class); @Value("${spring.datasource.url:#{null}}") private String dbUrl; @Value("${spring.datasource.username: #{null}}") private String username; @Value("${spring.datasource.password:#{null}}") private String password; @Value("${spring.datasource.driverClassName:#{null}}") private String driverClassName; @Value("${spring.datasource.initialSize:#{null}}") private Integer initialSize; @Value("${spring.datasource.minIdle:#{null}}") private Integer minIdle; @Value("${spring.datasource.maxActive:#{null}}") private Integer maxActive; @Value("${spring.datasource.maxWait:#{null}}") private Integer maxWait; @Value("${spring.datasource.timeBetweenEvictionRunsMillis:#{null}}") private Integer timeBetweenEvictionRunsMillis; @Value("${spring.datasource.minEvictableIdleTimeMillis:#{null}}") private Integer minEvictableIdleTimeMillis; @Value("${spring.datasource.validationQuery:#{null}}") private String validationQuery; @Value("${spring.datasource.testWhileIdle:#{null}}") private Boolean testWhileIdle; @Value("${spring.datasource.testOnBorrow:#{null}}") private Boolean testOnBorrow; @Value("${spring.datasource.testOnReturn:#{null}}") private Boolean testOnReturn; @Value("${spring.datasource.poolPreparedStatements:#{null}}") private Boolean poolPreparedStatements; @Value("${spring.datasource.maxPoolPreparedStatementPerConnectionSize:#{null}}") private Integer maxPoolPreparedStatementPerConnectionSize; @Value("${spring.datasource.filters:#{null}}") private String filters; @Value("{spring.datasource.connectionProperties:#{null}}") private String connectionProperties; @Bean @Primary public DataSource dataSource(){ DruidDataSource datasource = new DruidDataSource(); datasource.setUrl(this.dbUrl); datasource.setUsername(username); datasource.setPassword(password); datasource.setDriverClassName(driverClassName); //configuration if(initialSize != null) { datasource.setInitialSize(initialSize); } if(minIdle != null) { datasource.setMinIdle(minIdle); } if(maxActive != null) { datasource.setMaxActive(maxActive); } if(maxWait != null) { datasource.setMaxWait(maxWait); } if(timeBetweenEvictionRunsMillis != null) { datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); } if(minEvictableIdleTimeMillis != null) { datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); } if(validationQuery!=null) { datasource.setValidationQuery(validationQuery); } if(testWhileIdle != null) { datasource.setTestWhileIdle(testWhileIdle); } if(testOnBorrow != null) { datasource.setTestOnBorrow(testOnBorrow); } if(testOnReturn != null) { datasource.setTestOnReturn(testOnReturn); } if(poolPreparedStatements != null) { datasource.setPoolPreparedStatements(poolPreparedStatements); } if(maxPoolPreparedStatementPerConnectionSize != null) { datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize); } if(connectionProperties != null) { datasource.setConnectionProperties(connectionProperties); } List<Filter> filters = new ArrayList<>(); filters.add(statFilter()); filters.add(wallFilter()); datasource.setProxyFilters(filters); return datasource; } @Bean public ServletRegistrationBean druidServlet() { ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*"); //控制台管理用户,加入下面2行 进入druid后台就需要登录 //servletRegistrationBean.addInitParameter("loginUsername", "admin"); //servletRegistrationBean.addInitParameter("loginPassword", "admin"); return servletRegistrationBean; } @Bean public FilterRegistrationBean filterRegistrationBean() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(new WebStatFilter()); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); filterRegistrationBean.addInitParameter("profileEnable", "true"); return filterRegistrationBean; } @Bean public StatFilter statFilter(){ StatFilter statFilter = new StatFilter(); statFilter.setLogSlowSql(true); //slowSqlMillis用来配置SQL慢的标准,执行时间超过slowSqlMillis的就是慢。 statFilter.setMergeSql(true); //SQL合并配置 statFilter.setSlowSqlMillis(1000);//slowSqlMillis的缺省值为3000,也就是3秒。 return statFilter; } @Bean public WallFilter wallFilter(){ WallFilter wallFilter = new WallFilter(); //允许执行多条SQL WallConfig config = new WallConfig(); config.setMultiStatementAllow(true); wallFilter.setConfig(config); return wallFilter; } }
3.4在MybatisDatasourceConfig.java中使用3.3中生成的数据源生成sqlSessionFactory,并指定要扫描的mapper包,生成该数据源的事务管理器:
package com.zjt.config; import com.zjt.util.MyMapper; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import javax.sql.DataSource; /** * @author <a href="zhaojiatao"></a> * @version 1.0, 2017/11/24 * @description */ @Configuration // 精确到 mapper 目录,以便跟其他数据源隔离 @MapperScan(basePackages = "com.zjt.mapper", markerInterface = MyMapper.class, sqlSessionFactoryRef = "sqlSessionFactory") public class MybatisDatasourceConfig { @Autowired @Qualifier("dataSource") private DataSource ds; @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(ds); //指定mapper xml目录 ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); factoryBean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml")); return factoryBean.getObject(); } @Bean public SqlSessionTemplate sqlSessionTemplate() throws Exception { SqlSessionTemplate template = new SqlSessionTemplate(sqlSessionFactory()); // 使用上面配置的Factory return template; } //关于事务管理器,不管是JPA还是JDBC等都实现自接口 PlatformTransactionManager // 如果你添加的是 spring-boot-starter-jdbc 依赖,框架会默认注入 DataSourceTransactionManager 实例。 //在Spring容器中,我们手工注解@Bean 将被优先加载,框架不会重新实例化其他的 PlatformTransactionManager 实现类。 @Bean(name = "transactionManager") @Primary public DataSourceTransactionManager masterTransactionManager() { //MyBatis自动参与到spring事务管理中,无需额外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的数据源 // 与DataSourceTransactionManager引用的数据源一致即可,否则事务管理会不起作用。 return new DataSourceTransactionManager(ds); } }
3.5使用3.4生成的事务管理器通过AOP技术为指定包下的service方法提供事务支持,并根据不同的方法提供不同的事务隔离级别:
package com.zjt.interceptor; import org.aspectj.lang.annotation.Aspect; import org.springframework.aop.Advisor; import org.springframework.aop.aspectj.AspectJExpressionPointcut; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.interceptor.*; import java.util.Collections; import java.util.HashMap; import java.util.Map; /** * @author <a href="zhaojiatao"></a> * @version 1.0, 2017/11/29 * @description * 注解声明式事务 */ @Aspect @Configuration public class TxAdviceInterceptor { private static final int TX_METHOD_TIMEOUT = 50000;//单位秒 private static final String AOP_POINTCUT_EXPRESSION = "execution (* com.zjt.service.*.*(..))"; @Autowired @Qualifier("transactionManager") private PlatformTransactionManager transactionManager; @Bean public TransactionInterceptor txAdvice() { NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource(); /*只读事务,不做更新操作*/ RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute(); readOnlyTx.setReadOnly(true); readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED ); /*当前存在事务就使用当前事务,当前不存在事务就创建一个新的事务*/ RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute(); requiredTx.setRollbackRules( Collections.singletonList(new RollbackRuleAttribute(Exception.class))); requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); requiredTx.setTimeout(TX_METHOD_TIMEOUT); Map<String, TransactionAttribute> txMap = new HashMap<>(); txMap.put("add*", requiredTx); txMap.put("save*", requiredTx); txMap.put("insert*", requiredTx); txMap.put("update*", requiredTx); txMap.put("delete*", requiredTx); txMap.put("get*", readOnlyTx); txMap.put("select*", readOnlyTx); txMap.put("query*", readOnlyTx); source.setNameMap( txMap ); TransactionInterceptor txAdvice = new TransactionInterceptor(transactionManager, source); return txAdvice; } @Bean public Advisor txAdviceAdvisor() { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression(AOP_POINTCUT_EXPRESSION); return new DefaultPointcutAdvisor(pointcut, txAdvice()); } }
3.6使用mybatis-generator自动生成entity+mapper+xml:
首先,在util包下添加:MyMapper接口:
package com.zjt.util; import tk.mybatis.mapper.common.Mapper; import tk.mybatis.mapper.common.MySqlMapper; /** * 继承自己的MyMapper * * @author * @since 2017-06-26 21:53 */ public interface MyMapper<T> extends Mapper<T>, MySqlMapper<T> { //FIXME 特别注意,该接口不能被扫描到,否则会出错 //FIXME 最后在启动类中通过MapperScan注解指定扫描的mapper路径: }
然后,在resources目录下添加mybatis-generator.xml:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!--加载配置文件,为下面读取数据库信息准备--> <properties resource="application.properties"/> <context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat"> <plugin type="tk.mybatis.mapper.generator.MapperPlugin"> <!--其中tk.mybatis.mapper.generator.MapperPlugin很重要,用来指定通用Mapper对应的文件,这样我们生成的mapper都会继承这个通用Mapper--> <property name="mappers" value="com.zjt.util.MyMapper" /> <!--caseSensitive默认false,当数据库表名区分大小写时,可以将该属性设置为true--> <property name="caseSensitive" value="false"/> </plugin> <!-- 阻止生成自动注释 --> <commentGenerator> <property name="javaFileEncoding" value="UTF-8"/> <property name="suppressDate" value="true"/> <property name="suppressAllComments" value="true"/> </commentGenerator> <!--数据库链接地址账号密码,这里由于我使用的是根据开发和生产分离的配置文件,所以这里直接写上了--> <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/springboot-mybatis?useUnicode=true&characterEncoding=utf-8" userId="root" password="root"> </jdbcConnection> <javaTypeResolver> <property name="forceBigDecimals" value="false"/> </javaTypeResolver> <!--生成Model类存放位置--> <javaModelGenerator targetPackage="com.zjt.entity" targetProject="src/main/java"> <property name="enableSubPackages" value="true"/> <property name="trimStrings" value="true"/> </javaModelGenerator> <!--生成映射文件存放位置--> <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"> <property name="enableSubPackages" value="true"/> </sqlMapGenerator> <!--生成Dao类存放位置--> <!-- 客户端代码,生成易于使用的针对Model对象和XML配置文件 的代码 type="ANNOTATEDMAPPER",生成Java Model 和基于注解的Mapper对象 type="XMLMAPPER",生成SQLMap XML文件和独立的Mapper接口 --> <javaClientGenerator type="XMLMAPPER" targetPackage="com.zjt.mapper" targetProject="src/main/java"> <property name="enableSubPackages" value="true"/> </javaClientGenerator> <!--生成对应表及类名 去掉Mybatis Generator生成的一堆 example --> <!--<table tableName="LEARN_RESOURCE" domainObjectName="LearnResource" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"> <generatedKey column="id" sqlStatement="Mysql" identity="true"/> </table>--> <!--<table tableName="T_STUDENT" domainObjectName="Student" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"> <generatedKey column="id" sqlStatement="Mysql" identity="true"/> </table> <table tableName="T_TEACHER" domainObjectName="Teacher" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"> <generatedKey column="id" sqlStatement="Mysql" identity="true"/> </table>--> <table tableName="T_CLASS" domainObjectName="TClass" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"> <generatedKey column="id" sqlStatement="Mysql" identity="true"/> </table> </context> </generatorConfiguration>
因为我们已经在pom的plugin中添加了插件,所以我们可以在IDEA中使用maven插件命令执行:
执行成功之后会生成:TClass.java+TClassMapper.java+TClassMapper.xml
3.7使用通用IService接口实现service层构建
由于spring4支持泛型注入,所以通用mapper作者提供了一种通用mapper在spring4中的最佳实践:
首先,编写通用接口:
package com.zjt.service; import org.springframework.stereotype.Service; import java.util.List; /** * 通用接口 */ @Service public interface IService<T> { T selectByKey(Object key); int save(T entity); int saveNotNull(T entity); int delete(Object key); int updateAll(T entity); int updateNotNull(T entity); List<T> selectByExample(Object example); //TODO 其他... }
然后,编写抽象类BaseService实现通用接口:
package com.zjt.service.impl; import com.zjt.service.IService; import org.springframework.beans.factory.annotation.Autowired; import tk.mybatis.mapper.common.Mapper; import java.util.List; /** * 通用Service * @param <T> */ public abstract class BaseService<T> implements IService<T> { @Autowired protected Mapper<T> mapper; public Mapper<T> getMapper() { return mapper; } @Override public T selectByKey(Object key) { //说明:根据主键字段进行查询,方法参数必须包含完整的主键属性,查询条件使用等号 return mapper.selectByPrimaryKey(key); } @Override public int save(T entity) { //说明:保存一个实体,null的属性也会保存,不会使用数据库默认值 return mapper.insert(entity); } @Override public int saveNotNull(T entity) { //说明:保存一个实体,属性不为null的值 return mapper.insertSelective(entity); } @Override public int delete(Object key) { //说明:根据主键字段进行删除,方法参数必须包含完整的主键属性 return mapper.deleteByPrimaryKey(key); } @Override public int updateAll(T entity) { //说明:根据主键更新实体全部字段,null值会被更新 return mapper.updateByPrimaryKey(entity); } @Override public int updateNotNull(T entity) { //根据主键更新属性不为null的值 return mapper.updateByPrimaryKeySelective(entity); } @Override public List<T> selectByExample(Object example) { //说明:根据Example条件进行查询 //重点:这个查询支持通过Example类指定查询列,通过selectProperties方法指定查询列 return mapper.selectByExample(example); } }
这样,其他的业务service实现类serviceImpl就可以继承这个抽象类BaseService来简化基础数据库操作的代码;
如,我们建立TClassService业务接口,使其继承通用接口并指定泛型类型IService<TClass>:
package com.zjt.service; import com.zjt.entity.TClass; import com.zjt.model.QueryTClassList; import com.zjt.util.Page; import java.util.List; import java.util.Map; public interface TClassService extends IService<TClass>{ public List<TClass> queryTClassList(Page<QueryTClassList> page); public Map<String,Object> saveOrUpdateTClass(TClass tclass); }
然后在建立该业务接口的实现类TClassServiceImpl,使其继承并指定泛型类型BaseService<TClass>,并实现TClassService:
package com.zjt.service.impl; import com.github.pagehelper.PageHelper; import com.zjt.entity.TClass; import com.zjt.mapper.TClassMapper; import com.zjt.model.QueryTClassList; import com.zjt.service.TClassService; import com.zjt.util.Page; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import tk.mybatis.mapper.util.StringUtil; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; @Service("tclassServiceImpl") public class TClassServiceImpl extends BaseService<TClass> implements TClassService { @Autowired private TClassMapper tclassMapper; @Override public List<TClass> queryTClassList(Page<QueryTClassList> page) { PageHelper.startPage(page.getPage(), page.getRows()); // return tclassMapper.queryTClassList(page.getCondition()); return tclassMapper.selectAll(); } @Override public Map<String, Object> saveOrUpdateTClass(TClass tclass) { LinkedHashMap<String,Object> resultMap=new LinkedHashMap<String,Object>(); if(tclass!=null){ if(StringUtil.isNotEmpty(tclass.getId())){//编辑 if(StringUtil.isNotEmpty(tclass.getName())){ updateNotNull(tclass); resultMap.put("state","success"); resultMap.put("message","修改班级成功"); return resultMap; }else{ resultMap.put("state","fail"); resultMap.put("message","修改失败,缺少字段"); return resultMap; } }else{//新建 if(StringUtil.isNotEmpty(tclass.getName())){ tclass.setId(UUID.randomUUID().toString().replaceAll("-","")); saveNotNull(tclass); resultMap.put("state","success"); resultMap.put("message","新建班级成功"); return resultMap; }else{ resultMap.put("state","fail"); resultMap.put("message","新建失败,缺少字段"); return resultMap; } } }else{ resultMap.put("state","fail"); resultMap.put("message","失败"); return resultMap; } } }
最后,我们就可以在controller中调用该业务service的方法,并使用业务实现类去执行:
package com.zjt.web; import com.zjt.entity.TClass; import com.zjt.mapper.PageRusult; import com.zjt.model.QueryTClassList; import com.zjt.service.TClassService; import com.zjt.util.Page; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import tk.mybatis.mapper.util.StringUtil; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @Controller @RequestMapping("/tclass") public class TClassController { @Autowired @Qualifier("tclassServiceImpl") private TClassService tclassService; @ResponseBody @RequestMapping("/queryTClassList") public PageRusult selectByPages(Page<QueryTClassList> page){ List<TClass> tclassList=tclassService.queryTClassList(page); //PageInfo<TClass> pageInfo =new PageInfo<TClass>(tclassList); PageRusult<TClass> pageRusult =new PageRusult<TClass>(tclassList); pageRusult.setCode(0); return pageRusult; } @ResponseBody @RequestMapping("/saveOrUpdateTClass") public Map<String,Object> saveOrUpdateTClass(TClass tclass){ LinkedHashMap<String,Object> resultMap=new LinkedHashMap<String,Object>(); try { return tclassService.saveOrUpdateTClass(tclass); }catch (Exception e){ resultMap.put("state","fail"); resultMap.put("message","操作失败"); return resultMap; } } @ResponseBody @RequestMapping("/deleteTClass") public Map<String,Object> deleteTClass(String id){ LinkedHashMap<String,Object> resultMap=new LinkedHashMap<String,Object>(); try { if(StringUtil.isNotEmpty(id)){ tclassService.delete(id); resultMap.put("state","success"); resultMap.put("message","删除班级成功"); return resultMap; }else{ resultMap.put("state","fail"); resultMap.put("message","删除班级失败"); return resultMap; } }catch (Exception e){ resultMap.put("state","fail"); resultMap.put("message","操作异常,删除班级失败"); return resultMap; } } }
3.8 调整项目启动类SpringbootMybatisApplication.java中的配置:
开启@EnableTransactionManagement声明事务管理,并将spring boot自带的DataSourceAutoConfiguration禁掉:
package com.zjt; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.support.SpringBootServletInitializer; import org.springframework.transaction.annotation.EnableTransactionManagement; @EnableTransactionManagement // 启注解事务管理,等同于xml配置方式的 //首先要将spring boot自带的DataSourceAutoConfiguration禁掉,因为它会读取application.properties文件的spring.datasource.*属性并自动配置单数据源。在@SpringBootApplication注解中添加exclude属性即可: @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public class SpringbootMybatisApplication extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(SpringbootMybatisApplication.class); } public static void main(String[] args) { SpringApplication.run(SpringbootMybatisApplication.class, args); } }
至此,业务持久层和业务逻辑层全部完成;
4、配置web层
4.1配置MyWebMvcConfigurerAdapter.java
package com.zjt.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; /** * Created by tengj on 2017/3/13. */ @Configuration public class MyWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter { /** * 以前要访问一个页面需要先创建个Controller控制类,在写方法跳转到页面 * 在这里配置后就不需要那么麻烦了,直接访问http://localhost:8080/toLogin就跳转到login.html页面了 * * @param registry */ @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/toTest").setViewName("test"); super.addViewControllers(registry); } }
4.2 编写controller
package com.zjt.web; import com.zjt.entity.TClass; import com.zjt.mapper.PageRusult; import com.zjt.model.QueryTClassList; import com.zjt.service.TClassService; import com.zjt.util.Page; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import tk.mybatis.mapper.util.StringUtil; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @Controller @RequestMapping("/tclass") public class TClassController { @Autowired @Qualifier("tclassServiceImpl") private TClassService tclassService; @ResponseBody @RequestMapping("/queryTClassList") public PageRusult selectByPages(Page<QueryTClassList> page){ List<TClass> tclassList=tclassService.queryTClassList(page); //PageInfo<TClass> pageInfo =new PageInfo<TClass>(tclassList); PageRusult<TClass> pageRusult =new PageRusult<TClass>(tclassList); pageRusult.setCode(0); return pageRusult; } @ResponseBody @RequestMapping("/saveOrUpdateTClass") public Map<String,Object> saveOrUpdateTClass(TClass tclass){ LinkedHashMap<String,Object> resultMap=new LinkedHashMap<String,Object>(); try { return tclassService.saveOrUpdateTClass(tclass); }catch (Exception e){ resultMap.put("state","fail"); resultMap.put("message","操作失败"); return resultMap; } } @ResponseBody @RequestMapping("/deleteTClass") public Map<String,Object> deleteTClass(String id){ LinkedHashMap<String,Object> resultMap=new LinkedHashMap<String,Object>(); try { if(StringUtil.isNotEmpty(id)){ tclassService.delete(id); resultMap.put("state","success"); resultMap.put("message","删除班级成功"); return resultMap; }else{ resultMap.put("state","fail"); resultMap.put("message","删除班级失败"); return resultMap; } }catch (Exception e){ resultMap.put("state","fail"); resultMap.put("message","操作异常,删除班级失败"); return resultMap; } } }
4.3 在配置文件application-dev.properties中增加freemarker的配置和tomcat相关配置:
######################################################## ###FREEMARKER (FreeMarkerAutoConfiguration) ######################################################## spring.freemarker.allow-request-override=false #本机调试时,配置项template_update_delay=0,这样就关闭了模板缓存。注意线上环境要开启缓存 spring.freemarker.cache=false spring.freemarker.settings.template_update_delay=0 spring.freemarker.check-template-location=true spring.freemarker.charset=UTF-8 spring.freemarker.content-type=text/html spring.freemarker.expose-request-attributes=false spring.freemarker.expose-session-attributes=false spring.freemarker.expose-spring-macro-helpers=false spring.freemarker.prefix= #若在freemarker获取request对象,在spring boot 在application.properties可以这么配置 spring.freemarker.request-context-attribute=request #spring.freemarker.settings.*= spring.freemarker.suffix=.ftl #template-loader-path表示所有的模板文件都放在该目录下 spring.freemarker.template-loader-path=classpath:/templates/ #spring.freemarker.view-names= #whitelistofviewnamesthatcanberesolved #static-locations可以自定义静态资源路径,不过会覆盖springboot默认路径 #在这个最末尾的file:${web.upload-path}之所有要加file:是因为指定的是一个具体的硬盘路径,其他的使用classpath指的是系统环境变量 #spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,file:${web.upload-path} spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/ spring.freemarker.settings.auto_import=common/common.ftl as com spring.freemarker.settings.datetime_format=yyyy-MM-dd #兼容传统模式 spring.freemarker.settings.classic_compatible=true #表示访问该路径时代表请求静态资源,用户可以直接访问该请求路径中的静态资源 spring.mvc.static-path-pattern=/static/** server.port=8080 server.context-path=/test server.session.timeout=10000
4.4 编写测试页面:test.ftl
<@com.head title=""> <base id="base" href="${basePath!}"> <link href="${basePath!}/static/plugins/layui/css/layui.css" type="text/css" media="screen" rel="stylesheet"/> <script src="${basePath!}/static/plugins/layui/layui.js" type="text/javascript"></script> <script> //一般直接写在一个js文件中 layui.use(['layer', 'form','table'], function(){ var layer = layui.layer ,form = layui.form ,$ = layui.$ ,laytpl = layui.laytpl ,table = layui.table; var tableTClass =table.render({ id: 'idTClass' ,elem: '#TClass' ,height: 315 ,url: '${basePath!}/tclass/queryTClassList' //数据接口 ,page: true //开启分页 ,cols: [[ //表头 {type:'numbers', title: '序号', 80,sort: true} ,{field: 'id', title: 'ID', 300, unresize:true} ,{field: 'name', title: '班级名称', 200,unresize:true} ,{fixed: 'right', 150, align:'center', toolbar: '#barDemo'} //这里的toolbar值是模板元素的选择器 ]], method: 'post', request: { pageName: 'page' //页码的参数名称,默认:page ,limitName: 'rows' //每页数据量的参数名,默认:limit }, response: { statusName: 'code' ,statusCode: 0 ,countName: 'total' //数据总数的字段名称,默认:count ,dataName: 'list' //数据列表的字段名称,默认:data }, skin: 'line' //行边框风格 }); var TClassInsertLayerIndex; //新建 $("#TClassInsert").click(function(){ //置空表单 $("#TClassInsertForm").find(":input[name='name']").val(""); $("#TClassInsertForm").find(":input[name='id']").val(""); TClassInsertLayerIndex = layer.open({ title:"新建", type: 1, content: $('#TClassInsertDiv') }); }); form.on('submit(TClassInsertFormSubmit)', function(data){ $.ajax({ type: "POST", url: "${basePath!}/tclass/saveOrUpdateTClass", data: $("#TClassInsertForm").serialize(), async: false, error: function (request) { layer.alert("与服务器连接失败/(ㄒoㄒ)/~~"); return false; }, success: function (data) { if (data.state == 'fail') { layer.alert(data.message); layer.close(TClassInsertLayerIndex); return false; }else if(data.state == 'success'){ layer.alert(data.message); layer.close(TClassInsertLayerIndex); tableTClass.reload({ where: { //设定异步数据接口的额外参数,任意设 /*aaaaaa: 'xxx' ,bbb: 'yyy'*/ //… } ,page: { curr: 1 //重新从第 1 页开始 } }); } } }); return false; //阻止表单跳转。如果需要表单跳转,去掉这段即可。 }); //监听工具条 table.on('tool(TClass)', function(obj){ //注:tool是工具条事件名,TClass是table原始容器的属性 lay-filter="对应的值" var data = obj.data; //获得当前行数据 var layEvent = obj.event; //获得 lay-event 对应的值(也可以是表头的 event 参数对应的值) var tr = obj.tr; //获得当前行 tr 的DOM对象 if(layEvent === 'detail'){ //查看 //do somehing } else if(layEvent === 'del'){ //删除 layer.confirm('真的删除该行数据吗', function(index){ obj.del(); //删除对应行(tr)的DOM结构,并更新缓存 layer.close(index); //向服务端发送删除指令 $.ajax({ type: "POST", url: "${basePath!}/tclass/deleteTClass", data: {id:data.id}, async: false, error: function (request) { layer.alert("与服务器连接失败/(ㄒoㄒ)/~~"); return false; }, success: function (data) { if (data.state == 'fail') { layer.alert(data.message); return false; }else if(data.state == 'success'){ } } }); }); } else if(layEvent === 'edit'){ //编辑 //do something //置空表单 $("#TClassInsertForm").find(":input[name='name']").val(""); $("#TClassInsertForm").find(":input[name='id']").val(""); //添加值 $("#TClassInsertForm").find(":input[name='name']").val(data.name); $("#TClassInsertForm").find(":input[name='id']").val(data.id); TClassInsertLayerIndex = layer.open({ title:"编辑", type: 1, content: $('#TClassInsertDiv') }); } }); }); </script> </@com.head> <@com.body> <fieldset class="layui-elem-field"> <legend>班级信息</legend> <div class="layui-field-box"> <div class="layui-fluid"> <div class="layui-row"> <button class="layui-btn" id="TClassInsert">新建</button> </div> <div class="layui-row"> <table id="TClass" lay-filter="TClass"></table> </div> </div> </div> </fieldset> <div id="TClassInsertDiv" style="display: none"> <form class="layui-form" action="" id="TClassInsertForm"> <input type="hidden" id="TClassInsertFormId" name="id"/> <div class="layui-form-item"> <label class="layui-form-label">班级名称</label> <div class="layui-input-block"> <input type="text" name="name" required lay-verify="required" placeholder="请输入班级名称" autocomplete="off" class="layui-input"> </div> </div> <div class="layui-form-item"> <div class="layui-input-block"> <button class="layui-btn" lay-submit lay-filter="TClassInsertFormSubmit">立即提交</button> <button type="reset" class="layui-btn layui-btn-primary" id="TClassInsertFormReset">重置</button> </div> </div> </form> </div> <script type="text/html" id="barDemo"> <#--<a class="layui-btn layui-btn-xs" lay-event="detail">查看</a>--> <a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a> <a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a> </script> <!-- 序号监听事件 --> <script type="text/html" id="indexTpl"> {{d.LAY_TABLE_INDEX+1}} </script> </@com.body>
测试:
1.基本业务增删改查测试
启动项目成功后,输入地址:http://localhost:8080/test/toTest
得到如下页面,可以进行班级的增删改查,并进行分页:
这里我解释一下,是如何做到分页的,我们找到TClassServiceImpl.java,找到queryTClassList(Page<QueryTClassList> page)方法 :
@Override public List<TClass> queryTClassList(Page<QueryTClassList> page) { PageHelper.startPage(page.getPage(), page.getRows()); // return tclassMapper.queryTClassList(page.getCondition()); return tclassMapper.selectAll(); }
我们会发现:PageHelper.startPage(page.getPage(), page.getRows());设置了请求页码和每页记录数,然后在这句仅接着的下一句执行select请求就会执行分页,记住必须是紧挨着的下一句;(原理不懂的话请移步官方文档)
2.druid监控
地址栏输入:http://localhost:8080/test/druid
可以看到十分详细的sql监控信息
3.事务测试:
我们在TClassServiceImpl.java的saveOrUpdateTClass方法中,在执行更新的代码之后,人工制造一个RuntimeException ,来验证其事务是否回滚;
再执行编辑操作:
编辑原来的一班,将一班名称改为一班121,点击提交:
如果你debug的话,会发现1/0抛出运行时异常后,进入:
回滚事务,并在controller被catch,并返回前端错误提示;
检查数据库发现一班并没有改变名称:
在druid中也可以发现本次更新操作的事务回滚:
至此,事务已经证明生效了;大家可以自己尝试一下;
后记
本文项目源代码(含mysql数据库脚本):
https://github.com/zhaojiatao/springboot-zjt-chapter08-springboot-mybatis
相关文档(强烈推荐大家先阅读相关文档之后再动手整合,否则一些细节可能不了解,以后也是个坑):
通用mapper文档:https://gitee.com/free/Mapper
pagehelper文档:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md
layui文档:http://www.layui.com/doc/