1.自定义异常捕获处理
定义如下类 可捕获并处理相关异常
package com.imooc.exception;
import com.imooc.utils.IMOOCJSONResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
@RestControllerAdvice
public class CustomExceptionHandler {
    // 上传文件超过500k,捕获异常:MaxUploadSizeExceededException
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public IMOOCJSONResult handlerMaxUploadFile(MaxUploadSizeExceededException ex) {
        return IMOOCJSONResult.errorMsg("文件上传大小不能超过500k,请压缩图片或者降低图片质量再上传!");
    }
//    // 上传文件超过500k,捕获异常:MaxUploadSizeExceededException
//    @ExceptionHandler(MyTestException.class)
//    public IMOOCJSONResult handlerMaxUploadFile(MyTestException ex) {
//        return IMOOCJSONResult.errorMsg("错错错 是我的错");
//    }
}
2.脱敏
某些私密信息 如手机号 邮箱等 需要脱敏 如下是示例

package com.imooc.utils;
import sun.applet.Main;
/**
 * 通用脱敏工具类
 * 可用于:
 *      用户名
 *      手机号
 *      邮箱
 *      地址等
 */
public class DesensitizationUtil {
    private static final int SIZE = 6;
    private static final String SYMBOL = "*";
    public static void main(String[] args) {
        String name = commonDisplay("慕课网");
        String mobile = commonDisplay("13900000000");
        String mail = commonDisplay("admin@imooc.com");
        String address = commonDisplay("北京大运河东路888号");
        System.out.println(name);
        System.out.println(mobile);
        System.out.println(mail);
        System.out.println(address);
    }
    /**
     * 通用脱敏方法
     * @param value
     * @return
     */
    public static String commonDisplay(String value) {
        if (null == value || "".equals(value)) {
            return value;
        }
        int len = value.length();
        int pamaone = len / 2;
        int pamatwo = pamaone - 1;
        int pamathree = len % 2;
        StringBuilder stringBuilder = new StringBuilder();
        if (len <= 2) {
            if (pamathree == 1) {
                return SYMBOL;
            }
            stringBuilder.append(SYMBOL);
            stringBuilder.append(value.charAt(len - 1));
        } else {
            if (pamatwo <= 0) {
                stringBuilder.append(value.substring(0, 1));
                stringBuilder.append(SYMBOL);
                stringBuilder.append(value.substring(len - 1, len));
            } else if (pamatwo >= SIZE / 2 && SIZE + 1 != len) {
                int pamafive = (len - SIZE) / 2;
                stringBuilder.append(value.substring(0, pamafive));
                for (int i = 0; i < SIZE; i++) {
                    stringBuilder.append(SYMBOL);
                }
                if ((pamathree == 0 && SIZE / 2 == 0) || (pamathree != 0 && SIZE % 2 != 0)) {
                    stringBuilder.append(value.substring(len - pamafive, len));
                } else {
                    stringBuilder.append(value.substring(len - (pamafive + 1), len));
                }
            } else {
                int pamafour = len - 2;
                stringBuilder.append(value.substring(0, 1));
                for (int i = 0; i < pamafour; i++) {
                    stringBuilder.append(SYMBOL);
                }
                stringBuilder.append(value.substring(len - 1, len));
            }
        }
        return stringBuilder.toString();
    }
}
3.springboot自带定时任务的优劣
优点:
简单易用
缺点:
    /**
      * 使用定时任务关闭超期未支付订单,会存在的弊端:
      * 1. 会有时间差,程序不严谨
      *      10:39下单,11:00检查不足1小时,12:00检查,超过1小时多余39分钟
      * 2. 不支持集群
      *      单机没毛病,使用集群后,就会有多个定时任务
      *      解决方案:只使用一台计算机节点,单独用来运行所有的定时任务
      * 3. 会对数据库全表搜索,及其影响数据库性能:select * from order where orderStatus = 10;
      * 定时任务,仅仅只适用于小型轻量级项目,传统项目
      *
      * 后续课程会涉及到消息队列:MQ-> RabbitMQ, RocketMQ, Kafka, ZeroMQ...
      *      延时任务(队列)
      *      10:12分下单的,未付款(10)状态,11:12分检查,如果当前状态还是10,则直接关闭订单即可
      */
