分析项目:微人事 这次我们来分析一下这个名为:”微人事“ 的项目的功能以及各个模块的实现。
这个项目分为两个大模块,分别是server模块和mail模块,这个项目的作者也已经很久没有更新了,我们先忽略掉mail这个模块,来看看server这个模块有着什么。
点击进入到server模块内,可以得知这个server模块又被切分为了四个小的模块,分别是mapper、model、service和web。
显然,这个web模块,就是我们主要学习的内容了。
之后根据经验之谈,一般在config这个包内的,基本上都是security的配置相关。
Security配置 配置中有一行:
1 2 3 4 5 6 @Override public <O extends FilterSecurityInterceptor> O postProcess (O object) { object.setAccessDecisionManager(customUrlDecisionManager); object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource); return object; }
来决定security的设置,而这两个设置,是被自定义的构造了出来。
这个配置主要设置了两个选择,一个是在登录的时候,根据用户名去搜索数据库,根据用户名来得到用户的权限,从而跳转到该角色所能访问的页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Component public class CustomUrlDecisionManager implements AccessDecisionManager { @Override public void decide (Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { for (ConfigAttribute configAttribute : configAttributes) { String needRole = configAttribute.getAttribute(); if ("ROLE_LOGIN" .equals(needRole)) { if (authentication instanceof AnonymousAuthenticationToken) { throw new AccessDeniedException("尚未登录,请登录!" ); }else { return ; } } Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); for (GrantedAuthority authority : authorities) { if (authority.getAuthority().equals(needRole)) { return ; } } } throw new AccessDeniedException("权限不足,请联系管理员!" ); } }
请求分析 这个类的作用,主要是根据用户传来的请求地址,分析出请求需要的角色:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Component public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Autowired MenuService menuService; AntPathMatcher antPathMatcher = new AntPathMatcher(); @Override public Collection<ConfigAttribute> getAttributes (Object object) throws IllegalArgumentException { String requestUrl = ((FilterInvocation) object).getRequestUrl(); List<Menu> menus = menuService.getAllMenusWithRole(); for (Menu menu : menus) { if (antPathMatcher.match(menu.getUrl(), requestUrl)) { List<Role> roles = menu.getRoles(); String[] str = new String[roles.size()]; for (int i = 0 ; i < roles.size(); i++) { str[i] = roles.get(i).getName(); } return SecurityConfig.createList(str); } } return SecurityConfig.createList("ROLE_LOGIN" ); } @Override public Collection<ConfigAttribute> getAllConfigAttributes () { return null ; } @Override public boolean supports (Class<?> clazz) { return true ; } }
还有可以值得思考的地方是,作者的security为每一个成功或者失败的Handler,都做了自己处理的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess (HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8" ); PrintWriter out = resp.getWriter(); Hr hr = (Hr) authentication.getPrincipal(); hr.setPassword(null ); RespBean ok = RespBean.ok("登录成功!" , hr); String s = new ObjectMapper().writeValueAsString(ok); out.write(s); out.flush(); out.close(); } }) .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure (HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8" ); PrintWriter out = resp.getWriter(); RespBean respBean = RespBean.error("登录失败!" ); if (exception instanceof LockedException) { respBean.setMsg("账户被锁定,请联系管理员!" ); } else if (exception instanceof CredentialsExpiredException) { respBean.setMsg("密码过期,请联系管理员!" ); } else if (exception instanceof AccountExpiredException) { respBean.setMsg("账户过期,请联系管理员!" ); } else if (exception instanceof DisabledException) { respBean.setMsg("账户被禁用,请联系管理员!" ); } else if (exception instanceof BadCredentialsException) { respBean.setMsg("用户名或者密码输入错误,请重新输入!" ); } out.write(new ObjectMapper().writeValueAsString(respBean)); out.flush(); out.close(); } })
层次的结合 如果说单单security的设置的话,可能很多人自认为自己可以耐心一点,慢慢写,也可以做到很详细的配置。但是我还看到了别的地方。
我们来看到Controller层,这里有着:
1 2 3 4 5 6 7 8 9 10 @RestController @RequestMapping ("/system/config" )public class SystemConfigController { @Autowired MenuService menuService; @GetMapping ("/menu" ) public List<Menu> getMenusByHrId () { return menuService.getMenusByHrId(); } }
我们可以看出一点不太符合逻辑的东西,这里的方法明显是在展示一个菜单,为什么service的方法名有一个ByHrId?我们却没有在任何一点地方看到传入Hr的信息啊?
我们点进去看一看:
1 2 3 public List<Menu> getMenusByHrId () { return menuMapper.getMenusByHrId(((Hr) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId()); }
这个方法的实现是获取到security的权限表的一个ID,也就是说,它的菜单栏会根据Hr的ID在展现。
为什么要这么做呢?那是因为为了方便以后更方便扩展,可以拥有更高的权限去解锁更多的选项栏,从现在这样的一对多关系延伸到多对多关系。
规范的写法 我们还可以从Controller层看到,这里返回的是自定义的类RespBean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class RespBean { private Integer status; private String msg; private Object obj; public static RespBean build () { return new RespBean(); } public static RespBean ok (String msg) { return new RespBean(200 , msg, null ); } public static RespBean ok (String msg, Object obj) { return new RespBean(200 , msg, obj); } public static RespBean error (String msg) { return new RespBean(500 , msg, null ); } public static RespBean error (String msg, Object obj) { return new RespBean(500 , msg, obj); } }
这样可以更规范的去使用,便于让前端明白是什么类型,而不是非常粗暴的使用HashMap,使用HashMap有很多不好的地方。这里暂时不说明
自定义处理 还有,自己定义对时间的类型的处理,时间类型的绑定往往会是前后端解耦比较麻烦的地方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Component public class DateConverter implements Converter <String , Date > { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd" ); @Override public Date convert (String source) { try { return sdf.parse(source); } catch (ParseException e) { e.printStackTrace(); } return null ; } }
运用缓存 在service层上也运用了缓存的功能,我们可以使用缓存区缓解压力,就例如像菜单这样的设置,完全可以使用CacheConfig去设置一个缓存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Service @CacheConfig (cacheNames = "menus_cache" )public class MenuService { @Autowired MenuMapper menuMapper; @Autowired MenuRoleMapper menuRoleMapper; public List<Menu> getMenusByHrId () { return menuMapper.getMenusByHrId(((Hr) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId()); } @Cacheable public List<Menu> getAllMenusWithRole () { return menuMapper.getAllMenusWithRole(); } public List<Menu> getAllMenus () { return menuMapper.getAllMenus(); } public List<Integer> getMidsByRid (Integer rid) { return menuMapper.getMidsByRid(rid); } @Transactional public boolean updateMenuRole (Integer rid, Integer[] mids) { menuRoleMapper.deleteByRid(rid); if (mids == null || mids.length == 0 ) { return true ; } Integer result = menuRoleMapper.insertRecord(rid, mids); return result==mids.length; } }
追溯到底层,我们可以看到GetALlMenusWithRole这样的方法的SQL语句:
1 2 3 <select id="getAllMenusWithRole" resultMap="MenuWithRole"> select m.*,r.`id` as rid,r.`name` as rname,r.`nameZh` as rnameZh from menu m,menu_role mr,role r where m.`id` =mr.`mid` and mr.`rid` =r.`id` order by m.`id` </select >
像类似于这样的SQL语句完全可以进行一定的缓存封装,从而减少压力。
一些错误 这样项目就这么完美,没有差错吗?也不尽然,从以往的眼光来看,我们看这个项目就算是一个SSM项目的经典类型。这个项目不涉及云服务也不涉及应用监控相关的内容,但仅仅是用于自己的毕业设计的话,也就是足够了。
当然,我本身在测试这个项目的时候还发现一些这个项目错误的地方,现今指出来:
比如在使用Search功能的时候出现的keyword错误,需要把Mapper的SQL修改一下,改为 name
其次就是在使用导出数据的时候,作者还没有配置相关数据:
1 2 3 4 5 @GetMapping ("/export" )public ResponseEntity<byte []> exportData() { List<Employee> list = (List<Employee>) employeeService.getEmployeeByPage(null , null , null ,null ).getData(); return POIUtils.employee2Excel(list); }
总体来说,这个项目非常适合一些学了知识却又不知道怎么运用的初学者。