springboot + mybatis + gradle項目構建過程

1.從Spring boot官網根據需求下載腳手架或者到GitHub上去搜索對應的腳手架項目,D_iao ^0^

? 文件目錄如下(此處generatorConfig.xml 和 log4j2.xml文件請忽略,后續會講解)


?2.使用Mybatis代碼自動構建插件生成代碼

?? gradle 相關配置

// Mybatis 代碼自動生成所引入的包
compile group: 'org.mybatis.generator', name: 'mybatis-generator-core', version: '1.3.3'// MyBatis代碼自動生成插件工具
apply plugin: "com.arenagod.gradle.MybatisGenerator"configurations {mybatisGenerator
}mybatisGenerator {verbose = true// 配置文件路徑configFile = 'src/main/resources/generatorConfig.xml'
}

?? generatorConfig.xml配置詳解

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfigurationPUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN""http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration><!--數據庫驅動包路徑 --><classPathEntry<!--此驅動包路徑可在項目的包庫中找到,復制過來即可-->location="C:\Users\pc\.gradle\caches\modules-2\files-2.1\mysql\mysql-connector-java\5.1.38\dbbd7cd309ce167ec8367de4e41c63c2c8593cc5\mysql-connector-java-5.1.38.jar"/><context id="mysql" targetRuntime="MyBatis3"><!--關閉注釋 --><commentGenerator><property name="suppressAllComments" value="true"/></commentGenerator><!--數據庫連接信息 --><jdbcConnection driverClass="com.mysql.jdbc.Driver"connectionURL="jdbc:mysql://127.0.0.1:3306/xxx" userId="root"password=""></jdbcConnection><!--生成的model 包路徑 ,其中rootClass為model的基類,配置之后他會自動繼承該類作為基類,trimStrings會為model字串去空格--><javaModelGenerator targetPackage="com.springboot.mybatis.demo.model"targetProject="D:/self-code/spring-boot-mybatis/spring-boot-mybatis/src/main/java"><property name="enableSubPackages" value="true"/><property name="trimStrings" value="true"/><property name="rootClass" value="com.springboot.mybatis.demo.model.common.BaseModel"/></javaModelGenerator><!--生成mapper xml文件路徑 --><sqlMapGenerator targetPackage="mapper"targetProject="D:/self-code/spring-boot-mybatis/spring-boot-mybatis/src/main/resources"><property name="enableSubPackages" value="true"/></sqlMapGenerator><!-- 生成的Mapper接口的路徑 --><javaClientGenerator type="XMLMAPPER"targetPackage="com.springboot.mybatis.demo.mapper" targetProject="D:/self-code/spring-boot-mybatis/spring-boot-mybatis/src/main/java"><property name="enableSubPackages" value="true"/></javaClientGenerator><!-- 對應的表 這個是生成Mapper xml文件的基礎,enableCountByExample如果為true則會在xml文件中生成樣例,過于累贅所以不要--><table tableName="tb_user" domainObjectName="User"enableCountByExample="false"enableDeleteByExample="false"enableSelectByExample="false"enableUpdateByExample="false"></table></context></generatorConfiguration>

以上配置中注意targetProject路徑請填寫絕對路徑,避免錯誤,其中targetPackage是類所處的包路徑(確保包是存在的,否則無法生成),也就相當于

?? 代碼生成

配置完成之后首先得在數據庫中新建對應的表,然后確保數據庫能正常訪問,最后在終端執行gradle mbGenerator或者點擊如下任務

成功之后它會生成model、mapper接口以及xml文件

?


?3.集成日志

? gradle 相關配置

compile group: 'org.springframework.boot', name: 'spring-boot-starter-log4j2', version: '1.4.0.RELEASE'// 排除沖突
configurations {mybatisGeneratorcompile.exclude module: 'spring-boot-starter-logging'
}

當沒有引入spring-boot-starter-log4j2包時會報錯:java.lang.IllegalStateException: Logback configuration error detected Logback 配置錯誤聲明

原因參考鏈接;https://blog.csdn.net/blueheart20/article/details/78111350?locationNum=5&fps=1

解決方案:排除依賴 spring-boot-starter-logging

what???

排除依賴之后使用的時候又報錯:Failed to load class "org.slf4j.impl.StaticLoggerBinder" 加載slf4j.impl.StaticLoggerBinder類失敗

原因參考鏈接:https://blog.csdn.net/lwj_199011/article/details/51853110

解決方案:添加依賴 spring-boot-starter-log4j2 此包所依賴的包如下:

<?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><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starters</artifactId><version>1.4.0.RELEASE</version></parent><artifactId>spring-boot-starter-log4j2</artifactId><name>Spring Boot Log4j 2 Starter</name><description>Starter for using Log4j2 for logging. An alternative tospring-boot-starter-logging</description><url>http://projects.spring.io/spring-boot/</url><organization><name>Pivotal Software, Inc.</name><url>http://www.spring.io</url></organization><properties><main.basedir>${basedir}/../..</main.basedir></properties><dependencies><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-slf4j-impl</artifactId></dependency><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-api</artifactId></dependency><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId></dependency><dependency><groupId>org.slf4j</groupId><artifactId>jcl-over-slf4j</artifactId></dependency><dependency><groupId>org.slf4j</groupId><artifactId>jul-to-slf4j</artifactId></dependency></dependencies>
</project>

它依賴了 log4j-slf4j-impl ,使用的是log4j2日志框架。

這里涉及到log4j、logback、log4j2以及slf4j相關概念,那么它們是啥關系呢?unbelievable...相關知識如下:

slf4j、log4j、logback、log4j2 
日志接口(slf4j) slf4j是對所有日志框架制定的一種規范、標準、接口,并不是一個框架的具體的實現,因為接口并不能獨立使用,需要和具體的日志框架實現配合使用(如log4j、logback)
日志實現(log4j、logback、log4j2) log4j是apache實現的一個開源日志組件 logback同樣是由log4j的作者設計完成的,擁有更好的特性,用來取代log4j的一個日志框架,是slf4j的原生實現 Log4j2是log4j 1.x和logback的改進版,據說采用了一些新技術(無鎖異步、等等),使得日志的吞吐量、性能比log4j 1.x提高10倍,并解決了一些死鎖的bug,而且配置更加簡單靈活,官網地址: http://logging.apache.org/log4j/2.x/manual/configuration.html 為什么需要日志接口,直接使用具體的實現不就行了嗎? 接口用于定制規范,可以有多個實現,使用時是面向接口的(導入的包都是slf4j的包而不是具體某個日志框架中的包),即直接和接口交互,不直接使用實現,所以可以任意的更換實現而不用更改代碼中的日志相關代碼。 比如:slf4j定義了一套日志接口,項目中使用的日志框架是logback,開發中調用的所有接口都是slf4j的,不直接使用logback,調用是 自己的工程調用slf4j的接口,slf4j的接口去調用logback的實現,可以看到整個過程應用程序并沒有直接使用logback,當項目需要更換更加優秀的日志框架時(如log4j2)只需要引入Log4j2的jar和Log4j2對應的配置文件即可,完全不用更改Java代碼中的日志相關的代碼logger.info(“xxx”),也不用修改日志相關的類的導入的包(import org.slf4j.Logger; import org.slf4j.LoggerFactory;)
使用日志接口便于更換為其他日志框架,適配器作用 log4j、logback、log4j2都是一種日志具體實現框架,所以既可以單獨使用也可以結合slf4j一起搭配使用)

