springboot學習筆記:8. springboot+druid+mysql+mybatis+通用mapper+pagehelper+mybatis-generator+freemarker+layui


前言:

開發環境: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&amp;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/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM