一、全局统一返回
1. 引入
- 首先,无论前端发来的请求后台操作成功还是失败,都需要给前端界面进行一个反馈,让用户得知自己的操作是否成功。
- 其次,Controller层中的每一个get方法都需要对数据库进行查询,如果查询成功,会得到一系列的数据,然后将这些数据封装成一个对象,并将其放在响应体中,传送给前端页面,做出显示。由于不同的方法返回的对象并不相同,所以如果直接对我们封装好的对象进行返回,显然会对前端代码的书写造成极大的阻碍,所以我们不妨重新定义一个R类,对每一个get方法中查询到的数据封装成的对象再进行一次封装,封装在R类的一个实例中,然后对此实例进行返回,由于类型得到了统一,这就使代码更加规范,这个R类实际上是一个HashMap<String, Object>
2. 代码
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public R() {
put("code", 0);
put("msg", "success");
}
public static R error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
public Integer getCode(){
return (Integer)this.get("code");
}
}
二、跨域问题
1. 问题
- 引经过网关进行路由后,从8001端口去访问88端口(网关),依然会报错,原因就是引发了CORS跨域请求,浏览器会拒绝跨域请求。(注意,前端发送请求到网关会引发跨域,网关路由到指定的微服务是后台之间路由,与前端无关,不会引发跨域)
2. 概念
- 跨域:跨域指的是浏览器不能执行其他网站的脚本,它是由浏览器的同源策略产生的,是浏览器对javascript施加的安全限制。
- 同源策略:协议、域名、端口都要相同,其中只要有一个不同都会产生跨域。

- 跨域流程:当前端发送非简单请求到后端时,需要先发送预检请求OPTIONS,如果此时服务器响应允许跨域,此时前端再发送真正的请求到服务器进行交互。

3. 解决方案
- 方案一:利用nginx部署为统一域

- 方案二:利用网关配置当此请求允许跨域,自定义配置文件,添加指定的响应头

