配置命名规范

img_v2_43ea4bde-199b-4708-b044-5312638eb53g

多个单词分隔符用杠 -

RequestParam、PathVariable等注解区别

@RequestParam@PathVariable 注解是用于从request中接收请求的,两个都可以接收参数,关键点不同的是@RequestParam 是从request里面拿取值,而 @PathVariable 是从一个URI模板里面来填充

@RequestParam

http://localhost:8080/user/getUser?postId=14523,根据上面的这个URL,会从request中获取 post_id 的值,然后赋值给别名 postId

1
2
3
public User getUser(@RequestParam(value="post_id", required=false) String postId){
...
}

@RequestParam 支持下面四种参数:

  • defaultValue 如果本次请求没有携带这个参数,或者参数为空,那么就会启用默认值
  • name 绑定本次参数的名称,要跟URL上面的一样
  • required 这个参数是不是必须的
  • value 跟name一样的作用,是name属性的一个别名

@PathVariable

这个注解能够识别URL里面的一个模板,我们看下面的一个URL,http://localhost:8080/user/getUser/123
上面的一个url你可以这样写:

1
2
3
public User getUser(@PathVariable(value="postId") String postId){
...
}

参考CSDN - 一年e度的夏天

配置

1
2
3
注解@ConditionalOnProperty,这个注解能够控制某个configuration是否生效。
具体操作是通过其两个属性name以及havingValue来实现的,其中name用来从application.properties中读取某个属性值,如果该值为空,则返回false;
如果值不为空,则将该值与havingValue指定的值进行比较,如果一样则返回true;否则返回false。如果返回值为false,则该configuration不生效;为true则生效。

link: https://blog.csdn.net/wo541075754/article/details/104575745

1
2
3
Spring Boot中注解@ConfigurationProperties的三种使用场景: https://blog.csdn.net/wo541075754/article/details/104575745

注解在某个方法上将方法返回的对象定义为一个Bean,并使用配置文件中相应的属性初始化该Bean的属性。

两者结合使用

1
2
3
4
5
6
7
8
9
@Setter // 必须有setter才能为field赋上值
@ConditionalOnProperty(prefix = "smtp.config", name = "requestAddress")
@ConfigurationProperties(prefix = "smtp.config")
public class SmsUtil {

private String userName;

private String requestAddress;
}

依赖注入

传统方式

1
2
3
4
5
6
7
8
9
prative BaseDao baseDao;

public BaseDao getBaseDao() {
return baseDao;
}

public void setBaseDao(BaseDao baseDao) {
this.baseDao = baseDao;
}

@Autowired

由Spring提供,只按照byType注入

这样装配回去spring容器中找到类型为UserDao的类,然后将其注入进来。这样会产生一个问题,当一个类型有多个bean值的时候,会造成无法选择具体注入哪一个的情况,这个时候我们需要配合着@Qualifier使用

1
2
3
4
5
public class UserService {
@Autowired
@Qualifier(name="userDao1")
private UserDao userDao;
}

就可以通过类型和名称定位到我们想注入的对象, 取值 userDao1.属性

@Resource

由J2EE提供,默认按照byName自动注入

@Resource有两个重要的属性:name和type

Spring将@Resource注解的name属性解析为bean的名字,type属性则解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,而使用type属性则使用byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。

@Resource装配顺序:

  1. 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常

  2. 如果指定了name,则从Spring上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常

  3. 如果指定了type,则从Spring上下文中找到类型匹配的唯一bean进行装配,找不到或找到多个,都抛出异常

  4. 如果既没指定name,也没指定type,则自动按照byName方式进行装配。如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配。

@Resource的作用相当于@Autowired,只不过@Autowired按byType自动注入。

3、使用区别

(1)@Autowired与@Resource都可以用来装配bean,都可以写在字段或setter方法上

(2)@Autowired默认按类型装配,默认情况下必须要求依赖对象存在,如果要允许null值,可以设置它的required属性为false。如果想使用名称装配可以结合@Qualifier注解进行使用。

(3)@Resource,默认按照名称进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名进行名称查找。如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。