在线Cron表达式生成器
package com.imooc.config;
import com.imooc.service.OrderService;
import com.imooc.utils.DateUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class OrderJob {
    @Autowired
    private OrderService orderService;
//    @Scheduled(cron = "0/3 * * * * ?")
//    @Scheduled(cron = "0 0 0/1 * * ?")
    public void autoCloseOrder() {
        orderService.closeOrder();
        System.out.println("执行定时任务,当前时间为:"
                + DateUtil.getCurrentDateString(DateUtil.DATETIME_PATTERN));
    }
}
4.jsr303 hibernate validator 参数校验
jsr303是个规范 这里我们使用的实现框架是 hibernate validator
如下依赖中自带
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>需要被校验的BO类
package com.imooc.pojo.bo.center;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.*;
import java.util.Date;
@ApiModel(value="用户对象", description="从客户端,由用户传入的数据封装在此entity中")
public class CenterUserBO {
    /**
     * Bean Validation 中内置的 constraint
     * @Null 被注释的元素必须为 null
     * @NotNull 被注释的元素必须不为 null
     * @AssertTrue 被注释的元素必须为 true
     * @AssertFalse 被注释的元素必须为 false
     * @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
     * @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
     * @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
     * @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
     * @Size(max=, min=) 被注释的元素的大小必须在指定的范围内
     * @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
     * @Past 被注释的元素必须是一个过去的日期
     * @Future 被注释的元素必须是一个将来的日期
     * @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
     * Hibernate Validator 附加的 constraint
     * @NotBlank(message =) 验证字符串非null,且长度必须大于0
     * @Email 被注释的元素必须是电子邮箱地址
     * @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
     * @NotEmpty 被注释的字符串的必须非空
     * @Range(min=,max=,message=) 被注释的元素必须在合适的范围内
     */
    @ApiModelProperty(value="用户名", name="username", example="json", required = false)
    private String username;
    @ApiModelProperty(value="密码", name="password", example="123456", required = false)
    private String password;
    @ApiModelProperty(value="确认密码", name="confirmPassword", example="123456", required = false)
    private String confirmPassword;
    @NotBlank(message = "用户昵称不能为空")
    @Length(max = 12, message = "用户昵称不能超过12位")
    @ApiModelProperty(value="用户昵称", name="nickname", example="杰森", required = false)
    private String nickname;
    @Length(max = 12, message = "用户真实姓名不能超过12位")
    @ApiModelProperty(value="真实姓名", name="realname", example="杰森", required = false)
    private String realname;
    @Pattern(regexp = "^(((13[0-9]{1})|(15[0-9]{1})|(18[0-9]{1}))+\\d{8})$", message = "手机号格式不正确")
    @ApiModelProperty(value="手机号", name="mobile", example="13999999999", required = false)
    private String mobile;
    @Email
    @ApiModelProperty(value="邮箱地址", name="email", example="imooc@imooc.com", required = false)
    private String email;
    @Min(value = 0, message = "性别选择不正确")
    @Max(value = 2, message = "性别选择不正确")
    @ApiModelProperty(value="性别", name="sex", example="0:女 1:男 2:保密", required = false)
    private Integer sex;
    @ApiModelProperty(value="生日", name="birthday", example="1900-01-01", required = false)
    private Date birthday;
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getConfirmPassword() {
        return confirmPassword;
    }
    public void setConfirmPassword(String confirmPassword) {
        this.confirmPassword = confirmPassword;
    }
    public String getNickname() {
        return nickname;
    }
    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
    public String getRealname() {
        return realname;
    }
    public void setRealname(String realname) {
        this.realname = realname;
    }
    public String getMobile() {
        return mobile;
    }
    public void setMobile(String mobile) {
        this.mobile = mobile;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public Integer getSex() {
        return sex;
    }
    public void setSex(Integer sex) {
        this.sex = sex;
    }
    public Date getBirthday() {
        return birthday;
    }
    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }
    @Override
    public String toString() {
        return "CenterUserBO{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", confirmPassword='" + confirmPassword + '\'' +
                ", nickname='" + nickname + '\'' +
                ", realname='" + realname + '\'' +
                ", mobile='" + mobile + '\'' +
                ", email='" + email + '\'' +
                ", sex=" + sex +
                ", birthday=" + birthday +
                '}';
    }
}controller类中的方法 可以看到 要校验该BO参数只需要加上@valid注解然后 加上一个
BindingResult result 参数 然后 判断是否通过校验 再进行相应返回
   @ApiOperation(value = "修改用户信息", notes = "修改用户信息", httpMethod = "POST")
    @PostMapping("update")
    public IMOOCJSONResult update(
            @ApiParam(name = "userId", value = "用户id", required = true)
            @RequestParam String userId,
            @RequestBody @Valid CenterUserBO centerUserBO,
            BindingResult result,
            HttpServletRequest request, HttpServletResponse response) {
        // 判断BindingResult是否保存错误的验证信息,如果有,则直接return
        if (result.hasErrors()) {
//            Map<String, String> errorMap = getErrors(result);
//            return IMOOCJSONResult.errorMap(errorMap);
            return IMOOCJSONResult.errorMsg(result.getFieldErrors().get(0).getDefaultMessage());
        }
        Users userResult = centerUserService.updateUserInfo(userId, centerUserBO);
        userResult = setNullProperty(userResult);
        CookieUtils.setCookie(request, response, "user",
                JsonUtils.objectToJson(userResult), true);
        // TODO 后续要改,增加令牌token,会整合进redis,分布式会话
        return IMOOCJSONResult.ok();
    }5.文件上传基本流程