? 到此我們使用的是Log4j2日志框架,接下來是配置log4j(可以使用properties和xml兩種方式配置,這里使用xml形式;有關log4j詳細配置講解參考鏈接:https://blog.csdn.net/menghuanzhiming/article/details/77531977),具體配置詳解如下:

<?xml version="1.0" encoding="UTF-8"?>
<!--日志級別以及優先級排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --><!--Configuration后面的status,這個用于設置log4j2自身內部的信息輸出,可以不設置,當設置成trace時,你會看到log4j2內部各種詳細輸出-->
<!--monitorInterval:Log4j能夠自動檢測修改配置 文件和重新配置本身,設置間隔秒數-->
<Configuration status="WARN"><!--定義一些屬性--><Properties><Property name="PID">????</Property><Property name="LOG_PATTERN">[%d{yyyy-MM-dd HH:mm:ss.SSS}] - ${sys:PID} --- %c{1}: %m%n</Property></Properties><!--輸出源,用于定義日志輸出的地方--><Appenders><!--輸出到控制臺--><Console name="Console" target="SYSTEM_OUT" follow="true"><PatternLayoutpattern="${LOG_PATTERN}"></PatternLayout></Console><!--文件會打印出所有信息,這個log每次運行程序會自動清空,由append屬性決定,適合臨時測試用--><!--append為TRUE表示消息增加到指定文件中,false表示消息覆蓋指定的文件內容,默認值是true--><!--<File name="File" fileName="logs/log.log" append="false">--><!--<PatternLayout>--><!--<pattern>[%-5p] %d %c - %m%n</pattern>--><!--</PatternLayout>--><!--</File>--><!--這個會打印出所有的信息,每次大小超過size,則這size大小的日志會自動存入按年份-月份建立的文件夾下面并進行壓縮,作為存檔 --><RollingFile name="RollingAllFile" fileName="logs/all/all.log"filePattern="logs/all/$${date:yyyy-MM}/all-%d{yyyy-MM-dd}-%i.log.gz"><PatternLayoutpattern="${LOG_PATTERN}" /><Policies><!--以下兩個屬性結合filePattern使用,完成周期性的log文件封存工作--><!--TimeBasedTriggeringPolicy 基于時間的觸發策略,以下是它的兩個參數:1.interval,integer型,指定兩次封存動作之間的時間間隔。單位:以日志的命名精度來確定單位,比如yyyy-MM-dd-HH 單位為小時,yyyy-MM-dd-HH-mm 單位為分鐘2.modulate,boolean型,說明是否對封存時間進行調制。若modulate=true,則封存時間將以0點為邊界進行偏移計算。比如,modulate=true,interval=4hours,那么假設上次封存日志的時間為03:00,則下次封存日志的時間為04:00,之后的封存時間依次為08:00,12:00,16:00--><!--<TimeBasedTriggeringPolicy/>--><!--SizeBasedTriggeringPolicy 基于日志文件大小的觸發策略,以下配置解釋為:當單個文件達到20M后,會自動將以前的內容,先創建類似 2014-09(年-月)的目錄,然后按 "xxx-年-月-日-序號"命名,打成壓縮包--><SizeBasedTriggeringPolicy size="200 MB"/></Policies></RollingFile><!-- 添加過濾器ThresholdFilter,可以有選擇的輸出某個級別及以上的類別  onMatch="ACCEPT" onMismatch="DENY"意思是匹配就接受,否則直接拒絕 --><RollingFile name="RollingErrorFile" fileName="logs/error/error.log"filePattern="logs/error/$${date:yyyy-MM}/%d{yyyy-MM-dd}-%i.log.gz"><ThresholdFilter level="ERROR"/><PatternLayoutpattern="${LOG_PATTERN}" /><Policies><!--<TimeBasedTriggeringPolicy/>--><SizeBasedTriggeringPolicy size="200 MB"/></Policies></RollingFile><RollingFile name="RollingWarnFile" fileName="logs/warn/warn.log"filePattern="logs/warn/$${date:yyyy-MM}/%d{yyyy-MM-dd}-%i.log.gz"><Filters><ThresholdFilter level="WARN"/><ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="NEUTRAL"/></Filters><PatternLayoutpattern="${LOG_PATTERN}" /><Policies><!--<TimeBasedTriggeringPolicy/>--><SizeBasedTriggeringPolicy size="200 MB"/></Policies></RollingFile></Appenders><!--然后定義Loggers,只有定義了Logger并引入的Appender,Appender才會生效--><Loggers><Logger name="org.hibernate.validator.internal.util.Version" level="WARN"/><Logger name="org.apache.coyote.http11.Http11NioProtocol" level="WARN"/><Logger name="org.apache.tomcat.util.net.NioSelectorPool" level="WARN"/><Logger name="org.apache.catalina.startup.DigesterFactory" level="ERROR"/><Logger name="org.springframework" level="INFO" /><Logger name="com.springboot.mybatis.demo" level="DEBUG"/><!--以上的logger會繼承Root,也就是說他們默認會輸出到Root下定義的符合條件的Appender中,若不想讓它繼承可以設置 additivity="false"并可以在Logger中設置 <AppenderRef ref="Console"/> 指定輸出到Console--><Root level="INFO"><AppenderRef ref="Console" /><AppenderRef ref="RollingAllFile"/><AppenderRef ref="RollingErrorFile"/><AppenderRef ref="RollingWarnFile"/></Root></Loggers>
</Configuration>

到此我們就算是把日志集成進去了,可以在終端看到各種log,very exciting!!!

log4j還可以發送郵件

添加依賴:

compile group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '2.0.0.RELEASE'

修改log4j配置:

在appender中添加如下:<!-- subject: 郵件主題  to: 接收人,多個以逗號隔開  from: 發送人  replyTo: 發送賬號 smtp: QQ查看鏈接https://service.mail.qq.com/cgi-bin/help?subtype=1&no=167&id=28 smtpDebug: 開啟詳細日志 smtpPassword: 授權碼,參看https://service.mail.qq.com/cgi-bin/help?subtype=1&&id=28&&no=1001256 smtpUsername: 用戶名--><SMTP name="Mail" subject="Error Log" to="xxx.com" from="xxx@qq.com" replyTo="xxx@qq.com"smtpProtocol="smtp" smtpHost="smtp.qq.com" smtpPort="587" bufferSize="50" smtpDebug="false"smtpPassword="授權碼" smtpUsername="xxx.com"></SMTP>在root里添加上面的appender讓其生效
<AppenderRef ref="Mail" level="error"/>

?搞定!


?4.集成MybatisProvider

? Why ?

? ? 有了它我們可以通過注解的方式結合動態SQL實現基本的增刪改查操作,而不需要再在xml中寫那么多重復繁瑣的SQL了

? Come on?↓

  First: 定義一個Mapper接口并實現基本操作,如下:

package com.springboot.mybatis.demo.mapper.common;import com.springboot.mybatis.demo.mapper.common.provider.AutoSqlProvider;
import com.springboot.mybatis.demo.mapper.common.provider.MethodProvider;
import com.springboot.mybatis.demo.model.common.BaseModel;
import org.apache.ibatis.annotations.DeleteProvider;
import org.apache.ibatis.annotations.InsertProvider;
import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.annotations.UpdateProvider;import java.io.Serializable;
import java.util.List;public interface BaseMapper<T extends BaseModel, Id extends Serializable> {@InsertProvider(type = AutoSqlProvider.class, method = MethodProvider.SAVE)int save(T entity);@DeleteProvider(type = AutoSqlProvider.class, method = MethodProvider.DELETE_BY_ID)int deleteById(Id id);@UpdateProvider(type = AutoSqlProvider.class, method = MethodProvider.UPDATE_BY_ID)int updateById(Id id);@SelectProvider(type = AutoSqlProvider.class, method = MethodProvider.FIND_ALL)List<T> findAll(T entity);@SelectProvider(type = AutoSqlProvider.class, method = MethodProvider.FIND_BY_ID)T findById(T entity);@SelectProvider(type = AutoSqlProvider.class, method = MethodProvider.FIND_AUTO_BY_PAGE)List<T> findAutoByPage(T entity);
}

其中AutoSqlProvider是提供sql的類,MethodProvider是定義好我們使用MybatisProvider需要實現的基本持久層方法,這兩個方法具體實現如下:

package com.springboot.mybatis.demo.mapper.common.provider;import com.google.common.base.CaseFormat;
import com.springboot.mybatis.demo.mapper.common.provider.model.MybatisTable;
import com.springboot.mybatis.demo.mapper.common.provider.utils.ProviderUtils;
import org.apache.ibatis.jdbc.SQL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.lang.reflect.Field;
import java.util.List;public class AutoSqlProvider {private static Logger logger = LoggerFactory.getLogger(AutoSqlProvider.class);public String findAll(Object obj) {MybatisTable mybatisTable = ProviderUtils.getMybatisTable(obj);List<Field> fields = mybatisTable.getMybatisColumnList();SQL sql = new SQL();fields.forEach(field -> sql.SELECT(CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName())));sql.FROM(mybatisTable.getName());logger.info(sql.toString());return sql.toString();}public String save(Object obj) {
     ...
return null;}public String deleteById(String id) {
     ...
return null;}public String findById(Object obj) {
...
return null;}public String updateById(Object obj) {
...
return null;}public String findAutoByPage(Object obj) {return null;}}
package com.springboot.mybatis.demo.mapper.common.provider;public class MethodProvider {public static final String SAVE = "save";public static final String DELETE_BY_ID = "deleteById";public static final String UPDATE_BY_ID = "updateById";public static final String FIND_ALL = "findAll";public static final String FIND_BY_ID = "findById";public static final String FIND_AUTO_BY_PAGE = "findAutoByPage";
}

