若依分离版官方文档
写在前面:下面每一个功能后面写的(如/captchaImage、/login)都是实现该功能的核心方法或者映射路径,使用 Ctrl + Shift +F 全局查找,找到这些核心代码然后去debug。
/captchaImage、/login
// 进行登录校验的核心方法:AuthenticationManager.authenticate()
// 调用链
AuthenticationManager.authenticate() --> ProviderManager.authenticate()中的provider.authenticate()方法 --> AbstractUserDetailsAuthenticationProvider.authenticate()
// 最后的authenticate()方法中,先是调用了retrieveUser()方法,通过UserDetailsService.loadUserByUsername(username) 拿到数据库中查到的用户, 然后调用additionalAuthenticationChecks()方法进行了密码校验,校验成功就构造一个认证过的 UsernamePasswordAuthenticationToken 对象放入 SecurityContext。
// 我们只需要重写UserDetailsService的loadUserByUsername(username)方法即可,其它部分都是Security自己实现。
startPage()
public static void startPage()
{
PageDomain pageDomain = TableSupport.buildPageRequest();
Integer pageNum = pageDomain.getPageNum();
Integer pageSize = pageDomain.getPageSize();
if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize))
{
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
Boolean reasonable = pageDomain.getReasonable();
// 实际上还是调用了插件PageHelper.startPage()方法,只不过是把一些分页参数给封装到了一个实体类里
PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
}
}
@Excel 、/importTemplate、/importData、/export (反射实现)
// importData
// 定义一个map key:excel表中列的字段名 value:列序号
Map<String, Integer> cellMap = new HashMap<String, Integer>();
// ...
// 通过反射获取到实体类中所有带有@Excel注解的字段(有@Excel注解说明需要导入导出)
List<Object[]> fields = this.getFields(); //Object[0]:Field Object[1]:Excel注解对象
// ...
// 定义一个map key:excel表中的列序号 value:上面获得的field (建立对应关系)
Map<Integer, Object[]> fieldsMap = new HashMap<Integer, Object[]>();
// ...
// 然后根据fieldsMap的对应关系,把表中数据赋值给相应实体类
// export和importTemplate(下载模板)逻辑相同,只不过export要导出从数据库中查询到的数据,而importTemplate只用导出一个表头,不需要数据。
/upload
主要是前端实现,没什么好讲的。
@PreAuthorize、hasPermi()…
@PreAuthorize("@ss.hasPermi('system:dept:list')")
@GetMapping("/list")
public AjaxResult list(SysDept dept){...}
@Service("ss")
public class PermissionService{
public boolean hasPermi(String permission){...}
}
// 对于上面的例子,当有system:dept:list这个资源权限时,才会执行下面的list方法,那么,怎么判断有没有权限呢?
// @ss.hasPermi('system:dept:list') -> 找有@Service("ss")注解的类中的hasPermi()方法,并把字符串'system:dept:list'作为参数传给它,如果这个方法返回true(比如去数据库中获取当前用户的权限信息,判断是否有该权限),则有权限执行list方法。
// 同理,也可以用hasAnyPermi、hasRole...
@Transactional
直接使用该注解就行了,没什么好讲的,注解用法和注意事项可以看官方文档。
@RestControllerAdvice
对于项目中出现的异常(如权限异常、业务异常、登录异常等)进行统一拦截并处理,简化业务代码。全局异常处理器就是使用@ControllerAdvice注解,当返回给前端的是一个json对象时(Ajax),可以直接使用@RestControllerAdvice注解,代替@ControllerAdvice和@ResponseBody。
// 1.定义统一返回实体类AjaxResult,所有的异常信息都是赋值给该类然后返回给前端。
public class AjaxResult extends HashMap<String, Object>{
private static final long serialVersionUID = 1L;
/**
* @param code 错误码
* @param msg 内容
* @return 错误消息
*/
public static AjaxResult error(String msg){
AjaxResult json = new AjaxResult();
json.put("msg", msg);
json.put("code", 500);
return json;
}
/**
* @param msg 内容
* @return 成功消息
*/
public static AjaxResult success(String msg){
AjaxResult json = new AjaxResult();
json.put("msg", msg);
json.put("code", 0);
return json;
}
}
// 2.自定义一个异常类,如登录异常,继承运行时异常类
public class LoginException extends RuntimeException{
private static final long serialVersionUID = 1L;
protected final String message;
public LoginException(String message){
this.message = message;
}
@Override
public String getMessage(){
return message;
}
}
// 3.定义全局异常处理器(所有异常都可以放到该类中进行处理然后返回给前端)
@RestControllerAdvice
public class GlobalExceptionHandler{
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(LoginException.class)
public AjaxResult loginException(LoginException e){
log.error(e.getMessage(), e); // 打印异常消息日志
return AjaxResult.error(e.getMessage()); // 把异常消息赋值给AjaxResult类,然后返回给前端
}
}
// 4.测试
@Controller
public class SysIndexController {
@GetMapping("/index")
public String index(ModelMap mmap){
SysUser user = ShiroUtils.getSysUser();
if (StringUtils.isNull(user)){
// 模拟用户未登录,抛出业务逻辑异常
throw new LoginException("用户未登录,无法访问请求。");
}
mmap.put("user", user);
return "index";
}
}
// 5.前端接收结果
{
"msg": "用户未登录,无法访问请求。",
"code": 500
}
登录日志AsyncFactory.recordLogininfor() 操作日志@Log、LogAspect类
// 登录日志 login()方法中进行
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
// AsyncFactory.recordLogininfor() 定义一个定时任务,用来执行记录日志的工作,外层的execute()方法执行
// 操作日志 AOP实现
// 1.自定义@Log注解
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log{
public String title() default ""; // 模块
public BusinessType businessType() default BusinessType.OTHER; // 功能
public OperatorType operatorType() default OperatorType.MANAGE; // 操作人类别
public boolean isSaveRequestData() default true; // 是否保存请求的参数
public boolean isSaveResponseData() default true; // 是否保存响应的参数
}
// 2.使用@Log注解
@Log(title = "测试方法", businessType = BusinessType.INSERT)
public void save(){...}
// 3.定义切面类LogAspect,在该类中实现功能扩展(即打印日志)
@Aspect
@Component
public class LogAspect{
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
// pointcut属性指明切入点方法
// 属性值@annotation(controllerLog)和参数Log controllerLog结合起来看 -> 切入点为标有@Log注解的方法
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult){
handleLog(joinPoint, controllerLog, null, jsonResult);
}
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult){
// 在该方法中实现了日志数据的处理
}
}
@DataScope、DataScopeAspect类、${params.dataScope}
该功能是为了控制每个职位能查看的数据范围,如机密数据不能被普通员工看到。这个功能的实现比较有技巧!
核心思想:对xml中sql查询语句进行拼接处理。
实现步骤:
// 1.定义一个@DataScope注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope{
public String deptAlias() default ""; // 部门表的别名(从数据库中查询到的部门表起别名为d)
public String userAlias() default ""; // 用户表的别名(从数据库中查询到的用户表起别名为u)
}
// 2.要进行数据限制的实体类继承BaseEntity类,该类中有params属性,在xml中通过${params.dataScope}获取要拼接的语句
public class BaseEntity implements Serializable{
// ...
private Map<String, Object> params;
// ...
}
public class SysUser extends BaseEntity{...}
public class SysDept extends BaseEntity{...}
// 3.在业务层使用@DataScope注解
@DataScope(deptAlias = "d", userAlias = "u") // 部门和用户都有权限限制
public List<SysUser> selectUserList(SysUser user){
return userMapper.selectUserList(user); // 在查数据时使用,限制我们查到的数据范围
}
// 4.定义切面类DataScope
@Aspect
@Component
public class DataScopeAspect{
// 定义一些数据权限范围
public static final String DATA_SCOPE_ALL = "1";
// ...
// 在标有注解@DataScope的方法执行之前执行(我们要先设置查询数据范围,然后再在业务层中查询数据)
@Before("@annotation(controllerDataScope)")
public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable{
clearDataScope(point); // 拼接权限sql前先清空params.dataScope参数防止注入
handleDataScope(point, controllerDataScope); // 设置该用户的数据权限
}
protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope){
// 对数据权限进行初步过滤
dataScopeFilter(...);
}
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias){
// 在该方法中获取到该用户可以查看的数据范围,然后设计成一个字符串,赋值给params参数
getParams().put("dataScope", 要拼接的sql语句);
}
private void clearDataScope(final JoinPoint joinPoint){
// 拼接权限sql前先清空params.dataScope参数防止注入
}
}
// 5.xml文件中拼接
<select id="selectAllocatedList" parameterType="SysUser" resultMap="SysUserResult">
select ...
from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
where u.del_flag = '0'
<!-- 数据范围过滤 -->
${params.dataScope}
</select>
@DataSource、DruidConfig类、DynamicDataSource类、DataSourceAspect类
该功能可以直接使用,固定格式(也可以自己多加几个数据源)
通过AOP获得标有@DataSouce注解的方法是要用主库还是从库,然后设置到动态数据源的threadLocal中去,由动态数据源来实现数据库的切换
// 1.定义一个@DataSource注解,方法通过该注解选择数据源
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource{
public DataSourceType value() default DataSourceType.MASTER; // 要切换的数据源的名称
}
// 2.定义动态数据源
public class DynamicDataSource extends AbstractRoutingDataSource{
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources){
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey(){
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
// 3.定义数据源配置类DruidConfig
@Configuration
public class DruidConfig
{
// 配置主数据源 从配置文件中拿到配置信息
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource(DruidProperties druidProperties){
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
// 配置从数据源 从配置文件中拿到配置信息
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
@ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
public DataSource slaveDataSource(DruidProperties druidProperties){
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean(name = "dynamicDataSource")
@Primary // 基于name注入bean的时候,会有三个数据源bean,使用@Primary注解说明优先使用该bean
public DynamicDataSource dataSource(DataSource masterDataSource){
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
return new DynamicDataSource(masterDataSource, targetDataSources);
}
/**
* 设置数据源
*
* @param targetDataSources 备选数据源集合
* @param sourceName 数据源名称
* @param beanName bean名称
*/
public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName){
try{
DataSource dataSource = SpringUtils.getBean(beanName);
targetDataSources.put(sourceName, dataSource);
}
catch (Exception e){}
}
}
// 4.定义切面类DataSourceAspect
@Aspect
@Order(1)
@Component
public class DataSourceAspect{
protected Logger logger = LoggerFactory.getLogger(getClass());
@Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)"
+ "|| @within(com.ruoyi.common.annotation.DataSource)")
public void dsPointCut(){}
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable{
DataSource dataSource = getDataSource(point);
if (StringUtils.isNotNull(dataSource)){
DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
}
try{
return point.proceed();
}finally{
// 销毁数据源 在执行方法之后
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
public DataSource getDataSource(ProceedingJoinPoint point){
MethodSignature signature = (MethodSignature) point.getSignature();
DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
if (Objects.nonNull(dataSource)){
return dataSource;
}
return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
}
}
// 5.在业务层使用@DataSouce来切换数据源
@Override
@DataSource(DataSourceType.MASTER)
public SysConfig selectConfigById(Long configId){
SysConfig config = new SysConfig();
config.setConfigId(configId);
return configMapper.selectConfig(config);
}
SysJobController类
对于定时任务的增删改查就是一些基本操作,该功能的使用看官方文档按照文档步骤实验一下即可,底层是调用了org.quartz的Scheduler接口方法实现,也是固定方法。