前言:
開發環境: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: '序號', width:80,sort: true} ,{field: 'id', title: 'ID', width:300, unresize:true} ,{field: 'name', title: '班級名稱', width:200,unresize:true} ,{fixed: 'right', width: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/