推荐使用@Resource注解在字段上,这样就不用写setter方法了,并且这个注解是属于J2EE的,减少了与Spring的耦合。

原文参考链接

常用集成

Swagger3

接口文档这类配置,适合直接沉到 SpringBoot 工程实践里,不需要单开一篇碎文。

依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>3.0.0</version>
</dependency>

启用

1
2
3
4
5
6
7
@EnableOpenApi
@SpringBootApplication
public class Swagger3Application {
public static void main(String[] args) {
SpringApplication.run(Swagger3Application.class, args);
}
}

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Swagger3接口文档")
.description("SpringBoot整合Swagger3生成接口文档")
.contact(new Contact("coderblue。", "https://www.coderblue.cn", ""))
.version("1.0")
.build();
}
}

最常用的访问地址就是:/swagger-ui/index.html

AOP 自定义日志

这类功能本质是 SpringBoot 工程里的横切能力,和基础概念分开看更顺。

自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {

String title() default "my annotation for printing log";

BusinessType businessType() default BusinessType.OTHER;

OperatorType operatorType() default OperatorType.MANAGE;

boolean isSaveRequestData() default true;

int level() default 0;
}

开启 AOP

1
2
3
4
@EnableAspectJAutoProxy
@Configuration
public class AopConfig {
}

切面定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Aspect
@Component
public class MyLogAspect {

@Pointcut("@annotation(cn.coderblue.studyaop.annotation.Log)")
public void logPointCut() {
}

@AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
handleLog(joinPoint, null, jsonResult);
}

@AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
handleLog(joinPoint, e, null);
}

@Before("logPointCut()")
public void beforePrintLog() {
System.out.println("@Before切点方法执行之前,输出日志");
}
}

最实用的点不是切点表达式背多少,而是:

1、注解放在哪一层

2、请求参数和返回值是否要记录

3、异常时是不是也要统一落日志

阿里云短信发送

短信验证码也是典型 SpringBoot 集成能力,放到工程实践里更合适。

依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.28</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.3.3</version>
</dependency>

Controller

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
@RestController
@RequestMapping("/eduMsm/msm")
@CrossOrigin
public class MsmController {

@Resource
private MsmService msmService;

@Resource
private RedisTemplate<String, String> redisTemplate;

@ApiOperation("发送短信的方法")
@GetMapping("send/{phone}")
public Result sendMsm(@PathVariable String phone) {
String code = redisTemplate.opsForValue().get(phone);
if (!StringUtils.isEmpty(code)) {
return Result.success();
}
code = RandomUtil.getFourBitRandom();
Map<String, Object> param = new HashMap<>();
param.put("code", code);
if (true) {
redisTemplate.opsForValue().set(phone, code, 15, TimeUnit.MINUTES);
return Result.success();
} else {
return Result.error().message("短信发送失败");
}
}
}

