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
?
未完!待續。。。如有不妥之處,請提建議和意見,謝謝