@Configuration // gateway public class GulimallCorsConfiguration { @Bean // 添加过滤器 public CorsWebFilter corsWebFilter(){ // 基于url跨域,选择reactive包下的 UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource(); // 跨域配置信息 CorsConfiguration corsConfiguration = new CorsConfiguration(); // 允许跨域的头 corsConfiguration.addAllowedHeader("*"); // 允许跨域的请求方式 corsConfiguration.addAllowedMethod("*"); // 允许跨域的请求来源 corsConfiguration.addAllowedOrigin("*"); // 是否允许携带cookie跨域 corsConfiguration.setAllowCredentials(true); // 任意url都要进行跨域配置 source.registerCorsConfiguration("/**",corsConfiguration); return new CorsWebFilter(source); } }
4. 附加
- 简单请求和非简单请求:https://developer.mozilla.org/zh-CN/docs/web/http/cors
三、逻辑删除
1. 基本概念
- 大多数的情况下,用户在前端页面进行数据删除时,并不会真的删除数据,只是利用mybats-plus的逻辑删除功能,此时,我们再调用删除方法删除此字段时,发送的sql语句实际是update语句,将数据库中对应逻辑删除的相应字段的值改变。(其实查询什么的也会变,会增加额外的条件)
2. 实现
- 在
application.yml文件中加入配置mybatis-plus: global-config: db-config: id-type: auto logic-delete-value: 1 #逻辑已删除的值,默认为1 logic-not-delete-value: 0 #逻辑未删除的值,默认为0 - 找到表对应的实体类,将表需要进行逻辑删除的字段对应的属性上加上
@TableLogic注解
四、阿里云对象存储
1. 引入
- 我们在前端页面上传的商品的图片信息如果放在我们的服务器上,随着数据量的不断增大,会对内存造成很大的压力,所以我们可以选择将其放在第三方提供的平台上,在本项目中选择阿里云对象存储。此时,我们创建一个第三方服务模块,专门用来管理一些第三方服务(此步项目中做了,在此省略不写)。
2. 实现
首先我们需要在阿里云平台注册登录,创建一个属于我们自己的Bucket来存储前端传来的中的图片。随后,既可以进行对图片的传输,主要有三种方式。
2.1 方式一:利用原生的SDK进行上传
参照阿里云的相关文档,在
pom.xml中导入sdk相关依赖<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.8.0</version> </dependency>利用阿里云官方文档提供的代码即可上传
// Endpoint以杭州为例,其它Region请按实际情况填写。 String endpoint = "http://oss-cn-hangzhou.aliyuncs.com"; // 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。 String accessKeyId = "<yourAccessKeyId>"; String accessKeySecret = "<yourAccessKeySecret>"; // 创建OSSClient实例 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); // 上传文件流 InputStream inputStream = new FileInputStream("<yourlocalFile>"); ossClient.putObject("<yourBucketName>", "<yourObjectName>", inputStream); // 关闭OSSClient ossClient.shutdown();注意:为了保障安全性,我们不能直接将登陆阿里云的账号密码写进代码中进行图片的传输,而是需要为我们的账户创建一个子账户,并为其开通权限,将其账号密码写在代码中,以后,我们就通过子账号管理阿里云存储服务中的中图片
2.2 方式二:利用SpringCloudAlibaba的OSS服务进行上传
参照SpringCloudAlibaba的官方文档,在
pom.xml中导入starter<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alicloud-oss</artifactId> <version>2.2.0.RELEASE</version> </dependency>将OSS服务对应的accessKey、sercetKey、endpoint写入配置文件
spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848 alicloud: access-key: LTAI5tGToHFUkFWYYMuzZfzq secret-key: tYJoi7EdAUO04XC9oz7KNGsi9RH3WS oss: endpoint: oss-cn-beijing.aliyuncs.com注入OSSClient进行文件的上传
@Autowired OSSClient ossClient; @Test public void testUpload() throws FileNotFoundException { // 上传文件流。 InputStream inputStream = new FileInputStream("<yourlocalFile>"); ossClient.putObject("<yourBucketName>", "<yourObjectName>", inputStream); // 关闭OSSClient。 ossClient.shutdown(); }
2.3 方式三:利用签名
2.3.1 引入
- 以上两种上传方式,每次上传都需要把图片先上传到服务器中,在经过服务器来传送到阿里云存储服务,费时费力。所以我们采用新的策略,只需要要从服务器中获取签名,客户端拿着签名和图片直接上传到阿里云服务器。

2.3.2 实现
在方式二的基础上,配置文件的oss下新加一个
bucket的配置(只是为了能够动态改变bucket名称,不是oss自带的属性,是我们自己设置的)返回签名的代码如下:
@RestController public class OssController { @Autowired OSS ossClient; @Value("${spring.cloud.alicloud.oss.endpoint}") private String endpoint; @Value("${spring.cloud.alicloud.oss.bucket}") private String bucket; @Value("${spring.cloud.alicloud.access-key}") private String accessId; @RequestMapping("/oss/policy") public R policy(){ // 填写Host名称,格式为https://bucketname.endpoint。 String host = "https://" + bucket + "." + endpoint; // 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。 String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); String dir = format + "/"; Map<String, String> respMap = null; try { long expireTime = 30; long expireEndTime = System.currentTimeMillis() + expireTime * 1000; Date expiration = new Date(expireEndTime); // PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。 PolicyConditions policyConds = new PolicyConditions(); policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000); policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir); String postPolicy = ossClient.generatePostPolicy(expiration, policyConds); byte[] binaryData = postPolicy.getBytes("utf-8"); String encodedPolicy = BinaryUtil.toBase64String(binaryData); String postSignature = ossClient.calculatePostSignature(postPolicy); respMap = new LinkedHashMap<String, String>(); respMap.put("accessid", accessId); respMap.put("policy", encodedPolicy); respMap.put("signature", postSignature); respMap.put("dir", dir); respMap.put("host", host); respMap.put("expire", String.valueOf(expireEndTime / 1000)); // respMap.put("expire", formatISO8601Date(expiration)); } catch (Exception e) { // Assert.fail(e.getMessage()); System.out.println(e.getMessage()); } finally { ossClient.shutdown(); } return R.ok().put("data", respMap); } }
五、后台JSR303校验
1. 问题
项目中,只在前端进行数据的校验是远远不够的,因为如果黑客绕过前端,直接发送不合规则的数据到后端,而后端又不对这些数据进行校验,则会引发一系列异常,影响项目的正常运行,所以我们需要在后端也进行数据的校验。
2. JSR303
2.1 基本校验功能
- 给Controller中的对象参数的所属类的需要进行校验的属性,添加校验注解,这些注解都在
javax.validation.constraints,比如@Min、@NotNull、@URL等,同时我们还可以修改这些注解的message来指定如果不符合校验规则,应该返回什么信息。 - 在Controller的方法的使用到该类的形参之前标注
@Valid注解,来开启校验功能,开启后,若校验错误则返回提示(默认或自己上一步指定的)。 - 在开启校验的参数后,紧跟一个
BindingResult类型的形参(必须紧跟),就可以获取到校验的结果,该类封装了校验是否出错、如果错误有哪些提示。
2.2 进阶一:统一异常处理
2.2.1 基本概念
- 对于出现错误时的异常处理大多都是相通的,如果我们在每此出错都要编写这些代码,会导致代码的冗余,也不符合规范。因此,我们不妨将对异常的处理抽取出来,单独创建一个异常处理类,当发生错误时,统一进行处理。
2.2.2 步骤
- 新建一个
exception包,并在该包下创建一个异常处理类GulimallExceptionControllerAdvice。 - 使用SpirngMV提供的
@ControllerAdvice注解标注此类是一个异常处理类,并通过其basePackage属性说明可以处理哪个包下抛出的异常。 - 使用
@ResponseBody注解指示将此类返回的对象,封装成json的形式,返回到前端(这两个注解可以用@RestControllerAdvice代替)。 - 在类中定义方法,方法名上标注注解
@ExceptionHandler,配置该注解的value属性,指示该注解可以处理那些异常,如果发生了异常且被捕获,则该异常会自动传入该方法的形参中,我们可以通过操作方法的参数来对异常进行处理。 - 如果异常处理类中不能精确匹配到抛出异常,则找更大范围可匹配该抛出的异常的处理方法。
2.3 进阶二:枚举类
- 由于一个项目中可能会有许许多多的异常,所以我们可以在common模块中利用枚举,指定每个异常的状态码和提示信息,方便统一调用。
2.4 进阶三:分组校验
2.4.1 基本概念
- 在不同的情况下,我们需要对数据进行不同的校验,比如说,当我们新增一条数据时,对应方法传入的参数中一般不能具有id属性,然而,如果我们要修改一条数据时,一般又要求对应方法传入的参数中id属性必须传入,因此,这就要求我们在不同的场景下,对传入的参数进行不同的校验
24.2 基本步骤
- 分组校验的组别要求是按照接口进行分类,由于我们在很多微服务下都需要利用这些组别进行分组校验,所以我们要将这个接口定义在公共包common下,所以我们在common下创建一个
valid包,在其中创建不同的接口,代表不同的组别。 - 在方法形参对应的类(一般是实体类或者VO)中,利用
javax.validation.constraints包下的注解的groups属性设置组别,规定该注解在哪个组别下,也就是说,该属性在什么情况下可以使用该校验。 - 将Controller中的
@Valid注解都替换称@Validated注解,并在其value属性中设置组别,指定此种情况下对该参数应该使用哪个组别的校验,也就是说,在该情况下,应该使用该属性的哪个校验。 - 注意:没有指定分组的校验注解,在分组校验(@Validated注解中指明了组别)的情况下不起作用,只有在不分组(@Validated注解中未指明组别)的情况下才有作用。
2.5 进阶四:自定义校验
2.51 基本概念
- 有的时候,系统提供的校验注解,可能并不会符合我们预期的功能,此时则需要我们自定义校验注解,由于此校验注解可能用在各个微服务中,所以我们可以将其写在公共模块common中
2.5.2 步骤
在
pom.xml文件中导入javax.validation-api<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency>在公共模块common中,自定义一个注解,该注解必须具有四个元注解,同时必须具有三个方法
@Documented @Constraint(validatedBy = { ListValueConstraintValidator.class })//指定该注解使用哪个校验器进行校验,如果此处不指定,则在初始化时指定;一个注解可以指定多个校验器,可以根据被标注注解的属性的类型属性自动进行适配 @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME)String message() default "{com.atguigu.common.valid.ListValue.message}"; //当校验出错后,默认ValidationMessages.properties中获取错误信息 Class<?>[] groups() default { };//默认支持分组功能 Class<? extends Payload>[] payload() default { };//自定义负载信息在类路径下,创建一个
ValidationMessages.properties,书写默认的错误信息com.atguigu.common.valid.ListValue.message=必须提交指定的值的范围(zhidingdezhi)//配置与上面message方法default中的值对应自定义该注解的属性(其实也就是添加自己的校验规则,比如,只能传入0,1两个值)
int[] vals() default { };//自定义该注解的属性自定义一个校验器,该必须继承ConstraintValidator接口,该接口有两个泛型,前者指定绑定了哪个注解,后者指定要校验的数据的类型。同时,我们重写的方法中,书写真正的校验逻辑(比如,不传入0,1两个值就报错)
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> { private Set<Integer> set = new HashSet<>(); //初始化方法,constraintAnnotation接收了注解中的详细信息 @Override public void initialize(ListValue constraintAnnotation) { int[] vals = constraintAnnotation.vals(); for (int val : vals) {//最好进行非空判断,防止遍历没数据 set.add(val); } } //判断是否校验成功 /** * * @param integer 被注解的属性获得的值 * @param constraintValidatorContext 上下文环境信息 * @return */ @Override public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) { return set.contains(integer); } }注意:一个注解可以指定多个校验器,可以根据被标注注解的属性的类型属性自动进行适配
六、VO、TO的使用
1. 引入
很多时候,Controller中的方法的形参并不会跟前端发来的参数完全的匹配,比如当前端发来了新增一个商品属性的请求时,我不光要在attr表中新增一条记录,还需要知道该属性究竟属于哪个属性分组,并在attr_attrgroup表中为该属性和对应分组简历关联。此时我们可以选择在数据库的表对应的实体类中新增一个属性,并标注上@TableField()通过设置其exist的属性来表明该字段实际并不存在于数据库的表中,但这并不符合规范,于是,我们选择新建一个与前端传来的参数相匹配的VO对象,并将其作为Controller方法的形参,我们在方法中只需要拷贝VO对象中我们需要的属性即可。同时,如果返回的数据和前端页面需要的参数不符,我们也可以用VO作为中间载体,返回页面的对象最终只含有从VO中拷贝的部分属性。
2. 不同的Object的划分
2.1 PO(persistant object)持久对象
- PO就是对应数据库中某个表中的一条记录,多个记录可以用PO的集合。PO中应该不包含任何对数据库的操作。
2.2 DO(Domain Object)领域对象
- DO就是从现实世界中抽象出来的有形或无形的业务实体。
2.3 TO(Transfer Object) 数据传输对象
- TO是不同的应用程序之间传输的对象 。
2.4 DTO(Data Transfer Object)数据传输对象
- 这个概念来源于J2E 的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,泛指用于展示层与服务层之间的数据传输对象。
2.5 VO(value object)值对象
- 通常用于业务层之间的数据传递,和PO 一样也是仅仅包含数据而已。但应是抽象出的业务对象 , 可以和表对应 , 也可以不 , 这根据业务的需要 。用new关键字创建,由GC回收。
- Viewobject:视图对象; 接受页面传递来的数据,封装对象,同时将业务处理完成的对象,封装成页面要用的数据
2.6 BO(business object)
- 业务对象从业务模型的角度看见UML元件领域模型中的领域对象。封装业务逻辑的java对象 , 通过调用 DAO 方法, 结合P、VO 进行业务操作。
- business object:业务对象主要作用是把业务逻辑封装为一个对象。这个对象可以包括一个或多个其它的对象。 比如一个简历,有教育经历、工作经历、社会关系等等。 我们可以把教育经历对应一个 PO,工作经历对应一个 PO,社会关系对应一个 PO。 建立一个对应简历的BO对象处理简历,每个BO包含这些 PO 。这样处理业务逻辑时,我们就可以针对BO去处理。
2.7 POJO(plain ordinary java object)简单无规则 java 对象
- 传统意义的java对象。就是说在一些 Object/Relation Mapping工具中,能够做到维护数据库表记录的persisent object完全是一个符合Java Bean规范的纯 Java 对象,没有增加别的属性和方法。我的理解就是最基本的 java Bean ,只有属性字段及 setter 和 getter 方法。
- POJO 是 DO/DTO/BO/VO 的统称。
2.8 DAO(data access object) 数据访问对象
- DAO是一个sun的一个标准 j2ee 设计模式, 这个模式中有个接口就是 DAO ,它负责持久层的操作。为业务层提供接口。此对象用于访问数据库。通常和 PO 结合使用, DAO中包含了各种数据库的操作方法。通过它的方法 , 结合 PO 对数据库进行相关的操作。夹在业务逻辑与数据库资源中间。配合VO, 提供数据库的 CRUD 操作。