权限管理个人笔记之后端实现(二)
集成Druid数据源
首先添加依赖:
com.alibaba
druid-spring-boot-starter
1.1.10
添加配置:
application.yml
spring:
datasource:
name: druidDataSource
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/qxgl?useUnicode=true&zeroDateBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
username: root
password: root
#配置监控统计拦截的filters,去掉后监控界面SQL无法进行统计,wall用于防火墙
filters: stat,wall,log4j,config
#最大连接数
max-active: 100
#初始化大小
initial-size: 1
#获取连接等待超时时间
max-wait: 60000
#最小连接数
min-idle: 1
#间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 60000
#一个连接在连接池中最小的生存时间,单位是毫秒
min-evictable-idle-time-millis: 300000
validation-query: select 'x'
test-while-idle: true
test-on-return: false
test-on-borrow: false
pool-prepared-statements: true
max-open-prepared-statements: 50
max-pool-prepared-statement-per-connection-size: 20
DruidDataSourceProperties.java
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.datasource.druid")
public class DruidDataSourceProperties {
//jdbc
private String driverClassName;
private String url;
private String username;
private String password;
//jdbc connection pool
private int initialSize;
private int minIdle;
private int maxActive = 100;
private long maxWait;
private long timeBetweenEvictionRunsMillis;
private long MinEvictableIdleTimeMillis;
private String ValidationQuery;
private boolean testWhileIdle;
private boolean testOnReturn;
private boolean testOnBorrow;
private boolean poolPreparedStatements;
private int maxPoolPreparedStatementPerConnectionSize;
//filter
private String filters;
public long getMinEvictableIdleTimeMillis() {
return MinEvictableIdleTimeMillis;
}
public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) {
MinEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
}
public boolean isTestOnBorrow() {
return testOnBorrow;
}
public void setTestOnBorrow(boolean testOnBorrow) {
this.testOnBorrow = testOnBorrow;
}
public String getDriverClassName() {
return driverClassName;
}
public void setDriverClassName(String driverClassName) {
this.driverClassName = driverClassName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
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 int getInitialSize() {
return initialSize;
}
public void setInitialSize(int initialSize) {
this.initialSize = initialSize;
}
public int getMinIdle() {
return minIdle;
}
public void setMinIdle(int minIdle) {
this.minIdle = minIdle;
}
public int getMaxActive() {
return maxActive;
}
public void setMaxActive(int maxActive) {
this.maxActive = maxActive;
}
public long getMaxWait() {
return maxWait;
}
public void setMaxWait(long maxWait) {
this.maxWait = maxWait;
}
public long getTimeBetweenEvictionRunsMillis() {
return timeBetweenEvictionRunsMillis;
}
public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) {
this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
}
public String getValidationQuery() {
return ValidationQuery;
}
public void setValidationQuery(String validationQuery) {
ValidationQuery = validationQuery;
}
public boolean isTestWhileIdle() {
return testWhileIdle;
}
public void setTestWhileIdle(boolean testWhileIdle) {
this.testWhileIdle = testWhileIdle;
}
public boolean isTestOnReturn() {
return testOnReturn;
}
public void setTestOnReturn(boolean testOnReturn) {
this.testOnReturn = testOnReturn;
}
public boolean isPoolPreparedStatements() {
return poolPreparedStatements;
}
public void setPoolPreparedStatements(boolean poolPreparedStatements) {
this.poolPreparedStatements = poolPreparedStatements;
}
public int getMaxPoolPreparedStatementPerConnectionSize() {
return maxPoolPreparedStatementPerConnectionSize;
}
public void setMaxPoolPreparedStatementPerConnectionSize(int maxPoolPreparedStatementPerConnectionSize) {
this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize;
}
public String getFilters() {
return filters;
}
public void setFilters(String filters) {
this.filters = filters;
}
}
配置Servlet和Filter
在config包下新建一个DruidConfig配置类,主要是注入属性和配置连接池相关的配置。如:黑名单白名单、监控管理后台登录账号密码等:
DruidConfig.java
@Configuration
@EnableConfigurationProperties({DruidDataSourceProperties.class})
public class DruidConfig {
@Autowired
private DruidDataSourceProperties properties;
@Bean
@ConditionalOnMissingBean
public DataSource druidDataSource(){
//中间省略属性设置
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(properties.getDriverClassName());
druidDataSource.setUrl(properties.getUrl());
druidDataSource.setUsername(properties.getUsername());
druidDataSource.setPassword(properties.getPassword());
druidDataSource.setInitialSize(properties.getInitialSize());
druidDataSource.setMinIdle(properties.getMinIdle());
druidDataSource.setMaxActive(properties.getMaxActive());
druidDataSource.setMaxWait(properties.getMaxWait());
druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
druidDataSource.setValidationQuery(properties.getValidationQuery());
druidDataSource.setTestWhileIdle(properties.isTestWhileIdle());
druidDataSource.setTestOnBorrow(properties.isTestOnBorrow());
druidDataSource.setTestOnReturn(properties.isTestOnReturn());
druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements());
druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(properties.getMaxPoolPreparedStatementPerConnectionSize());
try {
druidDataSource.setFilters(properties.getFilters());
druidDataSource.init();
} catch (SQLException e) {
e.printStackTrace();
}
return druidDataSource;
}
/*
* 注册Servlet信息,配置监控视图
* */
@Bean
@ConditionalOnMissingBean
public ServletRegistrationBeandruidServlet(){
ServletRegistrationBeanservletServletRegistrationBean=new
ServletRegistrationBean(new StatViewServlet(),"/druid/*");
//白名单
servletServletRegistrationBean.addInitParameter("allow","127.0.0.1,139.196.87.48");
//IP黑名单(如果共存时,deny优先于allow)
//如果满足deny的话提示:Sprry,you are not permitted to view this page.
servletServletRegistrationBean.addInitParameter("deny","192.168.1.119");
//登录查看信息的账号密码,用于登录Druid监控后台
servletServletRegistrationBean.addInitParameter("loginUsername","admin");
servletServletRegistrationBean.addInitParameter("loginPassword","admin");
//是否能够重置数据
servletServletRegistrationBean.addInitParameter("resetEnable","true");
return servletServletRegistrationBean;
}
/*注册Filter信息,监控拦截器*/
@Bean
@ConditionalOnMissingBean
public FilterRegistrationBeanfilterFilterRegistrationBean(){
//省略具体注册逻辑
FilterRegistrationBeanfilterRegistrationBean = new FilterRegistrationBean ();
filterRegistrationBean.setFilter(new WebStatFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
编译启动会报一个错,没有log4j的依赖
1.首先在pom.xml添加log4j依赖
log4j
log4j
1.2.17
2.添加log4j配置
在resource目录下,新建一个log4j参数文件
log4j.properties
### set log levels ###
log4j.rootLogger = INFO,DEBUG, console, infoFile, errorFile ,debugfile,mail
LocationInfo=true
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern =[%d{yyyy-MM-dd HH:mm:ss,SSS}]-[%p]:%m %x %n
log4j.appender.infoFile = org.apache.log4j.DailyRollingFileAppender
log4j.appender.infoFile.Threshold = INFO
log4j.appender.infoFile.File = C:/logs/log
log4j.appender.infoFile.DatePattern = '.'yyyy-MM-dd'.log'
log4j.appender.infoFile.Append=true
log4j.appender.infoFile.layout = org.apache.log4j.PatternLayout
log4j.appender.infoFile.layout.ConversionPattern =[%d{yyyy-MM-dd HH:mm:ss,SSS}]-[%p]:%m%x %n
log4j.appender.errorFile = org.apache.log4j.DailyRollingFileAppender
log4j.appender.errorFile.Threshold = ERROR
log4j.appender.errorFile.File = C:/logs/error
log4j.appender.errorFile.DatePattern = '.'yyyy-MM-dd'.log'
log4j.appender.errorFile.Append=true
log4j.appender.errorFile.layout = org.apache.log4j.PatternLayout
log4j.appender.errorFile.layout.ConversionPattern =[%d{yyyy-MM-dd HH:mm:ss,SSS}]-[%p]:%m%x %n
log4j.appender.debugfile = org.apache.log4j.DailyRollingFileAppender
log4j.appender.debugfile.Threshold = DEBUG
log4j.appender.debugfile.File = C:/logs/debug
log4j.appender.debugfile.DatePattern = '.'yyyy-MM-dd'.log'
log4j.appender.debugfile.Append=true
log4j.appender.debugfile.layout = org.apache.log4j.PatternLayout
log4j.appender.debugfile.layout.ConversionPattern =[%d{yyyy-MM-dd HH:mm:ss,SSS}]-[%p]:%m%x %n
查看监控
启动成功之后http://localhost:8081/druid/login.html
里面有监控首页:用户名是Config配置的登陆账号和密码,还有数据源,SQL监控。SQL监控在接口调用成功之后,可以看见SQL监控的执行记录,可以查看和分析的SQL性能,方便进行数据库性能优化。
跨域解决方案
CORS实现:在config包下新建一个CORS配置类,实现WebMvcConfigurer接口
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry){
registry.addMapping("/**") //运行跨域访问的路径
.allowedOrigins("*")//运行跨域访问的源
.allowedMethods("POST","GET","PUT","OPTIONS","DELETE")//运行请求方法
.maxAge(168000)//预检间隔时间
.allowedHeaders("*")//允许头部设置
.allowCredentials(true);//是否发送cookie
}
}
业务功能实现
因为我们要采用的是微服务的架构,虽然现在我们只有一个工程,但是随着项目越来越大,代码的可重用性和可维护性就会变得越来越难,所以尽早对工程结构进行合理的规划,对项目前期开发和后期维护都是很重要的。
1.mango-admin 后台管理模块,包含用户、角色、菜单管理等
首先将原来的项目更名为mango-admin
1.先删除mango工程,不要勾选删除的。
2.找到文件的位置,修改工程名
3.编辑pom.xml将需要替换的mango字符串替换为mango-admin
4. 在把mango-admin放入父项目中
5.重构包结构,将基础包重构成:
多一个.admin以区分不同工程的包
6.重构包之后XML文件要是没有更新就会报错,把mapper和Model的包路径修改成正确的路径
7.将启动器加Admin用于区分
mango-common 公共代码模块,主要放置一些工具类
新建一个空的maven工程,除了pom.xnl,没有其他内容
mango-core 核心业务代码模块,主要封装公共业务模块
新建一个空的maven工程,后续放一些公共核心业务代码封装,如HTTP交互格式封装,业务基类封装和分页工具封装等
mango-pom 聚合模块,仅简化打包,一键执行打包所有模块
为了方便统一打包,新建一个mango-pom工程,这个工程依赖所有模块,负责统一进行打包(不然编译的时候需要每一个都进行编译,工程一多很麻烦),但我们采用的是微服务架构,每一个工程模块使用的依赖版本可能都是不一样的,所以mango-pom
与所有的模块不存在实质性的父子模块,也不由mango-pom统一进行版本和依赖管理方便打包。
打包测试:
因为我们每一个模块间的依赖还没有加,所以第一次还需要遵循以下步骤进行操作:
现在mango-core下的pom添加依赖:
在mango-admin下的pom.xml添加mango-core的dependency依赖
最后在mango-pom下pom.xml添加以上的所有模块为modules依赖然后执行打包命令
pom.xml
../mango-admin
../mango-common
../mango-core
业务代码封装
为了统一业务代码接口,保持代码整洁、提升代码性能,这里对一些通用的代码进行一些封装
通用CURD接口:
curdService是对通用增删改查接口的封装,统一定义包含保存、删除、批量删除、根据ID查询和分页查询的方法,一般的业务Service接口会继承此接口,提供基础增、删、改、查服务、
这几个接口能满足大部分基础CURD业务的需求。
在mango-core下建包名如:
内容目录:
在service中新建CurdService
/*通用CURD接口*/
public interface CurdService{
/*
* 保存操作
* */
int save(T record);
/*
* /删除操作
* */
int delete(T record);
/*
* 批量删除操作
* */
int delete(Listrecord);
/*根据id查询*/
T findById(Long id);
/**
* 分页查询
* 这里统一封装了分页请求和结果,避免直接引入具体框架的分页对象, 如MyBatis或JPA的分页对象
* 从而避免因为替换ORM框架而导致服务层、控制层的分页接口也需要变动的情况,替换ORM框架也不会
* 影响服务层以上的分页接口,起到了解耦的作用
* @param pageRequest 自定义,统一分页查询请求
* @return PageResult 自定义,统一分页查询结果
*/
PageResult findPage(PageRequest pageRequest);
}
分页请求封装:
对分页请求的参数进行统一的封装,传入分页查询的页码和数量即可。
pageRequest.java
/**
* 分页请求
* @author Louis
* @date Jan 12, 2019
*/
public class PageRequest {
/**
* 当前页码
*/
private int pageNum = 1;
/**
* 每页数量
*/
private int pageSize = 10;
/**
* 查询参数
*/
private Mapparams = new HashMap<>();
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public MapgetParams() {
return params;
}
public void setParams(Mapparams) {
this.params = params;
}
public Object getParam(String key) {
return getParams().get(key);
}
}
分页结果封装
对分页查询的结果进行了统一封装,结果返回业务数据和分页数据。
PageResult.java
/**
* 分页返回结果
* @author Louis
* @date Jan 12, 2019
*/
public class PageResult {
/**
* 当前页码
*/
private int pageNum;
/**
* 每页数量
*/
private int pageSize;
/**
* 记录总数
*/
private long totalSize;
/**
* 页码总数
*/
private int totalPages;
/**
* 分页数据
*/
private List<?> content;
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public long getTotalSize() {
return totalSize;
}
public void setTotalSize(long totalSize) {
this.totalSize = totalSize;
}
public int getTotalPages() {
return totalPages;
}
public void setTotalPages(int totalPages) {
this.totalPages = totalPages;
}
public List<?> getContent() {
return content;
}
public void setContent(List<?> content) {
this.content = content;
}
}
分页助手封装
对MyBatis的分页查询业务代码进行了统一封装,通过分页助手可以极大简化Service查询业务的编写。
MybatisPageHelper.java
/*
* MyBatis 分页查询助手
*/
public class MybatisPageHelper {
public static final String findPage = "findPage";
/**
* 分页查询, 约定查询方法名为 “findPage”
* @param pageRequest 分页请求
* @param mapper Dao对象,MyBatis的 Mapper
* @return*/
public static PageResult findPage(PageRequest pageRequest, Object mapper) {
return findPage(pageRequest, mapper, findPage);
}
/*
*
* 调用分页插件进行分页查询
* @param pageRequest 分页请求
* @param mapper Dao对象,MyBatis的 Mapper
* @param queryMethodName 要分页的查询方法名
* @param args 方法参数
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static PageResult findPage(PageRequest pageRequest, Object mapper, String queryMethodName, Object... args) {
// 设置分页参数
int pageNum = pageRequest.getPageNum();
int pageSize = pageRequest.getPageSize();
PageHelper.startPage(pageNum, pageSize);
// 利用反射调用查询方法
Object result = ReflectionUtils.invoke(mapper, queryMethodName, args);
return getPageResult(pageRequest, new PageInfo((List) result));
}
/*
*
* 将分页信息封装到统一的接口
* @param pageRequest
* @param page
* @return
*/
private static PageResult getPageResult(PageRequest pageRequest, PageInfo<?> pageInfo) {
PageResult pageResult = new PageResult();
pageResult.setPageNum(pageInfo.getPageNum());
pageResult.setPageSize(pageInfo.getPageSize());
pageResult.setTotalSize(pageInfo.getTotal());
pageResult.setTotalPages(pageInfo.getPages());
pageResult.setContent(pageInfo.getList());
return pageResult;
}
}
这里用到mango-common下的ReflectionUtils.java
/**
* 反射相关辅助方法
* @author Louis
* @date Aug 19, 2018
*/
public class ReflectionUtils {
/**
* 根据方法名调用指定对象的方法
* @param object 要调用方法的对象
* @param method 要调用的方法名
* @param args 参数对象数组
* @return
*/
public static Object invoke(Object object, String method, Object... args) {
Object result = null;
Class<? extends Object> clazz = object.getClass();
Method queryMethod = getMethod(clazz, method, args);
if(queryMethod != null) {
try {
result = queryMethod.invoke(object, args);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
} else {
try {
throw new NoSuchMethodException(clazz.getName() + " 类中没有找到 " + method + " 方法。");
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
return result;
}
/**
* 根据方法名和参数对象查找方法
* @param clazz
* @param name
* @param args 参数实例数据
* @return
*/
public static Method getMethod(Class<? extends Object> clazz, String name, Object[] args) {
Method queryMethod = null;
Method[] methods = clazz.getMethods();
for(Method method:methods) {
if(method.getName().equals(name)) {
Class<?>[] parameterTypes = method.getParameterTypes();
if(parameterTypes.length == args.length) {
boolean isSameMethod = true;
for(int i=0; iObject arg = args[i];
if(arg == null) {
arg = "";
}
if(!parameterTypes[i].equals(args[i].getClass())) {
isSameMethod = false;
}
}
if(isSameMethod) {
queryMethod = method;
break ;
}
}
}
}
return queryMethod;
}
}
下面是Http结果封装
对接口调用返回结果统一封装,方便前端或移动端对返回结果进行统一处理。
HttpResult.java
/**
* HTTP结果封装
* @author Louis
* @date Jan 12, 2019
*/
public class HttpResult {
private int code = 200;
private String msg;
private Object data;
public static HttpResult error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static HttpResult error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static HttpResult error(int code, String msg) {
HttpResult r = new HttpResult();
r.setCode(code);
r.setMsg(msg);
return r;
}
public static HttpResult ok(String msg) {
HttpResult r = new HttpResult();
r.setMsg(msg);
return r;
}
public static HttpResult ok(Object data) {
HttpResult r = new HttpResult();
r.setData(data);
return r;
}
public static HttpResult ok() {
return new HttpResult();
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
MyBatis分页查询
利用pagegelper分页插件帮助我们快速实现分页功能。
添加依赖:
在mango-core中pom.xml添加分页插件依赖包
pom.xml
com.github.pagehelper
pagehelper-spring-boot-starter
1.2.10
添加配置:
在mango-admin下添加配置
#pagehelper
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countSql
分页代码:
现在Dao层添加一个分页查询的方法
UserMapper.java中