若是您有幸能看到,請認閱讀如下內容;css
一、本項目臨摹自abel533的Guns,他的項目 fork 自 stylefeng 的 Guns!開源的世界真好,能夠學到不少知識。java
二、版權歸原做者全部,本身只是學習使用。跟着大佬的思路,但願本身也能變成大佬。gogogo》。。mysql
三、目前只是一個後臺模塊,但願本身技能加強到必定時,能夠把stylefeng 的 [Guns]融合進來。git
四、note裏面是本身的學習過程,菜鳥寫的,不是大佬寫的。內容都是大佬的。github
原本想一步一步的來,可是工具類快把我看暈了。因此咱們仍是先來看配置類吧,這纔是今天的主角。先從數據庫,日誌,緩存開始。web
想說明的是SpringBoot有四個重要的特性:spring
目前重要的是理解前兩個,只要看見這個spring-boot-starter-Xxx
它就屬於起步依賴。會自動導入想依賴的庫。sql
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.3.RELEASE</version>
<relativePath/>
</parent>
-------------------------------------------------------------------------------
<dependencies>
<!--spring boot依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
複製代碼
《SpringBoot實戰》小節 有機會必定要看《Spring實戰》是同一個做者。結合代碼效果更佳。實戰練習筆記數據庫
SpringBoot爲Spring應用程序的開發提供了一種激動人心的新方式,框架自己帶來的阻力很小,自動配置消除了傳統Spring應用程序裏不少的樣板配置,Spring的起步依賴讓你能經過庫所提供的功能而非名稱與版本號來指定構建依賴。編程
二、接下來,回到咱們項目中的配置吧,先從阿里的druid。WebConfig通常是配置的起點。帶有@Configuration
註解的就意味着這是一個配置類。還有就是@Bean
註解。bean的定義以前在XMl中形式爲<bean id ="xx" class="xx.xx.xx" />
在spring boot中添加本身的Servlet、Filter、Listener有兩種方法
ServletRegistrationBean
、FilterRegistrationBean
、ServletListenerRegistrationBean
得到控制//** * web 配置類 還有不少 */
@Configuration
public class WebConfig {
/** * druidServlet註冊 */
@Bean
public ServletRegistrationBean druidServletRegistration() {
ServletRegistrationBean registration = new ServletRegistrationBean(new StatViewServlet());
registration.addUrlMappings("/druid/*");
return registration;
}
/** * druid監控 配置URI攔截策略 */
@Bean
public FilterRegistrationBean druidStatFilter(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
//添加過濾規則.
filterRegistrationBean.addUrlPatterns("/*");
//添加不須要忽略的格式信息.
filterRegistrationBean.addInitParameter(
"exclusions","/static/*,*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid,/druid/*");
//用於session監控頁面的用戶名顯示 須要登陸後主動將username注入到session裏
filterRegistrationBean.addInitParameter("principalSessionName","username");
return filterRegistrationBean;
}
/** * druid數據庫鏈接池監控 */
@Bean
public DruidStatInterceptor druidStatInterceptor() {
return new DruidStatInterceptor();
}
/** * druid數據庫鏈接池監控 */
@Bean
public BeanTypeAutoProxyCreator beanTypeAutoProxyCreator() {
BeanTypeAutoProxyCreator beanTypeAutoProxyCreator = new BeanTypeAutoProxyCreator();
beanTypeAutoProxyCreator.setTargetBeanType(DruidDataSource.class);
beanTypeAutoProxyCreator.setInterceptorNames("druidStatInterceptor");
return beanTypeAutoProxyCreator;
}
/** * druid 爲druidStatPointcut添加攔截 * @return */
@Bean
public Advisor druidStatAdvisor() {
return new DefaultPointcutAdvisor(druidStatPointcut(), druidStatInterceptor());
}
}
複製代碼
三、接下來咱們在看看數據源的配置,先摘抄點我以前的筆記。配置H2數據庫和JDBC的。
H2是一個開源的嵌入式數據庫引擎,採用java語言編寫,不受平臺的限制,同時H2提供了一個十分方便的web控制檯用於操做和管理數據庫內容。H2還提供兼容模式,能夠兼容一些主流的數據庫,所以採用H2做爲開發期的數據庫很是方便。(數據存儲在內存中)。
還須要注意的是DataSource
數據源主要有兩種方式實現:
@Configuration
public class DataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("my-test-data.sql")
.build();
}
-----------------------------------------------------------------------------
@Bean
public JdbcOperations jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
複製代碼
四、須要補充一點的是:老外不少都在用底層的JDBC技術,由於原生,效率高。jdbcTemplate
是Spring對JDBC進一步封裝。命名參數的使用。這種思想理解了嗎?
其實還有一種更絕絕的那就是Spring Date。只要繼承了Repository
接口,你就擁有了18個方法,不知足你的話,還能夠本身定義,還有一個就是JpaRepository
建議瞭解下。
private static final String SELECT_SPITTLE = "select sp.id, s.id as spitterId, s.username, s.password, s.fullname, s.email, s.updateByEmail, sp.message, sp.postedTime from Spittle sp, Spitter s where sp.spitter = s.id";
private static final String SELECT_SPITTLE_BY_ID = SELECT_SPITTLE + " and sp.id=?";
private static final String SELECT_SPITTLES_BY_SPITTER_ID = SELECT_SPITTLE + " and s.id=? order by sp.postedTime desc";
private static final String SELECT_RECENT_SPITTLES = SELECT_SPITTLE + " order by sp.postedTime desc limit ?";
public List<Spittle> findBySpitterId(long spitterId) {
return jdbcTemplate.query(SELECT_SPITTLES_BY_SPITTER_ID, new SpittleRowMapper(), spitterId);
}
public List<Spittle> findBySpitterId(long spitterId) {
return jdbcTemplate.query(SELECT_SPITTLES_BY_SPITTER_ID, new SpittleRowMapper(), spitterId);
}
複製代碼
五、接下來咱們就是配置數據源了,
原本想錄個Gif,但我軟件出BUG了,有什麼好推薦的麼?爲了避免佔地方,只放一張。關於日誌的,自行腦補。好想給你們分享個人書籤,太多有用的了。
/** * <p>數據庫數據源配置</p> * <p>說明:這個類中包含了許多默認配置,若這些配置符合您的狀況,您能夠不用管,若不符合,建議不要修改本類,建議直接在"application.yml"中配置便可</p> */
@Component
@ConfigurationProperties(prefix = "spring.datasource")
public class DruidProperties {
private String url = "jdbc:mysql://127.0.0.1:3306/guns?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull";
private String username = "root";
private String password = "632364";
private String driverClassName = "com.mysql.jdbc.Driver";
//爲了節約地方就不都貼出來了。
public void config(DruidDataSource dataSource) {
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverClassName);
dataSource.setInitialSize(initialSize); //定義初始鏈接數
dataSource.setMinIdle(minIdle); //最小空閒
dataSource.setMaxActive(maxActive); //定義最大鏈接數
dataSource.setMaxWait(maxWait); //最長等待時間
// 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒
dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
// 配置一個鏈接在池中最小生存的時間,單位是毫秒
dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
dataSource.setValidationQuery(validationQuery);
dataSource.setTestWhileIdle(testWhileIdle);
dataSource.setTestOnBorrow(testOnBorrow);
dataSource.setTestOnReturn(testOnReturn);
// 打開PSCache,而且指定每一個鏈接上PSCache的大小
dataSource.setPoolPreparedStatements(poolPreparedStatements);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
try {
dataSource.setFilters(filters);
} catch (SQLException e) {
e.printStackTrace();
}
}
複製代碼
六、還有就是多數據源,採用切面織入。直接拿本身以前的筆記吧,
在軟件開發中,散佈於應用中多處的功能被稱爲橫切關注點(crosscutting concern)。一般來說橫切關注點從概念上是與應用的業務邏輯分離的。但每每是耦合在一塊兒的,把這些橫切關注點與業務邏輯相分離正是面向切面編程(AOP)所要解決的問題。
依賴注入(DI)管理咱們的應用對象,DI有助於應用對象之間解耦。而AOP能夠實現橫切關注點與它們所影響的對象之間的耦合。
橫切關注點能夠被模塊化爲特殊的類,這些類被稱爲切面(aspect). 這樣作帶來兩個好處:每一個關注點都集中到一個地方,而不是分散到多處代碼中:其次,服務模塊更簡潔,由於它只包含了主要關注點(核心功能)的代碼。而次要關注的代碼被移到切面中了。
描述切面的經常使用術語有:通知(advice)、切點(pointcut)、(鏈接點)。
通知(advice)
通知定義了切面是什麼以及什麼時候使用。除了描述切面要完成的工做外,通知還解決了什麼時候執行這個工做問題。它應該在某個方法被調用以前?以後?以前和以後都調用?仍是隻在方法拋出異常時調用?
Spring切面能夠應用5中類型的通知:
鏈接點
咱們的應用可能有數以千計的時機應用通知,這些時機被稱爲鏈接點。鏈接點是在應用執行過程當中可以插入的一個點。這個點能夠是調用方法時,拋出異常時,甚至修改一個字段時。切面能夠利用這些點插入到應用的正常流程之中,並添加新的行爲。
切點
若是說通知定義了切面的的「什麼」和「什麼時候」,那麼切點定義了「何處」。切點的定義會匹配通知所要織入的一個或多個鏈接點。
切面
切面是通知和切點的結合。通知和切點經過定義了切面的所有 內容——他是什麼,在何時和在哪裏完成其功能。
引入 引入容許咱們向現有的類添加新的方法或者屬性。
織入
織入是把切面應用到目標對象並建立新的代理對象的過程。切面在指定的鏈接點被織入到目標對象。在目標對象的生命週期裏有多個點能夠進行織入:
編譯器:切面在目標類編譯時被織入。Aspect的織入編譯器就是以這種方式織入切面的。
類加載器:切面在目標類加載到JVM時被織入。須要特殊的類加載(Classloader),它能夠在目標類被引入以前加強該目標類的字節碼(CGlib)
運行期:切面在應用運行時的某個時刻被織入。AOP會爲目標對象建立一個代理對象 Spring提供了4種類型的AOP支持:
基於代理的經典Spring AOP
純POJO切面
@AspectJ註解驅動的切面
注入式AspectJ切面
七、帶着上面的概念,咱們在來看下多數據源的配置,先看一下測試效果:
首先所數據源做爲一個切面,用@Aspect註解,而後定義了切點,只要使用@DataSource註解的方法它就是一個切點,簡單說就是切面切在那個方法上。而後用@Around("cut()")定義了環繞通知,就是調用前和調用以後執行這個數據源。還有就是這裏使用了日誌記錄功能,這個主題待會說。
/** * 多數據源的枚舉 */
public interface DSEnum {
String DATA_SOURCE_GUNS = "dataSourceGuns"; //guns數據源
String DATA_SOURCE_BIZ = "dataSourceBiz"; //其餘業務的數據源
}
--------------------------------------------------------------------------------
@Override
@DataSource(name = DSEnum.DATA_SOURCE_BIZ)
public void testBiz() {
Test test = testMapper.selectByPrimaryKey(1);
test.setId(22);
testMapper.insert(test);
}
@Override
@DataSource(name = DSEnum.DATA_SOURCE_GUNS)
public void testGuns() {
Test test = testMapper.selectByPrimaryKey(1);
test.setId(33);
testMapper.insert(test);
}
複製代碼
/** * * 多數據源切換的aop */
@Aspect
@Component
@ConditionalOnProperty(prefix = "guns", name = "muti-datasource-open", havingValue = "true")
public class MultiSourceExAop implements Ordered {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Pointcut(value = "@annotation(com.guo.guns.common.annotion.DataSource)")
private void cut() {
}
@Around("cut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
Signature signature = point.getSignature();
MethodSignature methodSignature = null;
if (!(signature instanceof MethodSignature)) {
throw new IllegalArgumentException("該註解只能用於方法");
}
methodSignature = (MethodSignature) signature;
Object target = point.getTarget();
Method currentMethod = target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
DataSource datasource = currentMethod.getAnnotation(DataSource.class);
if(datasource != null){
DataSourceContextHolder.setDataSourceType(datasource.name());
log.debug("設置數據源爲:" + datasource.name());
}else{
DataSourceContextHolder.setDataSourceType(DSEnum.DATA_SOURCE_GUNS);
log.debug("設置數據源爲:dataSourceCurrent");
}
try {
return point.proceed();
} finally {
log.debug("清空數據源信息!");
DataSourceContextHolder.clearDataSourceType();
}
}
}
複製代碼
這個項目使用了Mybatis做爲持久層框架,因此看看他是怎麼配置的。要使用固然要注入了,這裏使用了@Autowired註解。
在Spring中,對象無需本身查找或建立與其所關聯的其餘對象。相反,容器負責把須要相互協做的對象引用賦予各個對象。 一個訂單管理組件須要信用卡認證組件,但它不須要本身建立信用卡認證組件,容器會主動賦予它一我的在組件。Spirng自動知足bean之間的依賴
@MapperScan:自動掃描mappers,將其關聯到SqlSessionTemplate,並將mappers註冊到spring容器中,以便注入到咱們的beans中。
/** * MybatisPlus配置 */
@Configuration
@EnableTransactionManagement(order = 2)//因爲引入多數據源,因此讓spring事務的aop要在多數據源切換aop的後面
@MapperScan(basePackages = {"com.guo.guns.modular.*.dao", "com.guo.guns.common.persistence.dao"})
public class MybatisPlusConfig {
@Autowired
DruidProperties druidProperties;
@Autowired
MutiDataSourceProperties mutiDataSourceProperties;
/** * 另外一個數據源 */
private DruidDataSource bizDataSource() {
DruidDataSource dataSource = new DruidDataSource();
druidProperties.config(dataSource);
mutiDataSourceProperties.config(dataSource);
return dataSource;
}
//省略單數據源和guns數據源
/** * 多數據源鏈接池配置 */
@Bean
@ConditionalOnProperty(prefix = "guns", name = "muti-datasource-open", havingValue = "true")
public DynamicDataSource mutiDataSource() {
DruidDataSource dataSourceGuns = dataSourceGuns();
DruidDataSource bizDataSource = bizDataSource();
try {
dataSourceGuns.init(); //重點
bizDataSource.init();
}catch (SQLException sql){
sql.printStackTrace();
}
DynamicDataSource dynamicDataSource = new DynamicDataSource();
HashMap<Object, Object> hashMap = new HashMap(); //這裏使用了HashMap
hashMap.put(DSEnum.DATA_SOURCE_GUNS, dataSourceGuns);
hashMap.put(DSEnum.DATA_SOURCE_BIZ, bizDataSource);
dynamicDataSource.setTargetDataSources(hashMap);
dynamicDataSource.setDefaultTargetDataSource(dataSourceGuns);
return dynamicDataSource;
}
-----------------------------待會說--------------------------------------------
/** * 數據範圍mybatis插件 */
@Bean
public DataScopeInterceptor dataScopeInterceptor() {
return new DataScopeInterceptor();
}
}
複製代碼
看代碼可讓問題變得更簡單,
攔截器的一個做用就是咱們能夠攔截某些方法的調用,咱們能夠選擇在這些被攔截的方法執行先後加上某些邏輯,也能夠在執行這些被攔截的方法時執行本身的邏輯而再也不執行被攔截的方法。
原諒我沒看懂。
/** * 數據範圍 */
public class DataScope {
/** * 限制範圍的字段名稱 */
private String scopeName = "deptid";
/** * 限制範圍的 */
private List<Integer> deptIds;
//......
}
--------------------------------------------------------------------------------
/** * 數據範圍的攔截器 */
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DataScopeInterceptor implements Interceptor {
/** * 得到真正的處理對象,可能多層代理. */
public static Object realTarget(Object target) {
if (Proxy.isProxyClass(target.getClass())) {
MetaObject metaObject = SystemMetaObject.forObject(target);
return realTarget(metaObject.getValue("h.target"));
}
return target;
}
//省略一大堆,回來在縷縷。
}
複製代碼
數據部分就算配置完成了,接下來就是重要的日誌部分。這個很重要,可具體記錄哪一個用戶,執行了哪些業務,修改了哪些數據,而且日誌記錄爲異步執行,也是基於JavaConfig.
老樣子,先看工廠
/** * 日誌對象建立工廠 */
public class LogFactory {
/** * 建立操做日誌 */
public static OperationLog createOperationLog(LogType logType, Integer userId, String bussinessName, String clazzName, String methodName, String msg, LogSucceed succeed) {
OperationLog operationLog = new OperationLog();
operationLog.setLogtype(logType.getMessage());
operationLog.setLogname(bussinessName);
operationLog.setUserid(userId);
operationLog.setClassname(clazzName);
operationLog.setMethod(methodName);
operationLog.setCreatetime(new Date());
operationLog.setSucceed(succeed.getMessage());
operationLog.setMessage(msg);
return operationLog;
}
//登陸日誌省略
}
---------------------------------------------------------------------------------
Timer是一種定時器工具,用來在一個後臺線程計劃執行指定任務。它能夠計劃執行一個任務一次或反覆屢次。
TimerTask一個抽象類,它的子類表明一個能夠被Timer計劃的任務。
/** * 日誌操做任務建立工廠 * * @author fengshuonan * @date 2016年12月6日 下午9:18:27 */
public class LogTaskFactory {
private static Logger logger = LoggerFactory.getLogger(LogManager.class);
private static LoginLogMapper loginLogMapper = SpringContextHolder.getBean(LoginLogMapper.class);
private static OperationLogMapper operationLogMapper = SpringContextHolder.getBean(OperationLogMapper.class);
public static TimerTask loginLog(final Integer userId, final String ip) {
return new TimerTask() {
@Override
public void run() {
try {
LoginLog loginLog = LogFactory.createLoginLog(LogType.LOGIN, userId, null, ip);
loginLogMapper.insert(loginLog);
} catch (Exception e) {
logger.error("建立登陸日誌異常!", e);
}
}
};
}
//省略不少,慢慢研究代碼。
}
複製代碼
日誌管理器
public class LogManager {
//日誌記錄操做延時
private final int OPERATE_DELAY_TIME = 10;
//異步操做記錄日誌的線程池
private ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(10);
private LogManager() {
}
public static LogManager logManager = new LogManager();
public static LogManager me() {
return logManager;
}
public void executeLog(TimerTask task) {
executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
}
}
--------------------------------------------------------------------------------
/** * 被修改的bean臨時存放的地方 */
@Component
@Scope(scopeName = WebApplicationContext.SCOPE_SESSION)
public class LogObjectHolder implements Serializable{
private Object object = null;
public void set(Object obj) {
this.object = obj;
}
public Object get() {
return object;
}
//這個方法是重點。
public static LogObjectHolder me(){
LogObjectHolder bean = SpringContextHolder.getBean(LogObjectHolder.class);
return bean;
}
}
------------------------註解----------------------------------------------------
/** * 標記須要作業務日誌的方法 */
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface BussinessLog {
/** * 業務的名稱,例如:"修改菜單" */
String value() default "";
/** * 被修改的實體的惟一標識,例如:菜單實體的惟一標識爲"id" */
String key() default "id";
/** * 字典(用於查找key的中文名稱和字段的中文名稱) */
String dict() default "SystemDict";
}
複製代碼
這是一個切面,
@Aspect
@Component
public class LogAop {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Pointcut(value = "@annotation(com.guo.guns.common.annotion.log.BussinessLog)")
public void cutService() {
}
@Around("cutService()")
public Object recordSysLog(ProceedingJoinPoint point) throws Throwable {
//先執行業務
Object result = point.proceed();
try {
handle(point);
} catch (Exception e) {
log.error("日誌記錄出錯!", e);
}
return result;
}
private void handle(ProceedingJoinPoint point) throws Exception {
//獲取攔截的方法名
Signature sig = point.getSignature();
MethodSignature msig = null;
if (!(sig instanceof MethodSignature)) {
throw new IllegalArgumentException("該註解只能用於方法");
}
msig = (MethodSignature) sig;
Object target = point.getTarget();
Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
String methodName = currentMethod.getName();
//若是當前用戶未登陸,不作日誌
ShiroUser user = ShiroKit.getUser();
if (null == user) {
return;
}
//獲取攔截方法的參數
String className = point.getTarget().getClass().getName();
Object[] params = point.getArgs();
//獲取操做名稱
BussinessLog annotation = currentMethod.getAnnotation(BussinessLog.class);
String bussinessName = annotation.value();
String key = annotation.key();
String dictClass = annotation.dict();
StringBuilder sb = new StringBuilder();
for (Object param : params) {
sb.append(param);
sb.append(" & ");
}
//若是涉及到修改,比對變化
String msg;
if (bussinessName.indexOf("修改") != -1 || bussinessName.indexOf("編輯") != -1) {
Object obj1 = LogObjectHolder.me().get();
Map<String, String> obj2 = HttpKit.getRequestParameters();
msg = Contrast.contrastObj(dictClass, key, obj1, obj2);
} else {
Map<String, String> parameters = HttpKit.getRequestParameters();
AbstractDictMap dictMap = DictMapFactory.createDictMap(dictClass);
msg = Contrast.parseMutiKey(dictMap,key,parameters);
}
LogManager.me().executeLog(LogTaskFactory.bussinessLog(user.getId(), bussinessName, className, methodName, msg));
}
}
複製代碼
業務邏輯還需好好研究下。這裏只是走一個過程,用的時候內心有個印象。真的好想把做者的名字都貼上去,可是地方不容許。這裏感謝要abel533和 stylefeng,像大佬學習。
今晚就先到這裏吧,下一個是ehcache,前臺的jd插件和beet模板引擎留到最後看。gogogo。