注意:

1.如果你在BaseMapper中定義了某個方法一定要在SqlProvider類中去實現該方法,否則將報找不到該方法的錯誤

2.在動態拼接SQL的時候遇到一個問題:即使開啟了駝峰命名轉換,在拼接的時候依然需要手動將表屬性轉換,否則不會自動轉換

3.在SqlProvider中的SQL log可以去除,因為在集成日志的時候已經配置好了

4.ProviderUtils是通過反射的方式拿到表的一些基本屬性:表名,表屬性

?? 到這里MybatisProvider的基礎配置已經準備好,接下去就是讓每一個mapper接口去繼承我們這個基礎Mapper,這樣所有的基礎增刪改查都由BaseMapper負責,如下:

package com.springboot.mybatis.demo.mapper;import com.springboot.mybatis.demo.mapper.common.BaseMapper;
import com.springboot.mybatis.demo.model.User;import java.util.List;public interface UserMapper extends BaseMapper<User,String> {}

這樣UserMapper就不需要再關注那些基礎的操作了,wonderful !!!


?5. 整合JSP過程

? 引入核心包

compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.0.0.RELEASE'
// 注意此處一定要是compile或者缺省,不能使用providedRuntime否則jsp無法渲染
compile group: 'org.apache.tomcat.embed', name: 'tomcat-embed-jasper', version: '9.0.6' 

providedRuntime group: 'org.springframework.boot', name: 'spring-boot-starter-tomcat', version: '2.0.2.RELEASE' // 此行代碼是用于解決內置tomcat和外部tomcat沖突問題,若僅使用內置tomcat則無需此行代碼

這是兩個基本的包,其中spring-boot-starter-web會引入tomcat也就是我們常說的SpringBoot內置的tomcat,而tomcat-embed-jasper是解析jsp的包,如果這個包沒有引入或是有問題則無法渲染jsp頁面

? 修改Application啟動類

@EnableTransactionManagement 
@SpringBootApplication 
public class Application extends SpringBootServletInitializer { @Overrideprotected SpringApplicationBuilder configure(SpringApplicationBuilder application) {setRegisterErrorPageFilter(false);return application.sources(Application.class);}public static void main(String[] args) throws Exception {SpringApplication.run(Application.class, args);}
}

注意:啟動類必須繼承SpringBootServletInitializer 類并重寫configure方法

? 創建jsp頁面(目錄詳情如下)

? 接下來就是配置如何去獲取jsp頁面了,有兩中選擇

一:通過在application.properties文件中配置

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

然后創建controller(注意:在Spring 2.0之后如果要返回jsp頁面必須使用@Controller而不能使用@RestController)

@Controller // spring 2.0 如果要返回jsp頁面必須使用Controller而不能使用RestController
public class IndexController {@GetMapping("/")public String index() {return "index";}
}

二:通過配置文件實現,這樣的話直接請求 http:localhost:8080/就能直接獲取到index.jsp頁面,省去了controller代碼的書寫

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {// /static (or /public or /resources or /META-INF/resources@Beanpublic InternalResourceViewResolver viewResolver() {InternalResourceViewResolver resolver = new InternalResourceViewResolver();resolver.setPrefix("/WEB-INF/views/");resolver.setSuffix(".jsp");return resolver;}@Overridepublic void addViewControllers(ViewControllerRegistry registry) {registry.addViewController("/").setViewName("index");}

  // 此方法如果不重寫的話將無法找到index.jsp資源@Overridepublic void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {configurer.enable();} }

?6.集成Shiro認證和授權以及Session

? shiro核心

認證、授權、會話管理、緩存、加密

? 集成認證過程

(1)引包(注:包是按需引用的,以下只是個人構建時候引用的,僅供參考↓)

    // shirocompile group: 'org.apache.shiro', name: 'shiro-core', version: '1.3.2' // 必引包,shiro核心包compile group: 'org.apache.shiro', name: 'shiro-web', version: '1.3.2' // 與web整合的包compile group: 'org.apache.shiro', name: 'shiro-spring', version: '1.3.2' // 與spring整合的包compile group: 'org.apache.shiro', name: 'shiro-ehcache', version: '1.3.2' // shiro緩存

(2)shiro配置文件

@Configuration
public class ShiroConfig {@Bean(name = "shiroFilter")public ShiroFilterFactoryBean shiroFilterFactoryBean() {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();//攔截器MapMap<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();//配置不會被攔截的路徑filterChainDefinitionMap.put("/static/**", "anon");//配置退出filterChainDefinitionMap.put("/logout", "logout");
     //配置需要認證才能訪問的路徑filterChainDefinitionMap.put("/**", "authc");
     //配置需要認證和admin角色才能訪問的路徑
     filterChainDefinitionMap.put("user/**","authc,roles[admin]") //注意roles中的角色可以為多個且時and的關系,即要擁有所有角色才能訪問,如果要or關系可自行寫filter
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//配置登陸路徑shiroFilterFactoryBean.setLoginUrl("/login");//配置登陸成功后跳轉的路徑shiroFilterFactoryBean.setSuccessUrl("/index");//登陸失敗跳回登陸界面shiroFilterFactoryBean.setUnauthorizedUrl("/login");shiroFilterFactoryBean.setSecurityManager(securityManager());return shiroFilterFactoryBean;}@Beanpublic ShiroRealmOne shiroRealmOne() {ShiroRealmOne realm = new ShiroRealmOne(); // 此處是自定義shiro規則return realm;}@Bean(name = "securityManager")public DefaultWebSecurityManager securityManager() {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(shiroRealmOne());
     securityManager.setCacheManager(ehCacheManager());
     securityManager.setSessionManager(sessionManager());
return securityManager;}

@Bean(name = "ehCacheManager") // 將用戶信息緩存起來
public EhCacheManager ehCacheManager() {
return new EhCacheManager();
}

  @Bean(name = "shiroCachingSessionDAO") // shiroSession
  public SessionDAO shiroCachingSessionDAO() {
  EnterpriseCacheSessionDAO sessionDao = new EnterpriseCacheSessionDAO();
  sessionDao.setSessionIdGenerator(new JavaUuidSessionIdGenerator()); // SessionId生成器
  sessionDao.setCacheManager(ehCacheManager()); // 緩存
   return sessionDao;
  }
  @Bean(name = "sessionManager")
  public DefaultWebSessionManager sessionManager() {
  DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
  defaultWebSessionManager.setGlobalSessionTimeout(1000 * 60);
  defaultWebSessionManager.setSessionDAO(shiroCachingSessionDAO());
  return defaultWebSessionManager;
  }
}

自定義realm,繼承了AuthorizationInfo實現簡單的登陸驗證

package com.springboot.mybatis.demo.config.realm;import com.springboot.mybatis.demo.model.Permission;
import com.springboot.mybatis.demo.model.Role;
import com.springboot.mybatis.demo.model.User;
import com.springboot.mybatis.demo.service.PermissionService;
import com.springboot.mybatis.demo.service.RoleService;
import com.springboot.mybatis.demo.service.UserService;
import com.springboot.mybatis.demo.service.impl.PermissionServiceImpl;
import com.springboot.mybatis.demo.service.impl.RoleServiceImpl;
import com.springboot.mybatis.demo.service.impl.UserServiceImpl;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;public class ShiroRealmOne extends AuthorizingRealm {private Logger logger = LoggerFactory.getLogger(this.getClass());@Autowiredprivate UserService userServiceImpl;@Autowiredprivate RoleService roleServiceImpl;@Autowiredprivate PermissionService permissionServiceImpl;//授權(這里對授權不做講解,可忽略)
    @Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {logger.info("doGetAuthorizationInfo+" + principalCollection.toString());User user = userServiceImpl.findByUserName((String) principalCollection.getPrimaryPrincipal());List<Role> roleList = roleServiceImpl.findByUserId(user.getId());List<Permission> permissionList = roleList != null && !roleList.isEmpty() ? permissionServiceImpl.findByRoleIds(roleList.stream().map(Role::getId).collect(Collectors.toList())) : new ArrayList<>();
        SecurityUtils.getSubject().getSession().setAttribute(String.valueOf(user.getId()), SecurityUtils.getSubject().getPrincipals());SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();//賦予角色for (Role role : roleList) {simpleAuthorizationInfo.addRole(role.getRolName());}//賦予權限for (Permission permission : permissionList) {
            simpleAuthorizationInfo.addStringPermission(permission.getPrmName());}return simpleAuthorizationInfo;}// 認證@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {logger.info("doGetAuthenticationInfo +" + authenticationToken.toString());UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;String userName = token.getUsername();logger.info(userName + token.getPassword());User user = userServiceImpl.findByUserName(token.getUsername());if (user != null) {Session session = SecurityUtils.getSubject().getSession();session.setAttribute("user", user);return new SimpleAuthenticationInfo(userName, user.getUsrPassword(), getName());} else {return null;}}
}

到此shrio認證簡單配置就配置好了,接下來就是驗證了

控制器

package com.springboot.mybatis.demo.controller;import com.springboot.mybatis.demo.common.utils.SelfStringUtils;
import com.springboot.mybatis.demo.controller.common.BaseController;
import com.springboot.mybatis.demo.model.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;@Controller
public class IndexController extends BaseController{@PostMapping("login")public String login(User user, Model model) {if (user == null || SelfStringUtils.isEmpty(user.getUsrName()) || SelfStringUtils.isEmpty(user.getUsrPassword()) ) {model.addAttribute("warn","請填寫完整用戶名和密碼!");return "login";}Subject subject = SecurityUtils.getSubject();UsernamePasswordToken token = new UsernamePasswordToken(user.getUsrName(), user.getUsrPassword());token.setRememberMe(true);try {subject.login(token);} catch (AuthenticationException e) {model.addAttribute("error","用戶名或密碼錯誤,請重新登陸!");return "login";}return "index";}@GetMapping("login")public String index() {return "login";}}

login jsp:

<%--Created by IntelliJ IDEA.User: AdministratorDate: 2018/7/29Time: 14:34To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>登陸</title>
</head>
<body><form action="login" method="POST">User Name: <input type="text" name="usrName"><br />User Password: <input type="text" name="usrPassword" /><input type="submit" value="Submit" /></form><span style="color: #b3b20a;">${warn}</span><span style="color:#b3130f;">${error}</span>
</body>
</html>

index jsp:

<%--Created by IntelliJ IDEA.User: pcDate: 2018/7/23Time: 14:02To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body><h1>Welcome to here!</h1>
</body>
</html>

正常情況分析:

1.未登錄時訪問非login接口直接跳回login頁面

2.登陸失敗返回賬戶或密碼錯誤

3.未填寫完整賬戶和密碼返回請填寫完整賬戶和密碼

