若是您有幸能看到,请认阅读如下内容;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。