写点什么

架构师第一课,一文带你玩转 ruoyi 架构

作者:小鲍侃java
  • 2022 年 8 月 12 日
    辽宁
  • 本文字数:17904 字

    阅读完需:约 59 分钟

架构师第一课,一文带你玩转ruoyi架构

一.若依是什么

1.1 什么是框架/架构

我理解的架构/框架应该有以下功能:

  1. 满足日常开发功能,如单点登陆、消息队列、监控等;

  2. 规范开发者的开发,指定代码格式、注释等;

  3. 提高开发效率,提供一系列的封装方法,并减少 bug 的产生率。


例如,开发企业级项目时,除了用户提供的特定业务外还有一些通用的功能,如权限、部门、公告、登陆用户等功能,同时也需要提供中间件的使用与配置,灵活的方法封装,(如获取用户,获取部门)与开发规范,如果没有使用框架的话,以上都需要自行扩展和开发。

1.2 若依做了什么

若依提供了一系列的基础业务模块、常用的封装方法,灵活可配置的中间件等开发基础。让开发者专注于业务开发,做到上手即用。

1.3 若依适合什么场景使用

适合小企业小体量、基础功能无定制化需求的项目,同时对于毕设、学习更是难得的好项目。


项目地址:http://www.ruoyi.vip

1.4 若依提供了那些基础功能

1.4.1 系统管理

用户管理:用户是系统操作者,该功能主要完成系统用户配置。

部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。

岗位管理:配置系统用户所属担任职务。

菜单管理:配置系统菜单,操作权限,按钮权限标识等。

角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。

字典管理:对系统中经常使用的一些较为固定的数据进行维护。

参数管理:对系统动态配置常用参数。

通知公告:系统通知公告信息发布维护。

1.4.2 日志管理

操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。

登录日志:系统登录日志记录查询包含登录异常。

1.4.3 系统监控

在线用户:当前系统中活跃用户状态监控。

服务监控:监视当前系统 CPU、内存、磁盘、堆栈等相关信息。

缓存监控:对系统的缓存查询,查看、清理等操作。

在线构建器:拖动表单元素生成相应的 HTML 代码。

连接池监视:监视当期系统数据库连接池状态,可进行分析 SQL 找出系统性能瓶颈。

1.4.4 系统工具

定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。

代码生成:前后端代码的生成(java、html、xml、sql)支持 CRUD 下载 。

系统接口:根据业务代码自动生成相关的 api 接口文档。

二.快速开始若依

2.1 前端搭建

前端直接安装 npm,然后启动即可,与开源 vue 使用相同。

npm installnpm run dev
复制代码


2.2 后端搭建

修改数据库与 redis 的地址即可完成初始配置。


出现下图表示,运行成功!

三.代码介绍

ruoyi 为聚合工程项目分为以上 6 个功能包。下文就详细的介绍各个包的功能。请各位看管跟着博主思路慢慢了解 ruoyi 的全部核心代码。

3.1 ruoyi-quartz 包

使用 quartz 调度框架开发,此处篇幅较长,请参考博主其他文章。

3.2 ruoyi-generator 包

该包为代码生成器,通过前端页面指定业务表、模块功能等信息,就可以生成前后端代码,且生成的代码风格统一,极大提高了开发效率,主要的流程如下。


  1. 编写 vm 文件,提前定义模板;

  2. 通过页面输入业务表、模块相关信息;

  3. 首先通过 sql 语句查询指定表的字段信息,并将查询出的字段信息存入表中;

  4. 遍历 vm 文件,将表信息与输入功能信息填入 vm 文件预留变量中;

  5. 将文件转为流,并存放于指定位置。

3.2.1 GenTableServiceImpl