4.登陸成功跳轉到index頁面,如果不是admin角色則不能訪問user/**的路徑,其他可以正常訪問


?7.Docker 部署此項目

(1)基礎方式部署

? 構建Dockerfile

FROM docker.io/williamyeh/java8VOLUME /tmpVOLUME /opt/workspace#COPY /build/libs/spring-boot-mybatis-1.0-SNAPSHOT.war /opt/workspace/app.jarEXPOSE 8080ENTRYPOINT ["java","-jar","/app.jar"]

創建工作目錄掛載點,則可以將工作目錄掛載到host機上,然而也可以直接將jar包拷貝到容器中去,二者擇其一即可。本人較喜歡前者。

?? 在Dockerfile文件目錄下,執行? docker build -t 鏡像名:tag .? 構建鏡像

?? 因為此項目用到了Mysql,所以還得構建一個Mysql容器,運行命令:docker run --name mysql -v /home/vagrant/docker-compose/spring-boot-compose/enjoy-dir/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root mysql:5.7;

? 運行剛才構建的項目鏡像:docker run --name myproject -v /home/vagrant/workspace/:/opt/workspace --link mysql:mysql -p 8080:8080 -d 鏡像名字;掛載的目錄 /home/vagrant/workspace 根據自己的目錄而定

?? 訪問8080端口測試

(2)使用docker-compose工具管理單機部署(前提:安裝好docker-compose工具)

? 構建docker-compose.yml文件(此處除了有mysql外還加了個redis)

version: '3'
services:
??? db:
??????? image: docker.io/mysql:5.7
??????? command: --default-authentication-plugin=mysql_native_password
??????? container_name: db
??????? volumes:
??????????? - /home/vagrant/docker-compose/spring-boot-compose/enjoy-dir/mysql/data:/var/lib/mysql
??????????? - /home/vagrant/docker-compose/spring-boot-compose/enjoy-dir/mysql/logs:/var/log/mysql
??????? environment:
??????????? MYSQL_ROOT_PASSWORD: root
??????????? MYSQL_USER: 'test'
??????????? MYSQL_PASS: 'test'
??????? restart:
??????????? always
??????? networks:
??????????? - default
??? redis:
??????? image: docker.io/redis
??????? container_name: redis
??????? command: redis-server /usr/local/etc/redis/redis.conf
??????? volumes:
??????????? - /home/vagrant/docker-compose/spring-boot-compose/enjoy-dir/redis/data:/data
??????????? - /home/vagrant/docker-compose/spring-boot-compose/enjoy-dir/redis/redis.conf:/usr/local/etc/redis/redis.conf
??????? networks:
??????????? - default
??? spring-boot:
??????? build:
??????????? context: ./enjoy-dir/workspace
??????????? dockerfile: Dockerfile
??????? image:
??????????? spring-boot:1.0-SNAPSHOT
??????? depends_on:
??????????? - db
??????????? - redis
??????? links:
??????????? - db:mysql
??????????? - redis:redis
??????? ports:
??????????? - "8080:8080"
??????? volumes:
??????????? - /home/vagrant/docker-compose/spring-boot-compose/enjoy-dir/workspace:/opt/workspace
??????? networks:
??????????? - default
networks:
??? default:
??????? driver: bridge

注意:其中的掛載目錄依自己情況而定;redis密碼可以在redis.conf文件中配置,其詳細配置參見:https://woodenrobot.me/2018/09/03/%E4%BD%BF%E7%94%A8-docker-compose-%E5%9C%A8-Docker-%E4%B8%AD%E5%90%AF%E5%8A%A8%E5%B8%A6%E5%AF%86%E7%A0%81%E7%9A%84-Redis/

? 在docker-compose.yml文件目錄下執行:docker-compose up;在此過程中遇到的問題:mysql無法連接 -> 原因:root用戶外部無法使用,于是進入mysql中開放root用戶,具體參見:https://www.cnblogs.com/goxcheer/p/8797377.html

? 訪問 8080 端口測試

(3)使用docker swarm多機分布式部署

? 構建compose文件基于compose 3.0,其詳細配置參見官方網頁,

version: '3'
services:db:image: docker.io/mysql:5.7command: --default-authentication-plugin=mysql_native_password // 密碼加密機制volumes:- "/home/vagrant/docker-compose/spring-boot-compose/enjoy-dir/mysql/data:/var/lib/mysql"- "/home/vagrant/docker-compose/spring-boot-compose/enjoy-dir/mysql/logs:/var/log/mysql"environment:MYSQL_ROOT_PASSWORD: 'root'MYSQL_USER: 'test'MYSQL_PASS: 'test'restart: // 開機啟動alwaysnetworks: // mysql 數據庫容器連到 mynet overlay 網絡,只要連到該網絡的容器均可以通過別名 mysql 連接數據庫mynet:aliases:- mysqlports: - "3306:3306"deploy: // 使用 swarm 部署需要配置一下replicas: 1 // stack 啟動時默認開啟多少個服務restart_policy: // 重新構建策略condition: on-failureplacement: // 部署節點constraints: [node.role == worker]redis:image: docker.io/rediscommand: redis-server /usr/local/etc/redis/redis.confvolumes:- "/home/vagrant/docker-compose/spring-boot-compose/enjoy-dir/redis/data:/data"- "/home/vagrant/docker-compose/spring-boot-compose/enjoy-dir/redis/redis.conf:/usr/local/etc/redis/redis.conf"networks:mynet:aliases:- redisports: - "6379:6379"deploy: replicas: 1restart_policy: condition: on-failureplacement: constraints: [node.role == worker]spring-boot:build:context: ./enjoy-dir/workspacedockerfile: Dockerfileimage:spring-boot:1.0-SNAPSHOTdepends_on:- db- redisports: - "8080:8080"volumes: - "/home/vagrant/docker-compose/spring-boot-compose/enjoy-dir/workspace:/opt/workspace"networks:mynet:aliases:- spring-bootdeploy: replicas: 1 restart_policy: condition: on-failureplacement: constraints: [node.role == worker]
networks:mynet:

? compose 構建好了則執行 docker stack deploy -c [ compose文件路徑 ]? [ stack名字 ];如下:

執行完成之后可以在 manager 節點通過命令 docker service ls 查看 service,如下:

?以及查看 service 狀態:

? 通過 Protainer 工具可視化管理 Swarm;首先在任一臺機器上安裝 Protainer , 安裝詳解參見:http://www.pangxieke.com/linux/use-protainer-manage-docker.html

安裝完成之后則可以進去輕松橫向擴展自己的容器也就是service了,自由設置 scale...

?

總結:由 docker 基礎命令創建容器在容器數目不多的情況下很實用,但是容器多了怎么辦 -> 用 docker-compose 將容器進行分組管理,這樣大大提升效率,一個命令即可啟用和關閉多個容器。但是在單機下實用 docke-compose 確實能應付得過來,但是多機怎么辦 -> 用 docker swarm, 是的有了docker swarm 無論多少臺機器,再也不用一個機器一個機器去部署,docker swarm 會自動幫我們把容器部署到資源足夠的機器上去,這樣一個高效率的分布式部署就變得 so easy...


?8.讀寫分離

?采用讀寫分離來降低單個數據庫的壓力,提高訪問速度

(1)配置數據庫(將原來的數據庫配置改成下面的,這里只配置 master 和 slave1 兩個數據庫)

 
#----------------------------------------- 數據庫連接(單數據庫)----------------------------------------
#spring.datasource.url:=jdbc:mysql://localhost:3306/liuzj?useUnicode=true&characterEncoding=gbk&zeroDateTimeBehavior=convertToNull
#spring.datasource.username=root
#spring.datasource.password=
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
#----------------------------------------- 數據庫連接(單數據庫)----------------------------------------
#----------------------------------------- 數據庫連接(讀寫分離)----------------------------------------
# master(寫)
spring.datasource.master.url=jdbc:mysql://192.168.10.16:3306/test
spring.datasource.master.username=root
spring.datasource.master.password=123456
spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver
# slave1(讀)
spring.datasource.slave1.url=jdbc:mysql://192.168.10.17:3306/test
spring.datasource.slave1.username=test
spring.datasource.slave1.password=123456
spring.datasource.slave1.driver-class-name=com.mysql.jdbc.Driver
#----------------------------------------- 數據庫連接(讀寫分離)----------------------------------------

?(2)修改初始化 dataSource(將原來的?dataSource?替換成下面的)

    // ----------------------------------- 單數據源 start----------------------------------------//    @Bean
//    @ConfigurationProperties(prefix = "spring.datasource")
//    public DataSource dataSource() {
//        DruidDataSource druidDataSource = new DruidDataSource();
//        // 數據源最大連接數
//        druidDataSource.setMaxActive(Application.DEFAULT_DATASOURCE_MAX_ACTIVE);
//        // 數據源最小連接數
//        druidDataSource.setMinIdle(Application.DEFAULT_DATASOURCE_MIN_IDLE);
//        // 配置獲取連接等待超時的時間
//        druidDataSource.setMaxWait(Application.DEFAULT_DATASOURCE_MAX_WAIT);
//        return druidDataSource;
//    }// ----------------------------------- 單數據源 end----------------------------------------// ----------------------------------- 多數據源(讀寫分離)start----------------------------------------@Bean@ConfigurationProperties("spring.datasource.master")public DataSource masterDataSource() {return DataSourceBuilder.create().build();}@Bean@ConfigurationProperties("spring.datasource.slave1")public DataSource slave1DataSource() {return DataSourceBuilder.create().build();}@Beanpublic DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,@Qualifier("slave1DataSource") DataSource slave1DataSource) {Map<Object, Object> targetDataSources = new HashMap<>(2);targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource);MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);myRoutingDataSource.setTargetDataSources(targetDataSources);return myRoutingDataSource;}@ResourceMyRoutingDataSource myRoutingDataSource;// ----------------------------------- 多數據源(讀寫分離)end----------------------------------------

(3)使用 AOP 動態切換數據源(當然也可以采用 mycat,具體配置自行查閱資料)

/*** @author admin* @date 2019-02-27*/
@Aspect
@Component
public class DataSourceAspect {@Pointcut("!@annotation(com.springboot.mybatis.demo.config.annotation.Master) " +"&& (execution(* com.springboot.mybatis.demo.service..*.select*(..)) " +"|| execution(* com.springboot.mybatis.demo.service..*.get*(..))" +"|| execution(* com.springboot.mybatis.demo.service..*.find*(..)))")public void readPointcut() {}@Pointcut("@annotation(com.springboot.mybatis.demo.config.annotation.Master) " +"|| execution(* com.springboot.mybatis.demo.service..*.insert*(..)) " +"|| execution(* com.springboot.mybatis.demo.service..*.add*(..)) " +"|| execution(* com.springboot.mybatis.demo.service..*.update*(..)) " +"|| execution(* com.springboot.mybatis.demo.service..*.edit*(..)) " +"|| execution(* com.springboot.mybatis.demo.service..*.delete*(..)) " +"|| execution(* com.springboot.mybatis.demo.service..*.remove*(..))")public void writePointcut() {}@Before("readPointcut()")public void read() {DBContextHolder.slave();}@Before("writePointcut()")public void write() {DBContextHolder.master();}/*** 另一種寫法:if...else...  判斷哪些需要讀從數據庫,其余的走主數據庫*/
//    @Before("execution(* com.springboot.mybatis.demo.service.impl.*.*(..))")
//    public void before(JoinPoint jp) {
//        String methodName = jp.getSignature().getName();
//
//        if (StringUtils.startsWithAny(methodName, "get", "select", "find")) {
//            DBContextHolder.slave();
//        }else {
//            DBContextHolder.master();
//        }
//    }
}

(4)以上只是主要配置及步驟,像?DBContextHolder 等類此處沒有貼出,詳細參看 github

?

總結:參看資料:https://www.cnblogs.com/cjsblog/p/9712457.html


?9. 集成 Quartz 分布式定時任務

 ?? 幾個經典的定時任務比較:

  

? ? ?Spring 自帶定時器Scheduled是單應用服務上的,不支持分布式環境。如果要支持分布式需要任務調度控制插件spring-scheduling-cluster的配合,其原理是對任務加鎖實現控制,支持能實現分布鎖的中間件。

(1)初始化數據庫腳本(可自行到官網下載)

