一.若依是什么
1.1 什么是框架/架构
我理解的架构/框架应该有以下功能:
- 满足日常开发功能,如单点登陆、消息队列、监控等;
- 规范开发者的开发,指定代码格式、注释等;
- 提高开发效率,提供一系列的封装方法,并减少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包
该包为代码生成器,通过前端页面指定业务表、模块功能等信息,就可以生成前后端代码,且生成的代码风格统一,极大提高了开发效率,主要的流程如下。
- 编写vm文件,提前定义模板;
- 通过页面输入业务表、模块相关信息;
- 首先通过sql语句查询指定表的字段信息,并将查询出的字段信息存入表中;
- 遍历vm文件,将表信息与输入功能信息填入vm文件预留变量中;
- 将文件转为流,并存放于指定位置。
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框架对比市面上其他产品有以下优势:
- 代码为作者一个编写,格式统一,注释完备,整体结构干净舒服;
- 搭建与启动快捷,依赖组件较少;
- 基础功能完备,足够支撑小体量业务;
- 社区活跃,提供了工作流、单点登陆、多数据库版本,可按需使用。
四.总结
如果您有初识架构、自我提升、私活项目架构等需求,ruoyi是您不二选择,如果其他问题,可留言沟通。