配置命名规范

多个单词分隔符用杠 -
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:
注解在某个方法上将方法返回的对象定义为一个Bean,并使用配置文件中相应的属性初始化该Bean的属性。
|
两者结合使用
1 2 3 4 5 6 7 8 9
| @Setter @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装配顺序:
如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常
如果指定了name,则从Spring上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常
如果指定了type,则从Spring上下文中找到类型匹配的唯一bean进行装配,找不到或找到多个,都抛出异常
如果既没指定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、导入导出直接绑数据库实体,后面字段一变就容易连带出问题