drop table if exists qrtz_fired_triggers;
drop table if exists qrtz_paused_trigger_grps;
drop table if exists qrtz_scheduler_state;
drop table if exists qrtz_locks;
drop table if exists qrtz_simple_triggers;
drop table if exists qrtz_simprop_triggers;
drop table if exists qrtz_cron_triggers;
drop table if exists qrtz_blob_triggers;
drop table if exists qrtz_triggers;
drop table if exists qrtz_job_details;
drop table if exists qrtz_calendars;create table qrtz_job_details(sched_name varchar(120) not null,job_name  varchar(120) not null,job_group varchar(120) not null,description varchar(250) null,job_class_name   varchar(250) not null,is_durable varchar(1) not null,is_nonconcurrent varchar(1) not null,is_update_data varchar(1) not null,requests_recovery varchar(1) not null,job_data blob null,primary key (sched_name,job_name,job_group)
);create table qrtz_triggers(sched_name varchar(120) not null,trigger_name varchar(120) not null,trigger_group varchar(120) not null,job_name  varchar(120) not null,job_group varchar(120) not null,description varchar(250) null,next_fire_time bigint(13) null,prev_fire_time bigint(13) null,priority integer null,trigger_state varchar(16) not null,trigger_type varchar(8) not null,start_time bigint(13) not null,end_time bigint(13) null,calendar_name varchar(200) null,misfire_instr smallint(2) null,job_data blob null,primary key (sched_name,trigger_name,trigger_group),foreign key (sched_name,job_name,job_group)references qrtz_job_details(sched_name,job_name,job_group)
);create table qrtz_simple_triggers(sched_name varchar(120) not null,trigger_name varchar(120) not null,trigger_group varchar(120) not null,repeat_count bigint(7) not null,repeat_interval bigint(12) not null,times_triggered bigint(10) not null,primary key (sched_name,trigger_name,trigger_group),foreign key (sched_name,trigger_name,trigger_group)references qrtz_triggers(sched_name,trigger_name,trigger_group)
);create table qrtz_cron_triggers(sched_name varchar(120) not null,trigger_name varchar(120) not null,trigger_group varchar(120) not null,cron_expression varchar(200) not null,time_zone_id varchar(80),primary key (sched_name,trigger_name,trigger_group),foreign key (sched_name,trigger_name,trigger_group)references qrtz_triggers(sched_name,trigger_name,trigger_group)
);create table qrtz_simprop_triggers(sched_name varchar(120) not null,trigger_name varchar(120) not null,trigger_group varchar(120) not null,str_prop_1 varchar(512) null,str_prop_2 varchar(512) null,str_prop_3 varchar(512) null,int_prop_1 int null,int_prop_2 int null,long_prop_1 bigint null,long_prop_2 bigint null,dec_prop_1 numeric(13,4) null,dec_prop_2 numeric(13,4) null,bool_prop_1 varchar(1) null,bool_prop_2 varchar(1) null,primary key (sched_name,trigger_name,trigger_group),foreign key (sched_name,trigger_name,trigger_group)references qrtz_triggers(sched_name,trigger_name,trigger_group)
);create table qrtz_blob_triggers(sched_name varchar(120) not null,trigger_name varchar(120) not null,trigger_group varchar(120) not null,blob_data blob null,primary key (sched_name,trigger_name,trigger_group),foreign key (sched_name,trigger_name,trigger_group)references qrtz_triggers(sched_name,trigger_name,trigger_group)
);create table qrtz_calendars(sched_name varchar(120) not null,calendar_name  varchar(120) not null,calendar blob not null,primary key (sched_name,calendar_name)
);create table qrtz_paused_trigger_grps(sched_name varchar(120) not null,trigger_group  varchar(120) not null,primary key (sched_name,trigger_group)
);create table qrtz_fired_triggers(sched_name varchar(120) not null,entry_id varchar(95) not null,trigger_name varchar(120) not null,trigger_group varchar(120) not null,instance_name varchar(200) not null,fired_time bigint(13) not null,sched_time bigint(13) not null,priority integer not null,state varchar(16) not null,job_name varchar(200) null,job_group varchar(200) null,is_nonconcurrent varchar(1) null,requests_recovery varchar(1) null,primary key (sched_name,entry_id)
);create table qrtz_scheduler_state(sched_name varchar(120) not null,instance_name varchar(120) not null,last_checkin_time bigint(13) not null,checkin_interval bigint(13) not null,primary key (sched_name,instance_name)
);create table qrtz_locks(sched_name varchar(120) not null,lock_name  varchar(40) not null,primary key (sched_name,lock_name)
);

(2)創建并配置好 Quartz 配置文件

# --------------------------------------- quartz ---------------------------------------
# 主要分為scheduler、threadPool、jobStore、plugin等部分
org.quartz.scheduler.instanceName=DefaultQuartzScheduler
org.quartz.scheduler.rmi.export=false
org.quartz.scheduler.rmi.proxy=false
org.quartz.scheduler.wrapJobExecutionInUserTransaction=false
# 實例化ThreadPool時,使用的線程類為SimpleThreadPool
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
# threadCount和threadPriority將以setter的形式注入ThreadPool實例
# 并發個數
org.quartz.threadPool.threadCount=5
# 優先級
org.quartz.threadPool.threadPriority=5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true
org.quartz.jobStore.misfireThreshold=5000
# 默認存儲在內存中
#org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
#持久化
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.dataSource=qzDS
org.quartz.dataSource.qzDS.driver=com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL=jdbc:mysql://192.168.10.16:3306/test?useUnicode=true&characterEncoding=UTF-8
org.quartz.dataSource.qzDS.user=root
org.quartz.dataSource.qzDS.password=123456
org.quartz.dataSource.qzDS.maxConnections=10
# --------------------------------------- quartz -----------------------------------------

(3)初始化 Quartz 的初始Bean

@Configuration
public class QuartzConfig {/*** 實例化SchedulerFactoryBean對象** @return SchedulerFactoryBean* @throws IOException 異常*/@Bean(name = "schedulerFactory")public SchedulerFactoryBean schedulerFactoryBean() throws IOException {SchedulerFactoryBean factoryBean = new SchedulerFactoryBean();factoryBean.setQuartzProperties(quartzProperties());return factoryBean;}/*** 加載配置文件** @return Properties* @throws IOException 異常*/@Beanpublic Properties quartzProperties() throws IOException {PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));//在quartz.properties中的屬性被讀取并注入后再初始化對象
        propertiesFactoryBean.afterPropertiesSet();return propertiesFactoryBean.getObject();}/*** quartz初始化監聽器** @return QuartzInitializerListener*/@Beanpublic QuartzInitializerListener executorListener() {return new QuartzInitializerListener();}/*** 通過SchedulerFactoryBean獲取Scheduler的實例** @return Scheduler* @throws IOException 異常*/@Bean(name = "Scheduler")public Scheduler scheduler() throws IOException {return schedulerFactoryBean().getScheduler();}}

(3)創建 Quartz 的 service 對 job進行一些基礎操作,實現動態調度 job

/*** @author admin* @date 2019-02-28*/
public interface QuartzJobService {/*** 添加任務** @param scheduler      Scheduler的實例* @param jobClassName   任務類名稱* @param jobGroupName   任務群組名稱* @param cronExpression cron表達式* @throws Exception*/void addJob(Scheduler scheduler, String jobClassName, String jobGroupName, String cronExpression) throws Exception;/*** 暫停任務** @param scheduler    Scheduler的實例* @param jobClassName 任務類名稱* @param jobGroupName 任務群組名稱* @throws Exception*/void pauseJob(Scheduler scheduler, String jobClassName, String jobGroupName) throws Exception;/*** 繼續任務** @param scheduler    Scheduler的實例* @param jobClassName 任務類名稱* @param jobGroupName 任務群組名稱* @throws Exception*/void resumeJob(Scheduler scheduler, String jobClassName, String jobGroupName) throws Exception;/*** 重新執行任務** @param scheduler      Scheduler的實例* @param jobClassName   任務類名稱* @param jobGroupName   任務群組名稱* @param cronExpression cron表達式* @throws Exception*/void rescheduleJob(Scheduler scheduler, String jobClassName, String jobGroupName, String cronExpression) throws Exception;/*** 刪除任務** @param jobClassName* @param jobGroupName* @throws Exception*/void deleteJob(Scheduler scheduler, String jobClassName, String jobGroupName) throws Exception;/*** 獲取所有任務,使用前端分頁** @return List*/List<QuartzJob> findList();}
/*** @author admin* @date 2019-02-28* @see QuartzJobService*/
@Service
public class QuartzJobServiceImpl implements QuartzJobService {@Autowiredprivate QuartzJobMapper quartzJobMapper;@Overridepublic void addJob(Scheduler scheduler, String jobClassName, String jobGroupName, String cronExpression) throws Exception {jobClassName = "com.springboot.mybatis.demo.job." + jobClassName;// 啟動調度器
        scheduler.start();//構建job信息JobDetail jobDetail = JobBuilder.newJob(QuartzJobUtils.getClass(jobClassName).getClass()).withIdentity(jobClassName, jobGroupName).build();//表達式調度構建器(即任務執行的時間)CronScheduleBuilder builder = CronScheduleBuilder.cronSchedule(cronExpression);//按新的cronExpression表達式構建一個新的triggerCronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobClassName, jobGroupName).withSchedule(builder).build();// 配置scheduler相關參數
        scheduler.scheduleJob(jobDetail, trigger);}@Overridepublic void pauseJob(Scheduler scheduler, String jobClassName, String jobGroupName) throws Exception {jobClassName = "com.springboot.mybatis.demo.job." + jobClassName;scheduler.pauseJob(JobKey.jobKey(jobClassName, jobGroupName));}@Overridepublic void resumeJob(Scheduler scheduler, String jobClassName, String jobGroupName) throws Exception {jobClassName = "com.springboot.mybatis.demo.job." + jobClassName;scheduler.resumeJob(JobKey.jobKey(jobClassName, jobGroupName));}@Overridepublic void rescheduleJob(Scheduler scheduler, String jobClassName, String jobGroupName, String cronExpression) throws Exception {jobClassName = "com.springboot.mybatis.demo.job." + jobClassName;TriggerKey triggerKey = TriggerKey.triggerKey(jobClassName, jobGroupName);CronScheduleBuilder builder = CronScheduleBuilder.cronSchedule(cronExpression);CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);// 按新的cronExpression表達式重新構建triggertrigger = trigger.getTriggerBuilder().withIdentity(jobClassName, jobGroupName).withSchedule(builder).build();// 按新的trigger重新設置job執行
        scheduler.rescheduleJob(triggerKey, trigger);}@Overridepublic void deleteJob(Scheduler scheduler, String jobClassName, String jobGroupName) throws Exception {jobClassName = "com.springboot.mybatis.demo.job." + jobClassName;scheduler.pauseTrigger(TriggerKey.triggerKey(jobClassName, jobGroupName));scheduler.unscheduleJob(TriggerKey.triggerKey(jobClassName, jobGroupName));scheduler.deleteJob(JobKey.jobKey(jobClassName, jobGroupName));}@Overridepublic List<QuartzJob> findList() {return quartzJobMapper.findList();}
}