Service

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
@Override
public boolean send(Map<String, Object> param, String phone) {
if (StringUtils.isEmpty(phone)) {
return false;
}

DefaultProfile profile = DefaultProfile.getProfile(
"default",
ConstantMsmUtil.ACCESS_KEY_ID,
ConstantMsmUtil.ACCESS_KEY_SECRET);
IAcsClient client = new DefaultAcsClient(profile);

CommonRequest request = new CommonRequest();
request.setMethod(MethodType.POST);
request.setDomain("dysmsapi.aliyuncs.com");
request.setVersion("2017-05-25");
request.setAction("SendSms");

request.putQueryParameter("PhoneNumbers", phone);
request.putQueryParameter("SignName", "");
request.putQueryParameter("TemplateCode", "");
request.putQueryParameter("TemplateParam", JSONObject.toJSONString(param));

try {
CommonResponse response = client.getCommonResponse(request);
return response.getHttpResponse().isSuccess();
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

真正落地时要额外注意:

1、验证码缓存别漏掉过期时间

2、短信接口要做频控

3、签名、模板编码、密钥配置都不要写死在代码里

工程问题

多模块注入失败

SpringBoot 多模块项目里,最常见的问题之一就是依赖引进来了,但 @Autowired 还是报 no candidate bean

典型场景:

1、模块 A 依赖模块 B

2、A 里要注入 B 的 @Service@Repository

3、启动类默认只扫了 A 自己的包

这时候依赖虽然在,但 Spring 容器没有扫描到 B 的组件。

1
2
3
@SpringBootApplication(scanBasePackages = {"cn.lauy"})
public class AdminApplication {
}

或者按模块显式写:

1
2
3
@SpringBootApplication(scanBasePackages = {"cn.lauy.admin", "cn.lauy.service"})
public class AdminApplication {
}

判断这类问题时,先别怀疑依赖注入本身,先看:

1、pom 依赖是否真的引了

2、启动类扫描范围是否覆盖到目标包

3、目标类是否真的带了 @Service / @Repository

跨域配置

后端统一处理跨域时,最简单直接的方式就是配一个 WebMvcConfigurer

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class CorsConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
}

如果接口已经走网关,还要确认跨域到底应该放在网关层还是业务服务层,不然容易两边都配,最后行为不一致。

Thumbnailator 图片压缩

图片压缩和裁剪也属于常见 SpringBoot 工程能力,单独留一篇太碎,放到实践文里更顺。

依赖

1
2
3
4
5
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.8</version>
</dependency>

单图压缩

1
2
3
4
5
6
7
8
9
10
File file = new File("D:\\study-java\\src\\main\\resources\\images\\lauy.jpg");
InputStream fi = new FileInputStream(file);

String filePath = "D:\\study-java\\src\\main\\resources\\new_images\\";

Thumbnails.of(fi)
.size(750, 750)
.keepAspectRatio(false)
.outputQuality(0.7)
.toFile(filePath + System.currentTimeMillis() + "-" + file.getName());

输出到流或 BufferedImage

1
2
3
4
5
6
7
8
9
OutputStream os = new FileOutputStream(file.getAbsolutePath() + "_lauy.png");
Thumbnails.of(filePath)
.size(1280, 1024)
.toOutputStream(os);

BufferedImage thumbnail = Thumbnails.of(filePath)
.size(1280, 1024)
.asBufferedImage();
ImageIO.write(thumbnail, "jpg", new File(file.getAbsolutePath() + "_lauy.jpg"));

实际落地时优先注意:

1、PNG 透明图和 JPG 压缩效果不完全一样

2、size()scale()outputQuality() 一起调时,要先定目标是尺寸优先还是体积优先

3、批量处理时不要把大图全部一次性读进内存

Easypoi 导入导出

Excel 导入导出也是很典型的后台工程能力,保留一篇独立碎文价值不高,收进实践文更顺。

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
<version>3.2.0</version>
</dependency>

实体注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class UserInfo implements Serializable {

@Excel(name = "id")
private String id;

@Excel(name = "学生姓名", height = 20, width = 30, isImportField = "true_st")
private String name;

@Excel(name = "学生性别", replace = {"男_0", "女_1"}, suffix = "生", isImportField = "true_st")
private Integer sex;

@Excel(name = "出生日期", databaseFormat = "yyyyMMdd", format = "yyyy-MM-dd", isImportField = "true_st", width = 20)
private Date birthday;
}

导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("/export")
public List<UserInfo> findAll() {
List<UserInfo> list = userInfoMapper.findAll(null);
Workbook workbook = ExcelExportUtil.exportExcel(
new ExportParams("用户表", "UserInfo"),
UserInfo.class,
list);
try (FileOutputStream fos = new FileOutputStream("D://tyky/userInfo.xls")) {
workbook.write(fos);
} catch (IOException e) {
throw new RuntimeException(e);
}
return null;
}

导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@GetMapping("/import")
public List<UserInfo> add() {
ImportParams params = new ImportParams();
params.setTitleRows(1);
params.setHeadRows(1);
try {
List<UserInfo> list = ExcelImportUtil.importExcel(
new FileInputStream("D://tyky/userInfo.xls"),
UserInfo.class,
params);
System.out.println(list.size());
return list;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

用 Easypoi 时,最容易踩的点就是:

1、表头行数和标题行数配错,导致导入字段整体错位

2、日期格式、枚举替换规则和 Excel 实际内容对不上

3、导入导出直接绑数据库实体,后面字段一变就容易连带出问题