微人事项目代码泛读结对练习经验分享
一、练习目标:
阅读源码,对我们的编码能力提升还是比较大,就像我们搞研究,是需要看一些文献。所以阅读源码多了,我们在代码能力、代码功底就会有很大的提升。
我们在阅读代码前,首先是需要对你所阅读的代码有一个整体的认知,就比如我们阅读Hadoop源码。2008年1月,Hadoop成为Apache顶级项目。到现在2022年,已经发展了14年了,所以我们正确的认识到,我们行业顶级精英,天才级别的大神,写了14年的Hadoop项目,我们想花费多长时间来去读懂、读透。
显然我们想一周、两周、两个月来读懂读透,这是不大可能的,所以这是为什么很多的同学,在阅读源码的时候,还没有开始就已经结束的原因,很多是因为没有正确认知项目的发展和积累。这有点像我们刚会识字或者写作文,就让我们去看四大名著,而且像快速看完,这显然是达不到的。
在我们对源码有了整体的认识后,接着我们就要具体的去研读源码,那么该如何阅读源码,阅读源码的步骤:
1.首先了解项目背景
前面我们已经涉及到,项目背景是非常重要的,比如Hadoop,我们是否对他所了解。Hadoop为何产生,是为了解决在大数据量的情况下,单机很难计算和处理的数据的情况下,所以产生了价格和成本都非常昂贵的超级计算机。所以有的人就想如何通过廉价的普通的计算机来实现计算大数据量,所以Hadoop应运而生。Hadoop又分为Hdfs、Yarn等组件,当然这里面又会细分,我们了解的越详细,对我们阅读源码越方便和快捷。
2.了解项目功能、结构
了解项目功能、结构,比如哪些是通用部分,哪些是功能部分。所以这里我们需要一定猜测,这个猜测我们同样需要去验证。有的大神称其为““正向推导+验证””,这里其实也和我们的学习方法和思维关联。我们在学习比如当前的源码,你的学习思路是什么?是一直跟着文件或者文档的思路去学习,还是自己提前有一定的想法,然后去文档或者文件中去验证自己的想法,这二者的学习效果和速度是不一样的。
3.调试代码
调试代码,很多同学卡在了这里,因为跟踪代码的时候,跳来跳去,就整蒙了。要么不知道为何会跟踪到这里,要么跟踪到这里,不认识它,总之很多同学到这里有的就放弃了。
二、微人事项目部署
(1) 微人事是一个前后端分离的人力资源管理系统,项目采用 SpringBoot+Vue 开发,项目加入常见的企业级应用所涉及到的技术点,例如 Redis、RabbitMQ 等
(2) 项目地址:GitHub的vhr项目地址 或者 Gitee的vhr项目地址
(3) 功能特点
实现人事管理信息系统,包括员工资料、人事管理、工资管理、统计管理、系统管理等;
(4) 部署:
先使用git克隆项目到本地仓库
git clone git@github.com:lenve/vhr.git
配置数据库,在本地(或者云端和虚拟机)创建一个名为vhr的数据库 —创建即可
下载redis,可以使用我这边提供的网址:https://github.com/tporadowski/redis/releases
或者去官网自行下载:https://redis.io/download
配置rabbitmq消息队列,可以看我之前的博客,在Linux环境下利用docker部署
这里附上我之前的博客链接:http://t.csdnimg.cn/U2x5q
完成后需要在配置文件将上面三个的配置换成自己的
之后就可以运行了
三、git训练
(1) 创建针对本作业的项目和软件版本库,在版本库中建立“src”和“doc”两个文件夹,分别存储软件系统的源代码和报告文档
(2) 建立master、develop以及成员分支(a_branch),将当前版本存入master目录下
在远程仓库可以看到已经生成了一个develop
(3) 实践操作参考:组长组员两个人协同开发:组长负责维护开发分支dev,组员向dev上传提交;当dev测试合适后,组长有唯一权限向master上传作为最终结果。
① 远程仓库有master和dev两个分支
② 组长本地有master和dev分支,分别关联对应的远程分支
③ 组员本地只有一个分支,关联远程dev分支【可以选择clone某一个远程分支到本地】
④ 具体开发流程是:
-
组长和组员分别在各自的本地dev分支开发,有阶段性成果后push到远程dev【若有冲突,解决冲突再合并】
组员在src提交了一个文件:
组长提交:提交过程报错,需要pull最新版本的develop,这时候进行冲突处理后合并,解决后重新push到orgin develop
-
当开发完成、结果稳定后,组长将本地的master和dev分支merge,再把master分支push上去
最后展示一下项目
四、项目理解
(1) 用例图:通过用例图来描述微人事系统的主要功能以及它们之间的关系;
(2) 体系结构图(包图):通过绘制体系结构图来了解整个软件的总体设计思路;
(3) 类之间的调用关系图:通过绘制类之间的调用关系图来掌握微人事的具体设计;
(4) 核心类的主要作用:通过给出核心类的主要作用来进一步加深对软件设计的理解。
1.MailReceiver类
@Component
public class MailReceiver {
public static final Logger logger = LoggerFactory.getLogger(MailReceiver.class);
@Autowired
JavaMailSender javaMailSender;
@Autowired
MailProperties mailProperties;
@Autowired
TemplateEngine templateEngine;
@Autowired
StringRedisTemplate redisTemplate;
@RabbitListener(queues = MailConstants.MAIL_QUEUE_NAME)
public void handler(Message message, Channel channel) throws IOException {
Employee employee = (Employee) message.getPayload();
MessageHeaders headers = message.getHeaders();
Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
String msgId = (String) headers.get("spring_returned_message_correlation");
if (redisTemplate.opsForHash().entries("mail_log").containsKey(msgId)) {
//redis 中包含该 key,说明该消息已经被消费过
logger.info(msgId + ":消息已经被消费");
channel.basicAck(tag, false);//确认消息已消费
return;
}
//收到消息,发送邮件
MimeMessage msg = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(msg);
try {
helper.setTo(employee.getEmail());
helper.setFrom(mailProperties.getUsername());
helper.setSubject("入职欢迎");
helper.setSentDate(new Date());
Context context = new Context();
context.setVariable("name", employee.getName());
context.setVariable("posName", employee.getPosition().getName());
context.setVariable("joblevelName", employee.getJobLevel().getName());
context.setVariable("departmentName", employee.getDepartment().getName());
String mail = templateEngine.process("mail", context);
helper.setText(mail, true);
javaMailSender.send(msg);
redisTemplate.opsForHash().put("mail_log", msgId, "javaboy");
channel.basicAck(tag, false);
logger.info(msgId + ":邮件发送成功");
} catch (MessagingException e) {
channel.basicNack(tag, false, true);
e.printStackTrace();
logger.error("邮件发送失败:" + e.getMessage());
}
}
}
主要作用:
这段代码是一个邮件接收器 MailReceiver,通过 RabbitMQ 监听特定队列中的消息,并处理发送邮件的逻辑。让我简单解释一下代码的主要功能:
- 通过 @Autowired 注解注入了 JavaMailSender、MailProperties、TemplateEngine 和 StringRedisTemplate 实例。
- 使用 @RabbitListener 注解监听名为 MailConstants.MAIL_QUEUE_NAME 的 RabbitMQ 队列, MailConstants.MAIL_QUEUE_NAME的值可以在MailConstants常量类找到
public static final String MAIL_QUEUE_NAME = “javaboy.mail.queue”;
- 在 handler 方法中,从消息中获取员工信息,并检查消息是否已经被消费过(通过 Redis)。
- 如果消息未被消费过,则利用MimeMessage构建邮件内容并发送邮件,发送成功后将消息 ID 记录到 Redis 缓存中。
- 利用try-catch进行处理,如果发送过程中出现异常,将消息标记为未确认状态,并记录错误日志。
2.各个Control类(以SalaryControl类为例)
@RestController
@RequestMapping("/salary/sob")
public class SalaryController {
@Autowired
SalaryService salaryService;
@GetMapping("/")
public List<Salary> getAllSalaries() {
return salaryService.getAllSalaries();
}
@PostMapping("/")
public RespBean addSalary(@RequestBody Salary salary) {
if (salaryService.addSalary(salary) == 1) {
return RespBean.ok("添加成功!");
}
return RespBean.error("添加失败!");
}
@DeleteMapping("/{id}")
public RespBean deleteSalaryById(@PathVariable Integer id) {
if (salaryService.deleteSalaryById(id) == 1) {
return RespBean.ok("删除成功!");
}
return RespBean.error("删除失败!");
}
@PutMapping("/")
public RespBean updateSalaryById(@RequestBody Salary salary) {
if (salaryService.updateSalaryById(salary) == 1) {
return RespBean.ok("更新成功!");
}
return RespBean.error("更新失败!");
}
}
这个项目以Restful风格开发控制器,这个用例SalaryControl的作用是用于处理与薪资信息相关的HTTP请求。
- 通过@Autowired自动注入了SalaryService服务,用于处理与薪资信息相关的业务逻辑,一般来说功能的实现都需要放到service层进行处理,Controller层专注处理通讯问题。
- @GetMapping("/")注解的getAllSalaries()方法处理HTTP的GET请求,用于获取所有的薪资信息列表。
- @PostMapping("/")注解的addSalary(@RequestBody Salary salary)方法处理HTTP的POST请求,用于添加新的薪资信息。
- @DeleteMapping("/{id}")注解的deleteSalaryById(@PathVariable Integer id)方法处理HTTP的DELETE请求,根据提供的id删除特定的薪资信息。
- @PutMapping("/")注解的updateSalaryById(@RequestBody Salary salary)方法处理HTTP的PUT请求,用于更新特定的薪资信息。
选择这个类作为样例是因为这个使用到了比较全面的RestfulAPI中的GET(获取)、POST(创建)、DELETE(删除)和PUT(更新),用于实现对薪资信息的增删改查功能。
- SecurityConfig安全
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
HrService hrService;
@Autowired
CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;
@Autowired
CustomUrlDecisionManager customUrlDecisionManager;
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(hrService);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/js/**", "/index.html", "/img/**", "/fonts/**", "/favicon.ico", "/verifyCode");
}
@Bean
LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.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();
}
);
loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
RespBean respBean = RespBean.error(exception.getMessage());
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();
}
);
loginFilter.setAuthenticationManager(authenticationManagerBean());
loginFilter.setFilterProcessesUrl("/doLogin");
ConcurrentSessionControlAuthenticationStrategy sessionStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
sessionStrategy.setMaximumSessions(1);
loginFilter.setSessionAuthenticationStrategy(sessionStrategy);
return loginFilter;
}
@Bean
SessionRegistryImpl sessionRegistry() {
return new SessionRegistryImpl();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
return object;
}
})
.and()
.logout()
.logoutSuccessHandler((req, resp, authentication) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功!")));
out.flush();
out.close();
}
)
.permitAll()
.and()
.csrf().disable().exceptionHandling()
//没有认证时,在这里处理结果,不要重定向
.authenticationEntryPoint((req, resp, authException) -> {
resp.setContentType("application/json;charset=utf-8");
resp.setStatus(401);
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error("访问失败!");
if (authException instanceof InsufficientAuthenticationException) {
respBean.setMsg("请求失败,请联系管理员!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
);
http.addFilterAt(new ConcurrentSessionFilter(sessionRegistry(), event -> {
HttpServletResponse resp = event.getResponse();
resp.setContentType("application/json;charset=utf-8");
resp.setStatus(401);
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(RespBean.error("您已在另一台设备登录,本次登录已下线!")));
out.flush();
out.close();
}), ConcurrentSessionFilter.class);
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
这段代码的量比较多,但是作为Web应用安全控制应该是至关重要的,SecurityConfig类是Spring Security的配置类,用于配置应用程序的安全设置。
- 首先这个@Configuration注解表明这是一个配置类,Spring会自动扫描并加载该类中定义的Bean。
- 继承WebSecurityConfigurerAdapter类,通过继承该类,可以重写其中的方法来自定义安全配置。
- 在configure(AuthenticationManagerBuilder auth)方法中,通过auth.userDetailsService(hrService)配置了用户认证逻辑,指定了hrService作为用户详情服务。
- 在configure(WebSecurity web)方法中,通过web.ignoring().antMatchers()配置了不需要经过Spring Security过滤器链的静态资源,如CSS、JS、图片等。
- 通过@Bean注解定义了PasswordEncoder的Bean,使用了BCrypt加密算法,返回一个提供加密功能的对象。
- 定义了LoginFilter的Bean,并在其中配置了登录成功和失败的处理方式,以及会话控制策略。
- 在configure(HttpSecurity http)方法中配置了HTTP请求的安全处理规则,包括权限控制、登出处理、CSRF禁用、异常处理等。