GenTableServiceImpl/generatorCode 类为该包核心,velocity 框架将收集到的数据库字段与输入的模块信息填入模板变量中,并生成代码。

    @Override    public void generatorCode(String tableName)    {        // 查询表信息        GenTable table = genTableMapper.selectGenTableByName(tableName);        // 设置主子表信息        setSubTable(table);        // 设置主键列信息        setPkColumn(table);        VelocityInitializer.initVelocity();        VelocityContext context = VelocityUtils.prepareContext(table);        // 获取模板列表 也就是vm文件        List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory());        // 遍历模板文件        for (String template : templates)        {            if (!StringUtils.containsAny(template, "sql.vm", "api.js.vm", "index.vue.vm", "index-tree.vue.vm"))            {                // 渲染模板 将收集到的数据填入vm文件中                StringWriter sw = new StringWriter();                Template tpl = Velocity.getTemplate(template, Constants.UTF8);                tpl.merge(context, sw);                try                {                    //将生成的文件写入固定位置                    String path = getGenPath(table, template);                    FileUtils.writeStringToFile(new File(path), sw.toString(), CharsetKit.UTF_8);                }                catch (IOException e)                {                    throw new ServiceException("渲染模板失败,表名:" + table.getTableName());                }            }        }    }
复制代码

3.3 ruoyi-system 包


ruoyi 项目将 controller 与 service 部分隔离开来。该包为业务代码的 service 层,比较简单,代码请自行了解。

3.4 ruoyi-common 包

该包为项目的通用包,包括通用封装方法、自定义注解、枚举类等。统一将这些抽取出来更加方便开发者的使用,同时使写出的代码更加的统一,方便迭代与修改。

3.4.1 annotation 包

自定义注解,类使用注解后即可完成功能,使用 aop 作为自定义注解的逻辑。逻辑实现在 ruoyi-framework 中。

3.4.2 config 包

获取 application.yml 中的配置信息,并注入项目 bean 中,修改 yml 文件即可修改通用配置,实现配置统一管理。

3.4.3 constant 包

为项目提供常量池,统一管理常量,避免魔法值。

3.4.4 core 包

3.4.4.1 BaseController

所有接口层的基类。分页、排序、组装参数对于每一个 controller 都是必要的,所以为避免重复编写,封装在一基类中,业务 controller 继承该类即可调用。

public class BaseController{    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
/** * 设置请求分页数据 */ protected 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(pageNum, pageSize, orderBy).setReasonable(reasonable); } }
/** * 设置请求排序数据 */ protected void startOrderBy() { PageDomain pageDomain = TableSupport.buildPageRequest(); if (StringUtils.isNotEmpty(pageDomain.getOrderBy())) { String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); PageHelper.orderBy(orderBy); } }
/** * 响应请求分页数据 */ @SuppressWarnings({ "rawtypes", "unchecked" }) protected TableDataInfo getDataTable(List<?> list) { TableDataInfo rspData = new TableDataInfo(); rspData.setCode(HttpStatus.SUCCESS); rspData.setMsg("查询成功"); rspData.setRows(list); rspData.setTotal(new PageInfo(list).getTotal()); return rspData; } . . .
复制代码


3.4.4.2 domain

日常开发中使用实体类方式与前端对接,为了避免接口有多种返回类型,造成沟通成本的增加,使用 BaseEntity、AjaxResult 两种实体基类作为返回类型规范。


BaseEntity 规定了页码、总数等通用字段,需要其他实体类继承,避免减少重复代码与同义多名。

AjaxResult 是统一返回的实体类,能够与前台约定好固定的返回格式。如:{code: message: data}格式。