(4)創建 job

/*** @author admin* @date 2019-02-28* @see BaseJob*/
public class HelloJob implements BaseJob {private final Logger logger = LoggerFactory.getLogger(getClass());@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {logger.info("hello, I'm quartz job - HelloJob");}
}

(5)然后就可以對 job 進行測試(測試添加、暫停、重啟等操作)

?總結:

  ??以上只展示集成的主要步驟,詳細可參看 github。

  ??在分布式情況下,quartz 會將任務分布在不同的機器上執行,可以將項目打成jar包,開啟兩個終端模擬分布式查看 job 的執行情況,會發現 HelloJob 會在兩個機器上交替執行。

  ? 以上集成過程參看資料:https://zhuanlan.zhihu.com/p/38546754


?10. 自動分表

(1)概述:

一般來說,分表都是根據最高頻查詢的字段進行拆分的。但是考慮到很多功能是需要全局查詢,所以在這種情況下,是無法避免全局查詢的。
對于經常需要全局查詢的部分數據,可以單獨做個冗余表,這部分就不要分表了。
對于不經常的全局查詢,就只能 union 了。但是通常情況下這種查詢響應時間都很久。所以就需要在功能上做一定的限制。比如查詢間隔之類的,防止數據庫長時間無響應。或者把數據同步到只讀從庫上,在從庫上進行搜索。不影響主庫運行。

(2)分表準備

???? 分表可配置化(啟用分表,對哪張表進行分表以及分表策略)

???? 如何進行動態分表

(3)實踐

???? ?首先定義自己的配置類

import com.beust.jcommander.internal.Lists;
import com.springboot.mybatis.demo.common.constant.Constant;
import com.springboot.mybatis.demo.common.utils.SelfStringUtils;import java.util.Arrays;
import java.util.List;
import java.util.Map;/*** 獲取數據源配置信息** @author lzj* @date 2019-04-09*/
public class DatasourceConfig {private Master master;private Slave1 slave1;private SubTable subTable;public SubTable getSubTable() {return subTable;}public void setSubTable(SubTable subTable) {this.subTable = subTable;}public Master getMaster() {return master;}public void setMaster(Master master) {this.master = master;}public Slave1 getSlave1() {return slave1;}public void setSlave1(Slave1 slave1) {this.slave1 = slave1;}public static class Master {private String jdbcUrl;private String username;private String password;private String driverClassName;public String getJdbcUrl() {return jdbcUrl;}public void setJdbcUrl(String jdbcUrl) {this.jdbcUrl = jdbcUrl;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public String getDriverClassName() {return driverClassName;}public void setDriverClassName(String driverClassName) {this.driverClassName = driverClassName;}}public static class Slave1 {private String jdbcUrl;private String username;private String password;private String driverClassName;public String getJdbcUrl() {return jdbcUrl;}public void setJdbcUrl(String jdbcUrl) {this.jdbcUrl = jdbcUrl;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public String getDriverClassName() {return driverClassName;}public void setDriverClassName(String driverClassName) {this.driverClassName = driverClassName;}}public static class SubTable{private boolean enable;private String schemaRoot;private String schemas;private String strategy;public String getStrategy() {return strategy;}public void setStrategy(String strategy) {this.strategy = strategy;}public boolean isEnable() {return enable;}public void setEnable(boolean enable) {this.enable = enable;}public String getSchemaRoot() {return schemaRoot;}public void setSchemaRoot(String schemaRoot) {this.schemaRoot = schemaRoot;}public List<String> getSchemas() {if (SelfStringUtils.isNotEmpty(this.schemas)) {return Arrays.asList(this.schemas.split(Constant.Symbol.COMMA));}return Lists.newArrayList();}public void setSchemas(String schemas) {this.schemas = schemas;}}
}

因為此項目是配置了多數據源,所以分為master以及slave兩個數據源配置,再加上分表配置

#-------------------自動分表配置-----------------
spring.datasource.sub-table.enable = true
spring.datasource.sub-table.schema-root = classpath*:sub/
spring.datasource.sub-table.schemas = smg_user
spring.datasource.sub-table.strategy = each_day
#-------------------自動分表配置-----------------

以上配置是寫在application.properties配置文件中的。然后在將我們定義的配置類DataSourceConfig類交給IOC容器管理,即:

