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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
日志服务 SLS,月写入数据量 50GB 1个月
简介: 我理解的架构/框架应该有以下功能:1.满足日常开发功能,如单点登陆、消息队列、监控等;2.规范开发者的开发,指定代码格式、注释等;3.提高开发效率,提供一系列的封装方法,并减少bug的产生率。下文将详细介绍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 install
npm 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。

@RestControllerAdvice
public 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是您不二选择,如果其他问题,可留言沟通。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
6月前
|
敏捷开发 缓存 架构师
Apache 架构师总结的 30 条架构原则
Apache 架构师总结的 30 条架构原则
78 0
|
3月前
|
存储 架构师 测试技术
架构之道——人人都是架构师
本文的探讨和编写主要围绕三个方面:架构是什么?架构师要解决的问题有哪些?解决这些问题的方法论是什么?最后作者希望人人都能具备架构师思维。
|
6月前
|
机器学习/深度学习 人工智能 架构师
【架构师】AI时代架构师必备技能
【架构师】AI时代架构师必备技能
135 5
|
1月前
|
缓存 NoSQL Java
秒杀圣经:10Wqps秒杀,16大架构绝招,一文帮你秒变架构师 (2)
高并发下的秒杀系统设计是一个复杂的挑战,涉及多个关键技术点。40岁老架构师尼恩在其读者交流群中分享了16个关键架构要点,帮助解决高并发下的秒杀问题,如每秒上万次下单请求的处理、超卖问题的解决等。这些要点包括业务架构设计、流量控制、异步处理、缓存策略、限流熔断、分布式锁、消息队列、数据一致性、存储架构等多个方面。尼恩还提供了详细的实战案例和代码示例,帮助读者全面理解和掌握秒杀系统的架构设计。此外,他还分享了《尼恩Java面试宝典》等资源,帮助读者在面试中脱颖而出。如果你对高并发秒杀系统感兴趣,可以关注尼恩的技术自由圈,获取更多详细资料。
秒杀圣经:10Wqps秒杀,16大架构绝招,一文帮你秒变架构师 (2)
|
1月前
|
缓存 NoSQL Java
秒杀圣经:10Wqps高并发秒杀,16大架构杀招,帮你秒变架构师 (1)
高并发下,如何设计秒杀系统?这是一个高频面试题。40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试Shopee时遇到了这个问题,未能很好地回答,导致面试失败。为此,尼恩进行了系统化、体系化的梳理,帮助大家提升“技术肌肉”,让面试官刮目相看。秒杀系统设计涉及16个架构要点,涵盖业务架构、流量架构、异步架构、分层架构、缓存架构、库存扣减、MQ异步处理、限流、熔断、降级、存储架构等多个方面。掌握这些要点,可以有效应对高并发场景下的秒杀系统设计挑战。
秒杀圣经:10Wqps高并发秒杀,16大架构杀招,帮你秒变架构师 (1)
|
4月前
|
存储 架构师 测试技术
架构之道:人人都是架构师(2)
每个业务系统的开发者都应该具备一定的架构师素养,架构师的重要职责不仅仅是做决策,更重要的是提升团队的整体能力。一个好的架构师应该聚焦于业务和系统,定义问题和结果,设计系统、模块和代码,同时也需要解决跨域问题,确定团队间的边界,制定规范,统一语言,并创建一个让每个人都能成长为架构师的环境,以促进团队的敏捷性。本文旨在探讨如何培养架构思维,并阐述了架构师的职责、能力模型、方法论,以及如何成为架构师。
141 10
|
4月前
|
存储 运维 架构师
架构之道:人人都是架构师(1)
架构之道:人人都是架构师
178 8
|
6月前
|
运维 架构师 安全
架构师养成手册:架构师职责
小米是一名热情的技术爱好者和架构师,他探讨了架构师的角色和职责。主要涉及六个方面:顶层设计,需与企业战略目标对齐,制定架构原则;规划可适应未来变化的企业架构,分析需求并关注技术趋势;全局视角制定可落地的架构方案,兼顾全局与局部优化;技术选型与难题解决,选择合适技术并解决实际问题;关注方案与代码的广度与深度,确保宏观设计与微观实现的统一;同时,架构师还需具备管理能力,包括团队协作、资源调配和风险管理。
192 11
|
6月前
|
存储 消息中间件 算法
深度思考:架构师必须掌握的五大类架构设计风格
数据流风格注重数据在组件间的流动,适合处理大量数据。调用返回风格则强调函数或方法的调用与返回,过程清晰明了。独立构件风格让每个构件独立运作,通过接口交互,提升灵活性和可重用性。虚拟机风格则模拟完整系统,实现资源的高效利用。
360 0
深度思考:架构师必须掌握的五大类架构设计风格
|
6月前
|
机器学习/深度学习 人工智能 架构师
【架构师】AI时代架构师必备技能
【架构师】AI时代架构师必备技能