Pengzna's blog 👋

Jul 10, 2022

南京大学软件学院-软件工程与计算Ⅱ大作业心得

笔者在南京大学软件学院2022年春-软件工程与计算Ⅱ课程中获得99分,其中大作业118分(满分100,拿了18分的 bonus)。因此记录下自己的作业心得。

SpringBoot

1. 面向接口编程

  • 主要用于代码复用、消除循环依赖、实现拓展等
  • 方法:抽象公共父类(最低阶)、泛型编程(更灵活,根据具体类注入,可以实现参数和实现的多态)。下面以个人项目实践中的根据泛型实现面向接口编程举出实例:
// 接口类,通过泛型注入具体参数,实现每个方法的参数多态
public interface SheetService <SheetVO, SheetState>{
/**
* 创建单据
* @param userVO 操作用户
* @param sheetVO 单据VO
*/
void makeSheet(UserVO userVO, SheetVO sheetVO);
/**
* 根据状态获取单据(state == null 则获取所有单据)
* @param state 单据状态
* @return 符合条件的所有单据
*/
List<SheetVO> getSheetByState(SheetState state);
/**
* 根据进货单id进行审批(state == 审批完成"/"审批失败")
* 在controller层进行权限控制
* @param sheetId 单据id
* @param state 单据修改后的状态
*/
void approval(String sheetId, SheetState state);
/**
* 根据单据Id搜索单据信息
* @param sheetId 单据Id
* @return 单据
*/
SheetVO getSheetById(String sheetId);
}
// 实现类
public class xxxSheetServiceImpl implements SheetService<xxxSheetVO, xxxSheetState>

2. 设计模式

  • 策略模式:
    • 将策略方法抽象成接口,用不同的实现类实现它。
    • 在调用 service 里组合接口类(而不是具体的实现类),根据情况向接口中注入具体的类。
// 策略接口
public interface PromotionStrategy {

/**
* 根据type制定不同的促销策略
*/
public Integer makePromotionStrategy(PromotionStrategyVO promotionStrategyVO);

}

// 实现类略

// service类(调用strategy)
public Integer makePromotionStrategy(PromotionStrategyVO promotionStrategyVO){
logger.info(">>>>>>>>>>>>>>>>>>>>制定促销策略>>>>>>>>>>>>>>>>>>>>>>>>");
PromotionStrategy promotionStrategy;
// 策略模式
switch (promotionStrategyVO.getType()){
case 0:
promotionStrategy = new UserPromotionStrategy(promotionStrategyDao, customerDao);
logger.info(">>>>>>>>>>>>>>>>>>>>对不同级别用户制定促销策略...>>>>>>>>>>>>>>>>>>>>>>>>");
break;
case 1:
promotionStrategy = new SpecialPricePromotionStrategy(promotionStrategyDao);
logger.info(">>>>>>>>>>>>>>>>>>>制定特价包(组合商品降价)...>>>>>>>>>>>>>>>>>>>>>>>>");
break;
case 2:
promotionStrategy = new TotalAmountPromotionStrategy(promotionStrategyDao);
logger.info(">>>>>>>>>>>>>>>>>>>>针对不同总价制定促销策略...>>>>>>>>>>>>>>>>>>>>>>>>");
break;
default:
logger.info(">>>>>>>>>>>>>>>>>>>>错误:未指定促销策略类型!>>>>>>>>>>>>>>>>>>>>>>>>");
return null;
}
logger.info(">>>>>>>>>>>>>>>>>>>>完成促销策略制定>>>>>>>>>>>>>>>>>>>>>>>>");
return promotionStrategy.makePromotionStrategy(promotionStrategyVO);
}

3. 定时任务

  • 比较简单,主要通过 SpringBoot 的org.springframework.scheduling.annotation.Scheduled 包实现

  • 需要书写cron 表达式,跟 Linux 系统的 cron 定时任务语法相似

4. AOP(面向切面编程)

  • 思想:通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。

    • 所谓的切面(Aspect),可以简单理解为程序中的共性功能。AOP 通过关注程序中的共性功能(面),将其通过一些技术(预编译、代理等)进行统一处理,从而减少代码重复,提高效率。

    • 一般认为切面 = 通知 + 切入点

      • 切入点是指我们要对哪些 Joinpoint 进行拦截的定义,通俗的说就是被增强类中的被增强的方法,即切入的地方。注意,被增强类中并不是所有的方法都被代理了

      • 所谓通知是指拦截到 Joinpoint (被增强的方法)之后所要做的事情就是通知,通俗的说就是对被增强的方法进行增强的代码,即要做的事情

    • 典型的 AOP 应用场景有:HTTP request 的鉴权授权、日志记录等

  • 使用:

Spring Boot 使用 AOP 需要添加 spring-boot-starter-aop 依赖,如下:

<dependency>` `<groupId>org.springframework.boot</groupId>` `<artifactId>spring-boot-starter-aop</artifactId>` `</dependency>

代码实例:

// 表示当前的类是一个配置类
@Configuration
//该注解只能用在类上,作用:代表当前类是一个切面类
// 切面 == 通知 + 切入点
@Aspect
public class MyAdviceConfig {

/**
* @param joinPoint
* @Before:前置通知
* value:切入点表达式 二者加起来构建成为一个切面
* JoinPoint:连接点:可以理解为两个圆形的切点,从这个切点就可以获取到当前执行的目标类及方法
* 前置通知和后置通知的参数的都是 JoinPoint, 前置后置通知都没有返回值
*/
// 方法级别:具体到某个具体的方法
// @Before(value = "execution(* com.xxx.xxx.service.impl.*.*(..))")
// value值里可以加权限控制,比如public * com.xxx.xxx等
// 表示service包下的所有类所有方法都执行该前置通知
@Before(value = "within(com.xxx.xxx.service.*)")
public void before(JoinPoint joinPoint) {
System.out.println("before开始执行查询.......");
System.out.println("正在执行的目标类是: " + joinPoint.getTarget());
System.out.println("正在执行的目标方法是: " + joinPoint.getSignature().getName());
System.out.println("正在执行的目标方法参数是: " + joinPoint.getArgs());
}

/**
* 后置通知,属性参数同上面的前置通知
* @param joinPoint 前置通知和后置通知独有的参数
*/
@After(value = "execution(* com.xxx.xxx.service.impl.*.*(..))")
public void after(JoinPoint joinPoint) {
System.out.println("after查询结束.......");
// 获取执行目标类和方法名等等
}

/**
* @param proceedingJoinPoint 环绕通知的正在执行中的连接点(这是环绕通知独有的参数)
* @return 目标方法执行的返回值
* @Around: 环绕通知,有返回值,环绕通知必须进行放行方法(就相当于拦截器),否则目标方法无法执行)
*/
@Around(value = "execution(* com.xxx.xxx.service.impl.*.*(..))")
public Object aroud(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("aroud环绕通知开始.......");
System.out.println("执行的目标类 = " + proceedingJoinPoint.getTarget());
System.out.println("执行的目标方法 = " + proceedingJoinPoint.getSignature().getName());
// 必须方法目标方法
Object proceed = proceedingJoinPoint.proceed();
System.out.println("aroud环绕通知结束.......");
// 将目标方法的返回值进行返回,否则调用目标方法的方法无法获取到返回值
return proceed;
}
}

5. Bean 的装配

  • 这里主要记录踩到的坑,具体的知识点较为庞杂,在本周的学习中进行具体的记录

  • 主要是装配时发生了空指针异常,而异常类已经标上了相关注解。百思不得其解,最后发现 SpringBoot 对 bean 的装配是自动管理,即不需要程序员手动 new。而一旦程序员手动 new,SpringBoot 即认为程序员接管了装配,不再进行自动配置。而本人的异常即为手动 new 了对象造成。

MyBatis

  • resultType / resultMap
    • 用于指定返回类,用途相似,一般用 resultType 更方便。详细的知识点在之后的文档学习中具体记录。

Jenkins

  • Agent

    • 可以简单理解为执行 pipeline 的环境。一般选择代码构建所需的环境。
    • 在配置前端 Jenkins 时,曾遇到 npm: not found 错误。原因是 Jenkins 所在容器未安装 nodejs,也并未挂载宿主机的 nodejs 脚本。解决方法是配置 node 某个版本(我用的是 node: 14)的 agent。在 agent 中执行 npm

  • Pipeline

    • 简单的说就是 Jenkins 执行的一个个任务。可以按照环境清理(Image clear)、环境准备(Prepare)、构建(Build)、测试(Test)、打包(Deploy)等步骤细分
  • Docker in docker

    • 由于我的 Jenkins 部署在 docker 中,而有时需要在 Jenkins 环境下执行 docker 命令,即(Docker in docker),我是通过挂载宿主机的 docker 脚本和 docker.sock 实现的

Gitlab-runner

  • 基本配置

    • 基本照着网上的教程一路走下来即可。踩的一个大坑是需要手动在配置文件里指定clone_url (仓库的地址)。并且这个地址亲测不能是 ci/cd 页面给的 http 地址,必须与仓库 url 一致(如果是 https 必须是 https)。但是由此在设置artifact 时可能会带来请求拦截问题,目前尚未解决。
  • Volumes

    • 与上述 Jenkins 同样,需要挂载宿主机的 docker 脚本和 docker.sock 来实现 Docker in docker
  • Type

    • 我用的是 docker,没试过 shell 等其他类型
OLDER > < NEWER