定义上传相关的路径信息
package com.imooc.resource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "file")
@PropertySource("classpath:file-upload-dev.properties")
public class FileUpload {
    private String imageUserFaceLocation;
    private String imageServerUrl;
    public String getImageServerUrl() {
        return imageServerUrl;
    }
    public void setImageServerUrl(String imageServerUrl) {
        this.imageServerUrl = imageServerUrl;
    }
    public void setImageUserFaceLocation(String imageUserFaceLocation) {
        this.imageUserFaceLocation = imageUserFaceLocation;
    }
    public String getImageUserFaceLocation() {
        return imageUserFaceLocation;
    }
}
file-upload-dev.properties
file.imageUserFaceLocation=\\workspaces\\images\\foodie\\faces
file.imageServerUrl=http://localhost:8088/foodie/faces上传 后端示例代码
主要分为如下几步
校验大小及后缀,
文件名生成 这个记得要在里面加个用户id 或者订单id啥的 后面方便找或者删
生成文件
更新到数据库
package com.imooc.controller.center;
import com.imooc.controller.BaseController;
import com.imooc.exception.MyTestException;
import com.imooc.pojo.Users;
import com.imooc.pojo.bo.center.CenterUserBO;
import com.imooc.resource.FileUpload;
import com.imooc.service.UserService;
import com.imooc.service.center.CenterUserService;
import com.imooc.utils.CookieUtils;
import com.imooc.utils.DateUtil;
import com.imooc.utils.IMOOCJSONResult;
import com.imooc.utils.JsonUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Api(value = "用户信息接口", tags = {"用户信息相关接口"})
@RestController
@RequestMapping("userInfo")
public class CenterUserController extends BaseController {
    @Autowired
    private CenterUserService centerUserService;
    @Autowired
    private FileUpload fileUpload;
    @ApiOperation(value = "用户头像修改", notes = "用户头像修改", httpMethod = "POST")
    @PostMapping("uploadFace")
    public IMOOCJSONResult uploadFace(
            @ApiParam(name = "userId", value = "用户id", required = true)
            @RequestParam String userId,
            @ApiParam(name = "file", value = "用户头像", required = true)
            MultipartFile file,
            HttpServletRequest request, HttpServletResponse response) {
        // .sh .php
        // 定义头像保存的地址
//        String fileSpace = IMAGE_USER_FACE_LOCATION;
        String fileSpace = fileUpload.getImageUserFaceLocation();
        // 在路径上为每一个用户增加一个userid,用于区分不同用户上传
        String uploadPathPrefix = File.separator + userId;
        // 开始文件上传
        if (file != null) {
            FileOutputStream fileOutputStream = null;
            try {
                // 获得文件上传的文件名称
                String fileName = file.getOriginalFilename();
                if (StringUtils.isNotBlank(fileName)) {
                    // 文件重命名  imooc-face.png -> ["imooc-face", "png"]
                    String fileNameArr[] = fileName.split("\\.");
                    // 获取文件的后缀名
                    String suffix = fileNameArr[fileNameArr.length - 1];
                    if (!suffix.equalsIgnoreCase("png") &&
                            !suffix.equalsIgnoreCase("jpg") &&
                            !suffix.equalsIgnoreCase("jpeg")) {
                        return IMOOCJSONResult.errorMsg("图片格式不正确!");
                    }
                    // face-{userid}.png
                    // 文件名称重组 覆盖式上传,增量式:额外拼接当前时间
                    String newFileName = "face-" + userId + "." + suffix;
                    // 上传的头像最终保存的位置
                    String finalFacePath = fileSpace + uploadPathPrefix + File.separator + newFileName;
                    // 用于提供给web服务访问的地址
                    uploadPathPrefix += ("/" + newFileName);
                    File outFile = new File(finalFacePath);
                    if (outFile.getParentFile() != null) {
                        // 创建文件夹
                        outFile.getParentFile().mkdirs();
                    }
                    // 文件输出保存到目录
                    fileOutputStream = new FileOutputStream(outFile);
                    InputStream inputStream = file.getInputStream();
                    IOUtils.copy(inputStream, fileOutputStream);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (fileOutputStream != null) {
                        fileOutputStream.flush();
                        fileOutputStream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } else {
            return IMOOCJSONResult.errorMsg("文件不能为空!");
        }
        // 获取图片服务地址
        String imageServerUrl = fileUpload.getImageServerUrl();
        // 由于浏览器可能存在缓存的情况,所以在这里,我们需要加上时间戳来保证更新后的图片可以及时刷新
        String finalUserFaceUrl = imageServerUrl + uploadPathPrefix
                 + "?t=" + DateUtil.getCurrentDateString(DateUtil.DATE_PATTERN);
        // 更新用户头像到数据库
        Users userResult = centerUserService.updateUserFace(userId, finalUserFaceUrl);
        userResult = setNullProperty(userResult);
        CookieUtils.setCookie(request, response, "user",
                JsonUtils.objectToJson(userResult), true);
        // TODO 后续要改,增加令牌token,会整合进redis,分布式会话
//        throw new MyTestException();
        return IMOOCJSONResult.ok();
    }
    private Map<String, String> getErrors(BindingResult result) {
        Map<String, String> map = new HashMap<>();
        List<FieldError> errorList = result.getFieldErrors();
        for (FieldError error : errorList) {
            // 发生验证错误所对应的某一个属性
            String errorField = error.getField();
            // 验证错误的信息
            String errorMsg = error.getDefaultMessage();
            map.put(errorField, errorMsg);
        }
        return map;
    }
    private Users setNullProperty(Users userResult) {
        userResult.setPassword(null);
        userResult.setMobile(null);
        userResult.setEmail(null);
        userResult.setCreatedTime(null);
        userResult.setUpdatedTime(null);
        userResult.setBirthday(null);
        return userResult;
    }
}
6.mybatis pagehelper分页注意事项

前面几点都还好理解
最后一点 意思就是 比如当前 查询结果为订单VO 他又包含许多子订单项VO
分页条数为10 如果你想要页面分页的主体是订单 也就是一个页面 十个订单
这个 时候mapper.xml就应该写成如下俩查询 如果是只用一个查询语句查出所有字段,那么分页就会根据订单项分页,也就是一页十个订单项
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.mapper.OrdersMapperCustom" >
  <resultMap id="myOrdersVO" type="com.imooc.pojo.vo.MyOrdersVO">
    <id column="orderId" property="orderId"/>
    <result column="createdTime" property="createdTime"/>
    <result column="payMethod" property="payMethod"/>
    <result column="realPayAmount" property="realPayAmount"/>
      <result column="postAmount" property="postAmount"/>
      <result column="orderStatus" property="orderStatus"/>
      <result column="isComment" property="isComment"/>
    <collection property="subOrderItemList"
                select="getSubItems"
                column="orderId"
                ofType="com.imooc.pojo.vo.MySubOrderItemVO">
      <result column="itemId" property="itemId"/>
      <result column="itemName" property="itemName"/>
      <result column="itemImg" property="itemImg"/>
<!--        <result column="itemSpecId" property="itemSpecId"/>-->
        <result column="itemSpecName" property="itemSpecName"/>
        <result column="buyCounts" property="buyCounts"/>
        <result column="price" property="price"/>
    </collection>
  </resultMap>
  <select id="queryMyOrders" resultMap="myOrdersVO" parameterType="Map">
    SELECT
        od.id as orderId,
        od.created_time as createdTime,
        od.pay_method as payMethod,
        od.real_pay_amount as realPayAmount,
        od.post_amount as postAmount,
        os.order_status as orderStatus,
        od.is_comment as isComment
    FROM
        orders od
    LEFT JOIN
        order_status os
    on od.id = os.order_id
    WHERE
        od.user_id = #{paramsMap.userId}
    AND
        od.is_delete = 0
        <if test="paramsMap.orderStatus != null">
          and os.order_status = #{paramsMap.orderStatus}
        </if>
    ORDER BY
        od.updated_time ASC
  </select>
    
    
    <select id="getSubItems" parameterType="String" resultType="com.imooc.pojo.vo.MySubOrderItemVO">
      select
        oi.item_id as itemId,
        oi.item_name as itemName,
        oi.item_img as itemImg,
        oi.item_spec_name as itemSpecName,
        oi.buy_counts as buyCounts,
        oi.price as price
      from
        order_items oi
      where
        oi.order_id = #{orderId}
    </select>
    
</mapper>7.环境切换spring.profile.active
比如我们想要配置不同环境的mysql 的url
不同环境给应用配置不同端口
还有mybatis的sql日志 生产环境没必要打印
如果在一个文件中改就很麻烦
如果配置文件基本不改 当然可以设置gitignore然后 提交时不管
比如我们想在不同环境 配置不同的bean
都可以用到这玩意
配置切换示例
application.yml
spring:
  datasource: # 数据源的相关配置
      type: com.zaxxer.hikari.HikariDataSource # 数据源类型:HikariCP
      driver-class-name: com.mysql.jdbc.Driver # mysql驱动
      url: jdbc:mysql://49.235.78.53/foodie?useUnicode=true&useSSL=false&characterEncoding=UTF-8&autoReconnect
      username: root
      password: root
#  profiles:
#    active: devapplication-dev.yml
server:
  port: 8088
spring:
  datasource:                                           # 数据源的相关配置
    url: jdbc:mysql://localhost:3306/foodie-shop-dev?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true
    password: roota
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImplapplication-prod.yml
server:
  port: 80
spring:
  datasource:                                           # 数据源的相关配置
    url: jdbc:mysql://localhost:3306/foodie-shop-dev?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true
    password: imoocbean切换示例
SpringBoot配置文件中spring.profiles.active配置多个bean不同作用环境
8.云服务器端口开放
这里以腾讯云服务器举例
点击查看详情

点击防火墙,然后添加对应规则 懒得话可以运行访问所有端口

9.jdk上传及环境变量配置
去下载 好后解压jdk
tar -zxvf jdk8.tar.gz
编辑profile文件 添加环境变量
vim /etc/profile
文件最后添加如下三行 /usr/java/jdk1.8.0_191 这个是根据你自己路径来
export JAVA_HOME=/usr/java/jdk1.8.0_191
export PATH=$JAVA_HOME/bin:$PATH 
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar 
刷新配置
source /etc/profile
测试环境变量是否生效
java -version10.部署咱们的单体应用项目并运行
添加这样一个war包的启动类
默认是jar包 后面整到微服务再换成jar

package com.imooc;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
// 打包war [4] 增加war的启动类
public class WarStarterApplication extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        // 指向Application这个springboot启动类
        return builder.sources(Application.class);
    }
}
打包方式改成war

点击install 打包到本地
可以看到 target目录下有这样一个war

上传两次tomcat 然后 解压然后改名 一个放前端项目 一个放后端项目

把我们的war包上传解压 放到tomcat下webapps目录

修改端口

修改好后启动startup.sh 然后测试 成功
 
 
前端项目部署就是直接 把对应俩项目拷贝到 tomcat-frontend下然后启动即可



