public class AjaxResult extends HashMap<String, Object>{    private static final long serialVersionUID = 1L;
/** 状态码 */ public static final String CODE_TAG = "code";
/** 返回内容 */ public static final String MSG_TAG = "msg";
/** 数据对象 */ public static final String DATA_TAG = "data";
/** * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。 */ public AjaxResult() { }
/** * 初始化一个新创建的 AjaxResult 对象 * * @param code 状态码 * @param msg 返回内容 * @param data 数据对象 */ public AjaxResult(int code, String msg, Object data) { super.put(CODE_TAG, code); super.put(MSG_TAG, msg); if (StringUtils.isNotNull(data)) { super.put(DATA_TAG, data); } } . . .
public class BaseEntity implements Serializable{ private static final long serialVersionUID = 1L;
/** 搜索值 */ private String searchValue;
/** 创建者 */ private String createBy;
/** 创建时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime;
/** 更新者 */ private String updateBy;
/** 更新时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date updateTime;
/** 备注 */ private String remark;
/** 请求参数 */ private Map<String, Object> params; . . .
复制代码

3.4.5 enums 包

对枚举的统一管理,防止枚举值的重复定义。

3.4.6 exception 包

封装了一系列的异常,在特定场景抛出,方便错误的排查与定位。


3.4.7 filter 包

过滤器包,通用写法,如需添加过滤器,按照规则复写即可。


public class RepeatableFilter implements Filter{    @Override    public void init(FilterConfig filterConfig) throws ServletException    {    }
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ServletRequest requestWrapper = null; //判断是否为application/json类型 如果是重新构建请求体 if (request instanceof HttpServletRequest && StringUtils.equalsAnyIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) { requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response); } // 否则通过 if (null == requestWrapper) { chain.doFilter(request, response); } else { chain.doFilter(requestWrapper, response); } }
@Override public void destroy() { }}
复制代码

3.4.8 utils 包

提供了非常多常见封装工具类,更加方便调用,如其他项目需要,也可直接复制使用。


3.5 ruoyi-framework 包

3.5.1 aspectj 包

此包为上文 ruoyi-common 中 annotation 包内自定义注解的实现。使用了 AOP,指定注释的逻辑类。

3.5.1.1 DataScopeAspect

该类为数据权限注解实现,在执行接口时,判断当前用户配置的数据权限(在角色页面处配置,如部门可见、本人可见),并将 sql 拼接到 mybatis 的 xml 中。核心代码如下:


public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias)    {        //拼接sql        StringBuilder sqlString = new StringBuilder();        for (SysRole role : user.getRoles())        {            String dataScope = role.getDataScope();            // 判断角色中的数据权限            if (DATA_SCOPE_ALL.equals(dataScope))            {                sqlString = new StringBuilder();                break;            }            //自定义权限拼接            else if (DATA_SCOPE_CUSTOM.equals(dataScope))            {                sqlString.append(StringUtils.format(                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,                        role.getRoleId()));            }            //部门权限拼接            else if (DATA_SCOPE_DEPT.equals(dataScope))            {                sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));            }            //部门及以下权限拼接            else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))            {                sqlString.append(StringUtils.format(                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",                        deptAlias, user.getDeptId(), user.getDeptId()));            }            .            .            .
复制代码

3.5.1.2 DataSourceAspect

DataSource 注解动态功能为切换数据源,固定写法。

3.5.1.3 LogAspect

LogAspect 为日志收集注解,在接口调用时记录用户姓名、接口方法、调用 ip 等,并插入数据库留存。

protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)    {        try        {            // 获取当前的用户            LoginUser loginUser = SecurityUtils.getLoginUser();            // *========数据库日志=========*//            SysOperLog operLog = new SysOperLog();            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());            // 请求的地址            String ip = IpUtils.getIpAddr(ServletUtils.getRequest());            operLog.setOperIp(ip);            operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());            //获取用户姓名            if (loginUser != null)            {                operLog.setOperName(loginUser.getUsername());            }            if (e != null)            {                operLog.setStatus(BusinessStatus.FAIL.ordinal());                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));            }            // 设置方法名称            String className = joinPoint.getTarget().getClass().getName();            String methodName = joinPoint.getSignature().getName();            operLog.setMethod(className + "." + methodName + "()");            // 设置请求方式            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());            // 处理设置注解上的参数            getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);            // 保存数据库            AsyncManager.me().execute(AsyncFactory.recordOper(operLog));        }        catch (Exception exp)        {            // 记录本地异常日志            log.error("==前置通知异常==");            log.error("异常信息:{}", exp.getMessage());            exp.printStackTrace();        }    }
复制代码

3.5.1.4 RateLimiterAspect

此为限流功能,接口调用时将客户端 ip 存放在 redis 中,并判断本次调用和上次调用的相隔时间。如频繁调用会拦截本次。

    @Before("@annotation(rateLimiter)")    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable    {        //redis固定的参数        String key = rateLimiter.key();        int time = rateLimiter.time();        int count = rateLimiter.count();        //获取ip+调用的方法        String combineKey = getCombineKey(rateLimiter, point);        List<Object> keys = Collections.singletonList(combineKey);        try        {            //获取一定时间内的调用次数            Long number = redisTemplate.execute(limitScript, keys, count, time);            if (StringUtils.isNull(number) || number.intValue() > count)            {                throw new ServiceException("访问过于频繁,请稍候再试");            }            log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);        }        catch (ServiceException e)        {            throw e;        }        catch (Exception e)        {            throw new RuntimeException("服务器限流异常,请稍候再试");        }    }
复制代码

3.5.2 config 包

该包为项目的核心配置类,当 application.yml 满足固定写法即可生效。配置写法固定统一,可直接复制使用,下文不做详细解释。

3.5.2.1 DruidProperties

从 application.yml 中获取数据源配置。

3.5.2.2 ApplicationConfig

时区信息配置。

3.5.2.3 CaptchaConfig

验证码使用,文字文本框格式等配置。

3.5.2.4 DruidConfig

多数据源配置。

3.5.2.5 FastJson2JsonRedisSerializer

redis 序列化配置,如缺少可能会生成乱码。

3.5.2.6 FilterConfig

过滤器配置,@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")含义为:根据 application.yml 是否配置 xss.enabled 值决定是否加载该类,也就是是否开启 xss 拦截器。

3.5.2.7 KaptchaTextCreator

计算验证码规则配置。

3.5.2.8 MyBatisConfig

Mybatis 配置类,从 application.yml 动态获取 mybatis 包的地址。并重新封装 SqlSessionFactory。实现 mybatis 路径的配置化。

    @Bean    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception    {        //从application.yml获取配置        String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");        String mapperLocations = env.getProperty("mybatis.mapperLocations");        String configLocation = env.getProperty("mybatis.configLocation");        //获取实体类的包        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);        VFS.addImplClass(SpringBootVFS.class);        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();        //加入数据源        sessionFactory.setDataSource(dataSource);        //加入实体类地址        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);        //加入mapper        sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));        //加入配置文件地址        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));        return sessionFactory.getObject();    }
复制代码

3.5.2.9 RedisConfig

redis 配置固定写法。

3.5.2.10 ResourcesConfig

配置拦截器、跨域是否生效。

public class ResourcesConfig implements WebMvcConfigurer{    @Autowired    private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { /** 本地文件上传路径 */ registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**") .addResourceLocations("file:" + RuoYiConfig.getProfile() + "/");
/** swagger配置 */ registry.addResourceHandler("/swagger-ui/**") .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/"); }
//配置拦截器生效 @Override public void addInterceptors(InterceptorRegistry registry) { //此处配置了上文点击重复的拦截器 registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); }
//跨域配置 @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); // 设置访问源地址 config.addAllowedOriginPattern("*"); // 设置访问源请求头 config.addAllowedHeader("*"); // 设置访问源请求方法 config.addAllowedMethod("*"); // 有效期 1800秒 config.setMaxAge(1800L); // 添加映射路径,拦截一切请求 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); // 返回新的CorsFilter return new CorsFilter(source); }}
复制代码

3.5.2.11 SecurityConfig

Spring Security 的配置,该处篇幅较多,请参考博主其他 Spring Security 文章。

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter{    /**     * 自定义用户认证逻辑     */    @Autowired    private UserDetailsService userDetailsService;        /**     * 认证失败处理类     */    @Autowired    private AuthenticationEntryPointImpl unauthorizedHandler;
/** * 退出处理类 */ @Autowired private LogoutSuccessHandlerImpl logoutSuccessHandler;
/** * token认证过滤器 */ @Autowired private JwtAuthenticationTokenFilter authenticationTokenFilter;
/** * 跨域过滤器 */ @Autowired private CorsFilter corsFilter; /** * 解决 无法直接注入 AuthenticationManager * * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
/** * anyRequest | 匹配所有请求路径 * access | SpringEl表达式结果为true时可以访问 * anonymous | 匿名可以访问 * denyAll | 用户不能访问 * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 * hasRole | 如果有参数,参数表示角色,则其角色可以访问 * permitAll | 用户可以任意访问 * rememberMe | 允许通过remember-me登录的用户访问 * authenticated | 用户登录后可访问 */ @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity // CSRF禁用,因为不使用session .csrf().disable() // 认证失败返回json .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() // 基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 过滤请求 验证那些接口需要鉴权 .authorizeRequests() // 对于登录login 验证码captchaImage 允许匿名访问 .antMatchers("/login", "/captchaImage").anonymous() .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js" ).permitAll() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated() .and() .headers().frameOptions().disable(); httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 添加JWT过滤器 httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加CORS过滤器 httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class); httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class); }
/** * 强散列哈希加密实现 该加密算法为spring security提供 */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); }
/** * 身份认证接口 配置查询用户的service */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); }}
复制代码

3.5.2.12 ServerConfig

获取请求信息,包括:域名,端口,上下文访问路径等。

3.5.2.13 ThreadPoolConfig

线程池配置,manager 为异步线程池实例。

3.5.3 datasource 包

多数据源固定配置,与 DataSourceAspect 配合使用。


3.5.4 interceptor 包

提供不允许重复点击功能,实现原理:调用信息组装为 key 值存放到 redis 中,调用时从 redis 获取该 key 数据并验证相隔时间,如过于频繁,抛弃本次调用。

public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation)    {        String nowParams = "";        if (request instanceof RepeatedlyRequestWrapper)        {            RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;            nowParams = HttpHelper.getBodyString(repeatedlyRequest);        }        // body参数为空,获取Parameter的数据        if (StringUtils.isEmpty(nowParams))        {            nowParams = JSONObject.toJSONString(request.getParameterMap());        }        Map<String, Object> nowDataMap = new HashMap<String, Object>();        nowDataMap.put(REPEAT_PARAMS, nowParams);        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());        // 请求地址(作为存放cache的key值)        String url = request.getRequestURI();        // 唯一值(没有消息头则使用请求地址)        String submitKey = request.getHeader(header);        if (StringUtils.isEmpty(submitKey))        {            submitKey = url;        }        // 组装成加入redis的key值        String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + submitKey;        //根据key值查询redsi        Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);        //如果能够查询到        if (sessionObj != null)        {            Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;            if (sessionMap.containsKey(url))            {                Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);                //比对参数,同时比对时间                if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval()))                {                    return true;                }            }        }        Map<String, Object> cacheMap = new HashMap<String, Object>();        cacheMap.put(url, nowDataMap);        redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);        return false;    }
复制代码

3.5.5 manager 包


AsyncManager、ShutdownManager为异步工厂配置,AsyncFactory 为使用实例。

3.5.6 security 包


该包为 token 相关方法集合。

3.5.6.1 JwtAuthenticationTokenFilter

主要为验证 token 是否正确。

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter{    @Autowired    private TokenService tokenService;
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { //验证用户信息 LoginUser loginUser = tokenService.getLoginUser(request); // 验证用户是否有权限 if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) { //刷新token tokenService.verifyToken(loginUser); //获取用户权限对象 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); //将用户权限等信息存放在SecurityContext中 SecurityContextHolder.getContext().setAuthentication(authenticationToken); } chain.doFilter(request, response); }}
复制代码

3.5.6.2 AuthenticationEntryPointImpl、LogoutSuccessHandlerImpl

将登陆成功、失败等返回信息转换为 json 方式。

3.5.7 web 包

3.5.7.1 server

获取服务器信息,如 cpu 等信息。

private void setCpuInfo(CentralProcessor processor)    {        // CPU信息        long[] prevTicks = processor.getSystemCpuLoadTicks();        Util.sleep(OSHI_WAIT_SECOND);        long[] ticks = processor.getSystemCpuLoadTicks();        long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()];        long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()];        long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()];        long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()];        long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()];        long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()];        long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()];        long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()];        long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal;        cpu.setCpuNum(processor.getLogicalProcessorCount());        cpu.setTotal(totalCpu);        cpu.setSys(cSys);        cpu.setUsed(user);        cpu.setWait(iowait);        cpu.setFree(idle);    }
/** * 设置内存信息 */ private void setMemInfo(GlobalMemory memory) { mem.setTotal(memory.getTotal()); mem.setUsed(memory.getTotal() - memory.getAvailable()); mem.setFree(memory.getAvailable()); } . . .
复制代码

3.5.7.2 exception

@RestControllerAdvice、@ExceptionHandler为全局异常拦截配置,当系统中有异常时,会直接调用该类,并根据异常类型抛出指定输出,减少业务代码中的 try/catch。

@RestControllerAdvicepublic class GlobalExceptionHandler{    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/** * 权限校验异常 */ @ExceptionHandler(AccessDeniedException.class) public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage()); return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权"); }
/** * 请求方式不支持 */ @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod()); return AjaxResult.error(e.getMessage()); }
复制代码

3.5.7.3 service

权限相关的 service。大部分都是 curd 的业务,这里详细说 TokenService。

3.5.7.3.1 TokenService

使用 jwt 实现 token 生成、token 获取数据等方法。

   /**     * 从数据声明生成令牌     *     * @param claims 数据声明     * @return 令牌     */    private String createToken(Map<String, Object> claims)    {        String token = Jwts.builder()                .setClaims(claims)                .signWith(SignatureAlgorithm.HS512, secret).compact();        return token;    }
/** * 从令牌中获取数据声明 * * @param token 令牌 * @return 数据声明 */ private Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); }
复制代码

3.6 ruoyi-admin 包

ruoyi-admin 包提供了一系列的通用接口(controller 接口),如需使用直接调用即可,防止开发者未经过沟通相同功能接口开发多次,造成不统一。

3.6.1 common 包

3.6.1.1 CaptchaController

通过谷歌验证码方法生成验证码,详细请看注释。

    @GetMapping("/captchaImage")    public AjaxResult getCode(HttpServletResponse response) throws IOException    {        // 通过uuid生成验证码        String uuid = IdUtils.simpleUUID();        String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
String capStr = null, code = null; BufferedImage image = null;
// 根据类型生成不同的验证码 //数字验证码 if ("math".equals(captchaType)) { //使用谷歌验证码方法 String capText = captchaProducerMath.createText(); capStr = capText.substring(0, capText.lastIndexOf("@")); code = capText.substring(capText.lastIndexOf("@") + 1); //使用谷歌验证码方法生成图片 image = captchaProducerMath.createImage(capStr); } //文字验证码 else if ("char".equals(captchaType)) { capStr = code = captchaProducer.createText(); image = captchaProducer.createImage(capStr); }
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); // 转换流信息写出 FastByteArrayOutputStream os = new FastByteArrayOutputStream(); try { ImageIO.write(image, "jpg", os); } catch (IOException e) { return AjaxResult.error(e.getMessage()); }
AjaxResult ajax = AjaxResult.success(); ajax.put("uuid", uuid); ajax.put("img", Base64.encode(os.toByteArray())); return ajax; }
复制代码

3.6.1.2 CommonController

通用的上传下载接口,防止重复编写造成一个需求需要修改多个类方法。

3.6.2 monitor 包

3.6.2.1 CacheController

通过 redis 对外接口监控 redis 信息,如 rediskey 数量、key 的详细信息等。

public class CacheController{    @Autowired    private RedisTemplate<String, String> redisTemplate;
@PreAuthorize("@ss.hasPermi('monitor:cache:list')") @GetMapping() public AjaxResult getInfo() throws Exception { //redis的常用信息 Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info());
Properties commandStats = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info("commandstats")); //rediskey数量 Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize());
Map<String, Object> result = new HashMap<>(3); result.put("info", info); result.put("dbSize", dbSize); //key的详细信息 List<Map<String, String>> pieList = new ArrayList<>(); commandStats.stringPropertyNames().forEach(key -> { Map<String, String> data = new HashMap<>(2); String property = commandStats.getProperty(key); data.put("name", StringUtils.removeStart(key, "cmdstat_")); data.put("value", StringUtils.substringBetween(property, "calls=", ",usec")); pieList.add(data); }); result.put("commandStats", pieList); return AjaxResult.success(result); }}
复制代码

3.6.2.2 ServerController

主要监控正在运行服务器的信息,如 cpu、内存、磁盘等信息。

public void copyTo() throws Exception    {        SystemInfo si = new SystemInfo();        HardwareAbstractionLayer hal = si.getHardware();        setCpuInfo(hal.getProcessor());        setMemInfo(hal.getMemory());        setSysInfo();        setJvmInfo();        setSysFiles(si.getOperatingSystem());    }
/** * 设置CPU信息 */ private void setCpuInfo(CentralProcessor processor) { // CPU信息 long[] prevTicks = processor.getSystemCpuLoadTicks(); Util.sleep(OSHI_WAIT_SECOND); long[] ticks = processor.getSystemCpuLoadTicks(); long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()]; long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()]; long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()]; long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()]; long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()]; long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()]; long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()]; long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()]; long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal; cpu.setCpuNum(processor.getLogicalProcessorCount()); cpu.setTotal(totalCpu); cpu.setSys(cSys); cpu.setUsed(user); cpu.setWait(iowait); cpu.setFree(idle); }
/** * 设置内存信息 */ private void setMemInfo(GlobalMemory memory) { mem.setTotal(memory.getTotal()); mem.setUsed(memory.getTotal() - memory.getAvailable()); mem.setFree(memory.getAvailable()); }
. . .
}
复制代码


3.SysLogininforController,SysOperlogController 登录日志、操作日志


主要查询前文 AOP 生成的日志表,普通的增删改查。


4.SysUserOnlineController 在线用户管理


主要功能为在线用户监控与强踢下线。通过查询和删除 redis 缓存即可实现。

3.6.3 system 包

上文说到 ruoyi 业务代码的 service 与 controller 层分在两个包中,该处为业务代码的 controller 层。



3.6.3.1 @PreAuthorize

@PreAuthorize("@ss.hasPermi('system:dict:list')")此处为 shiro 框架提供权限注解,配合权限表使用,当菜单表中 perms 字段与 @PreAuthorize 注解内容system:dict:list匹配后才有权限访问接口。

3.6.3.2 AjaxResult

提供了固定的结果编码/调用信息/数据的返回格式,为前台提供了统一的返回格式,这样防止过多种类的返回类型,从而增大沟通成本。该方案为架构的基础,绝大部分架构均为该种处理方案。

public class AjaxResult extends HashMap<String, Object>{    private static final long serialVersionUID = 1L;
/** 状态码 */ public static final String CODE_TAG = "code";
/** 返回内容 */ public static final String MSG_TAG = "msg";
/** 数据对象 */ public static final String DATA_TAG = "data";
/** * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。 */ public AjaxResult() { } /** * 初始化一个新创建的 AjaxResult 对象 * * @param code 状态码 * @param msg 返回内容 */ public AjaxResult(int code, String msg) { super.put(CODE_TAG, code); super.put(MSG_TAG, msg); }
/** * 返回成功消息 * * @return 成功消息 */ public static AjaxResult success() { return AjaxResult.success("操作成功"); } . . .
复制代码

三.ruoyi 的优势

ruoyi 框架对比市面上其他产品有以下优势:

  1. 代码为作者一个编写,格式统一,注释完备,整体结构干净舒服;

  2. 搭建与启动快捷,依赖组件较少;

  3. 基础功能完备,足够支撑小体量业务;

  4. 社区活跃,提供了工作流、单点登陆、多数据库版本,可按需使用。

四.总结

如果您有初识架构、自我提升、私活项目架构等需求,ruoyi 是您不二选择,如果其他问题,可留言沟通。


发布于: 刚刚阅读数: 6
用户头像

小鲍侃java

关注

小鲍加油 2021.07.13 加入

一位技术落地与应用的博主,带你从入门,了解和使用各项顶流开源项目。

评论

发布
暂无评论
架构师第一课,一文带你玩转ruoyi架构_框架_小鲍侃java_InfoQ写作社区