若依项目分离版 后端部分讲解

RuoYi-Vue版:后端部分

若依分离版官方文档

​ 写在前面:下面每一个功能后面写的(如/captchaImage、/login)都是实现该功能的核心方法或者映射路径,使用 Ctrl + Shift +F 全局查找,找到这些核心代码然后去debug。

1. 登录逻辑(含验证码)

​ /captchaImage、/login

// 进行登录校验的核心方法:AuthenticationManager.authenticate()
// 调用链
AuthenticationManager.authenticate()  --> ProviderManager.authenticate()中的provider.authenticate()方法 --> AbstractUserDetailsAuthenticationProvider.authenticate()
// 最后的authenticate()方法中,先是调用了retrieveUser()方法,通过UserDetailsService.loadUserByUsername(username) 拿到数据库中查到的用户, 然后调用additionalAuthenticationChecks()方法进行了密码校验,校验成功就构造一个认证过的 UsernamePasswordAuthenticationToken 对象放入 SecurityContext。
// 我们只需要重写UserDetailsService的loadUserByUsername(username)方法即可,其它部分都是Security自己实现。

2. 分页

​ 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);
        }
    }

3. 导入、导出

​ @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只用导出一个表头,不需要数据。

4. 上传、下载

​ /upload

​ 主要是前端实现,没什么好讲的。

5. 权限管理

​ @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...

6. 事务管理

​ @Transactional

​ 直接使用该注解就行了,没什么好讲的,注解用法和注意事项可以看官方文档。

7. 全局异常处理

​ @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
}

8. 日志管理

​ 登录日志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){
        // 在该方法中实现了日志数据的处理
    }
}

9. 数据权限

​ @DataScope、DataScopeAspect类、${params.dataScope}

​ 该功能是为了控制每个职位能查看的数据范围,如机密数据不能被普通员工看到。这个功能的实现比较有技巧!

​ 核心思想:对xml中sql查询语句进行拼接处理。

若依项目分离版 后端部分讲解_第1张图片

​ 实现步骤:

// 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>

10. 多数据源

​ @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);
}

11. 定时任务

​ SysJobController类

​ 对于定时任务的增删改查就是一些基本操作,该功能的使用看官方文档按照文档步骤实验一下即可,底层是调用了org.quartz的Scheduler接口方法实现,也是固定方法。

你可能感兴趣的:(Java,java,后端)