   @Bean@ConfigurationProperties(prefix = "spring.datasource")public DatasourceConfig datasourceConfig(){return new DatasourceConfig();}

這樣我們便可以通過自定義的配置類拿到相關的配置

? ??? 然后通過AOP切入mapper方法層,每次調用mapper方法時判斷該執行sql的相關實體類是否需要分表

@Aspect
@Component
public class BaseMapperAspect {private final static Logger logger = LoggerFactory.getLogger(BaseMapperAspect.class);//    @Autowired
//    DataSourceProperties dataSourceProperties;//    @Autowired
//    private DataSource dataSource;
@Autowiredprivate DatasourceConfig datasourceConfig;@AutowiredSubTableUtilsFactory subTableUtilsFactory;@Autowiredprivate DBService dbService;@ResourceMyRoutingDataSource myRoutingDataSource;@Pointcut("execution(* com.springboot.mybatis.demo.mapper.common.BaseMapper.*(..))")public void getMybatisTableEntity() {}/*** 獲取runtime class* @param joinPoint target* @throws ClassNotFoundException 異常*/@Before("getMybatisTableEntity()")public void setThreadLocalMap(JoinPoint joinPoint) throws ClassNotFoundException {...// 自動分表MybatisTable mybatisTable = MybatisTableUtils.getMybatisTable(Class.forName(actualTypeArguments[0].getTypeName()));Assert.isTrue(mybatisTable != null, "Null of the MybatisTable");String oldTableName = mybatisTable.getName();if (datasourceConfig.getSubTable().isEnable() && datasourceConfig.getSubTable().getSchemas().contains(oldTableName)) {ThreadLocalUtils.setSubTableName(subTableUtilsFactory.getSubTableUtil(datasourceConfig.getSubTable().getStrategy()).getTableName(oldTableName));// 判斷是否需要分表
            dbService.autoSubTable(ThreadLocalUtils.getSubTableName(),oldTableName,datasourceConfig.getSubTable().getSchemaRoot());}else {
      ThreadLocalUtils.setSubTableName(oldTableName);
     }
}

如果需要分表則會通過配置的策略獲取表名,然后判斷數據庫是否有該表,如果沒有則自動創建,否則跳過

? ???? ?創建對應分表后,則是對sql進行攔截修改,這里是定義mybatis攔截器攔截sql,如果該sql對應的實體類需要分表,則修改sql的表名,即定位到對應表進行操作

/*** 動態定位表** @author liuzj* @date 2019-04-15*/
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class})})
public class SubTableSqlHandler implements Interceptor {Logger logger = LoggerFactory.getLogger(SubTableSqlHandler.class);@Overridepublic Object intercept(Invocation invocation) throws Throwable {StatementHandler handler = (StatementHandler)invocation.getTarget();BoundSql boundSql = handler.getBoundSql();String sql = boundSql.getSql();// 修改 sqlif (SelfStringUtils.isNotEmpty(sql)) {MybatisTable mybatisTable = MybatisTableUtils.getMybatisTable(ThreadLocalUtils.get());Assert.isTrue(mybatisTable != null, "Null of the MybatisTable");Field sqlField = boundSql.getClass().getDeclaredField("sql");sqlField.setAccessible(true);sqlField.set(boundSql,sql.replaceAll(mybatisTable.getName(),ThreadLocalUtils.getSubTableName()));}return invocation.proceed();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}
}

以上是此項目動態分表的基本思路,詳細代碼參見GitHub

?

未完!待續。。。如有不妥之處,請提建議和意見,謝謝

轉載于:https://www.cnblogs.com/lzj123/p/9277021.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/279950.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/279950.shtml
英文地址,請注明出處:http://en.pswp.cn/news/279950.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

基于間隔推送全量更新數據狀態的設計方法

2019獨角獸企業重金招聘Python工程師標準>>> 假如有個直播間&#xff0c;在數據有更新的時候&#xff0c;能及時反映在客戶端上。通信方式來說&#xff0c;有兩種&#xff1a; 1、拉取模式。 2、推送拉取模式&#xff08;或者純推送&#xff09; 拉取模式&#xff0…

Redis 哈希(Hash)

哈希hash又稱為散列、雜湊等&#xff0c;是將任意長度的輸入通過散列算法變換為固定長度的輸出&#xff0c;最終輸出也就是哈希值。這種轉換是一種壓縮映射。也就是說&#xff0c;散列值的空間通常要遠小于輸入控件&#xff0c;不同的輸入可能會散列成相同的輸出&#xff0c;所…

京東Vue組件庫NutUI 2.0發布:將支持跨平臺!

NutUI 是一套來自京東用戶體驗設計部&#xff08;JDC&#xff09;前端開發部的移動端 Vue 組件庫&#xff0c;NutUI 1.0 版本于 2018 年發布。據不完全統計&#xff0c;目前在京東至少有30多個 web 項目正在使用 NutUI。 經過一段時間緊鑼密鼓的開發&#xff0c;近期&#xff0…

macbook 下載時睡眠_MacBook進入睡眠狀態時如何自動使其靜音

macbook 下載時睡眠You open your MacBook to take notes in class or during a meeting, and your music starts playing. Loudly. Not only did you disrupt everyone, you also revealed your passion for 90’s boy bands to a room full of people who once respected you…

Mac 的mysql5.7沒有配置文件,如何解決only_full_group_by 問題

數據庫版本是5.7.19&#xff0c;在寫語句的時候&#xff0c;只要涉及ORDER BY,就會報錯&#xff0c; ERROR 1055 (42000): Expression #7 of SELECT list is not in GROUP BY clause and contains nonaggregated column postscan.verifyDelayLog.auditor which is not function…

Spring MVC 入門(一)

什么是 Spring MVC 學習某一樣東西之前&#xff0c;我們一定要大致知道這個東西是什么&#xff0c;能干什么&#xff0c;為什么要用它。 Spring MVC 是一個開源平臺&#xff0c;一個基于 Spring 的 MVC 框架&#xff0c;它支持基于 Java 開發 Web 應用程序。MVC 架構很利于開發…

開源網關 Apache APISIX 認證鑒權精細化實戰講解

關注公眾號并添加到“星標?”&#xff0c;防止錯過消息后臺回復【資料包】獲取學習資料GitOps 新手入門到專家進階實戰詳細教程作者錢勇&#xff0c;API7.ai 開發工程師&#xff0c;Apache APISIX Committer在當下云原生越發成熟的環境下&#xff0c;API 網關最核心的功能可以…

python應用POP3、IMAP、SMTP 協議,獲取郵箱驗證碼

&#xff30;&#xff2f;&#xff30;&#xff13;和&#xff29;&#xff2d;&#xff21;&#xff30;是郵件相關的協議&#xff0c;&#xff29;&#xff2d;&#xff21;&#xff30;是比&#xff30;&#xff2f;&#xff30;&#xff13;更高級一點的協議&#xff0c;實…

固件中啟用的虛擬化否_哪些固件或硬件機制可啟用強制關機?

固件中啟用的虛擬化否At one time or another, all of us have had to force our computers to shut down by pushing and holding the power button down until they powered off. Is this mechanism hardware-based, firmware-based, or both? Today’s SuperUser Q&A p…

簡述閉包

閉包 這是我對閉包的一點小理解.有問題請直接指出,在此先謝過! 閉包的含義 封閉隔離的空間,在javascript中,只有函數能夠符合這種特性; 為什么要用閉包呢? 因為在引用外部js文件(如jquery,各種框架)時防止變量重名造成的問題,同時也使代碼更具隱私性; 獲取閉包中數據的方法: …

Confluence 6 配置服務器基礎地址備注

使用不同 URL。如果你配置了不同的基礎 URL 地址或者你站點的訪問者使用了不同的 URL 地址來訪問你的 Confluence 地址&#xff0c;你有很大概率可能會受到錯誤信息。修改上下文地址。如果你修改了基礎 URL 地址的上下文地址&#xff0c;你同時也需要修改下面的配置&#xff1a…

2019第10周知識總結

react 事件綁定 函數寫法 文檔總結 https://react.docschina.org/docs/handling-events.html 1 通過 constroucor綁定 class Toggle extends React.Component {constructor(props) {super(props);this.state {isToggleOn: true};// This binding is necessary to make this wo…

.NET 云原生架構師訓練營(基于 OP Storming 和 Actor 的大型分布式架構二)--學習筆記...

▲ 點擊上方“DotNet NB”關注公眾號回復“1”獲取開發者路線圖學習分享 丨作者 / 鄭 子 銘 這是DotNet NB 公眾號的第202篇原創文章目錄為什么我們用 OrleansDapr VS OrleansActor 模型Orleans 的核心概念結合 OP Storming 的實踐結合 OP Storming 的實踐業務模型設計模型代…

PHP 多維數組轉json對象

PHP 多維數組轉json對象 php 數組轉json對象&#xff0c;可能大家都知道要用json_encode,但是轉換出來的格式多有不同&#xff0c;此處做個小小的記錄&#xff01; 1. 一維數組轉json對象 <?php $arr_1 [one, two, three]; var_dump(json_encode($arr_1)); $arr_2 [0 >…

微軟文本檢索_如何在Microsoft Word中引用其他文檔中的文本

微軟文本檢索You probably have some text that you type often in your Word documents, such as addresses. Instead of retyping this text every time you need it, you can put this common text into one Word document and reference it in other documents–it’ll eve…

Hadoop-Flume-類比吸塵器圖解

2019獨角獸企業重金招聘Python工程師標準>>> 這是我自己理解Hadoop-Flume的方式 轉載于:https://my.oschina.net/u/3697442/blog/1560613

BZOJ4327:[JSOI2012]玄武密碼(SAM)

Description 在美麗的玄武湖畔&#xff0c;雞鳴寺邊&#xff0c;雞籠山前&#xff0c;有一塊富饒而秀美的土地&#xff0c;人們喚作進香河。相傳一日&#xff0c;一縷紫氣從天而至&#xff0c;只一瞬間便消失在了進香河中。老人們說&#xff0c;這是玄武神靈將天書藏匿在此。 很…

ChatGPT 之后,再玩玩 Stable-Diffusion

前些天體驗的 ChatGPT 主要用來進行文本方面的處理&#xff0c;那么圖片生成有沒有這樣的 AI 工具 呢&#xff1f;答案是肯定的。例如&#xff1a;和菜頭公眾號的題圖和文章中的插圖大多都是使用 Stable-Diffusion 的 AI 圖形生成工具創作的。順著 Stable-Diffusion 搜索了下相…

滲透測試入門DVWA 教程1:環境搭建

首先歡迎新萌入坑。哈哈。你可能抱著好奇心或者疑問。DVWA 是個啥&#xff1f; DVWA是一款滲透測試的演練系統&#xff0c;在圈子里是很出名的。如果你需要入門&#xff0c;并且找不到合適的靶機&#xff0c;那我就推薦你用DVWA。 我們通常將演練系統稱為靶機&#xff0c;下面請…

指派問題(匈牙利算法)

問題描述&#xff1a; 在生活中經常遇到這樣的問題&#xff0c;某單位需完成n項任務&#xff0c;恰好有n個人可承擔這些任務。由于每人的專長不同&#xff0c;各人完成任務不同(或所費時間)&#xff0c;效率也不同。于是產生應指派哪個人去完成哪項任務&#xff0c;使完成n項任…