我们接下来。再对application.properties这文件进行下基础的配置:
spring.datasource.name=shop spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shop?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.username=root spring.datasource.password=zc20020106 server.port=8083 mybatis.mapper-locations=classpath:mappers/*.xml
下面我们做一个简单的查询来测试一下我们的项目和数据库方面是否初始化成功。
我们之前生成的dao包里的mappers并没有自动生成spring注解,我们可以手动给他们都加上@Repository注解。我们还要给我们应用的启动类添加一个@MapperScan(basePackages = “dao包名”)的注解。这个注解用于扫描mapper映射,指向我们的dao包。
package com.haiexijun.mall; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = "com.haiexijun.mall.model.dao") public class MallApplication { public static void main(String[] args) { SpringApplication.run(MallApplication.class, args); } }
下面就创建好controller包、service包,并编写如下的类和接口:
service包下创建UserService接口,这个接口不用加注解:
package com.haiexijun.mall.service; import com.haiexijun.mall.model.pojo.User; public interface UserService { public User getUser(); }
在service包下面再创建一个impl包,用于存放接口的实现类,下面是UserServiceImpl类,我们在这个接口里面添加@Service注解,并在类里面用@Autowired通过类型引入UserMapper,我们通过UserMapper的selectByPrimaryKey方法通过主键查询数据:
package com.haiexijun.mall.service.impl; import com.haiexijun.mall.model.dao.UserMapper; import com.haiexijun.mall.model.pojo.User; import com.haiexijun.mall.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserServiceImpl implements UserService { @Autowired UserMapper userMapper; @Override public User getUser() { return userMapper.selectByPrimaryKey(4); } }
最后我们在controller包下面创建好UserController,并在里面引入UserService,配置好请求参数。
package com.haiexijun.mall.controller; import com.haiexijun.mall.model.pojo.User; import com.haiexijun.mall.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; /** * 描述:用户控制器 */ @Controller public class UserController { @Autowired UserService userService; @GetMapping("/test") @ResponseBody public User personalPage(){ return userService.getUser(); } }
之后,就可以运行项目了。浏览器中运行结果如下,成功查询到数据:
3.配置log4j2日志组件
log4j2日志组件是我们开发中必不可少的一个关键组成部分。日志的级别(从高到低)有:error、warn、info、debug、trace。
我们在安装log4j2日志组件前,先要排除之前的日志组件,先添加如下的配置:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency>
还要再引入log4j2的依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>
这个依赖不用设置版本号,因为它会工具我们spring-boot的版本自动匹配版本号。
下面我们还要在resources目录下创建一个log4j2的配置文件log4j2.xml,如下:
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="fatal"> <Properties> <!--日志文件的存放位置,要在linux中创建,在windows里面对应C盘用户目录下的logs目录,value可自己更改--> <Property name="baseDir" value="${sys:user.home}/logs"/> </Properties> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) --> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="[%d{MM:dd HH:mm:ss.SSS}] [%level] [%logger{36}] - %msg%n"/> </Console> <!--debug级别日志文件输出--> <RollingFile name="debug_appender" fileName="${baseDir}/debug.log" filePattern="${baseDir}/debug_%i.log.%d{yyyy-MM-dd}"> <!-- 过滤器 --> <Filters> <!-- 限制日志级别在debug及以上在info以下 --> <ThresholdFilter level="debug"/> <ThresholdFilter level="info" onMatch="DENY" onMismatch="NEUTRAL"/> </Filters> <!-- 日志格式 --> <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/> <!-- 策略 --> <Policies> <!-- 每隔一天转存 --> <TimeBasedTriggeringPolicy interval="1" modulate="true"/> <!-- 文件大小 --> <SizeBasedTriggeringPolicy size="100 MB"/> </Policies> </RollingFile> <!-- info级别日志文件输出 --> <RollingFile name="info_appender" fileName="${baseDir}/info.log" filePattern="${baseDir}/info_%i.log.%d{yyyy-MM-dd}"> <!-- 过滤器 --> <Filters> <!-- 限制日志级别在info及以上在error以下 --> <ThresholdFilter level="info"/> <ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/> </Filters> <!-- 日志格式 --> <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/> <!-- 策略 --> <Policies> <!-- 每隔一天转存 --> <TimeBasedTriggeringPolicy interval="1" modulate="true"/> <!-- 文件大小 --> <SizeBasedTriggeringPolicy size="100 MB"/> </Policies> </RollingFile> <!-- error级别日志文件输出 --> <RollingFile name="error_appender" fileName="${baseDir}/error.log" filePattern="${baseDir}/error_%i.log.%d{yyyy-MM-dd}"> <!-- 过滤器 --> <Filters> <!-- 限制日志级别在error及以上 --> <ThresholdFilter level="error"/> </Filters> <!-- 日志格式 --> <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/> <Policies> <!-- 每隔一天转存 --> <TimeBasedTriggeringPolicy interval="1" modulate="true"/> <!-- 文件大小 --> <SizeBasedTriggeringPolicy size="100 MB"/> </Policies> </RollingFile> </Appenders> <Loggers> <Root level="debug"> <AppenderRef ref="Console"/> <AppenderRef ref="debug_appender"/> <AppenderRef ref="info_appender"/> <AppenderRef ref="error_appender"/> </Root> </Loggers> </Configuration>
4. AOP统一处理web请求日志
这是对于系统健壮性的一种保证,在真实的项目中,基本上都会有这样的功能,我们会用filter,把每个请求都给打印出来,这样的良好习惯可以提高我们开发和调试的效率。我们这一节的目的就是创建filter,把我们的请求信息和返回信息给打印出来。
下面就来实操演示,究竟应该如何配置这个功能:
要想引入AOP的功能,我们首先要引入一个依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
有了这个依赖之后,我们先要去建立一个过滤器,来对我们的请求来做一个拦截和打印。我们新建一个filter包,里面用来存放过滤器。
我们在里面创建一个WebLogAspect类:
package com.haiexijun.mall.filter; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.mvc.condition.RequestConditionHolder; import javax.servlet.http.HttpServletRequest; import java.util.Arrays; /** * 描述:打印请求和响应信息 */ @Aspect @Component public class WebLogAspect { private final Logger log = LoggerFactory.getLogger(WebLogAspect.class); @Pointcut("execution(public * com.haiexijun.mall.controller.*.*(..))") public void webLog(){ } //捕获,并打印请求的相关信息 @Before("webLog()") public void doBefore(JoinPoint joinPoint){ //收到请求,记录请求内容 ServletRequestAttributes attributes= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request=attributes.getRequest(); //打印请求的url log.info("URL : " + request.getRequestURL().toString()); //打印请求方法 log.info("HTTP_METHOD : " +request.getMethod()); //打印请求的Ip地址 log.info("IP : " + request.getRemoteAddr()); //打印请求处理类的一些信息 log.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName()+"."+joinPoint.getSignature().getName()); //打印请求的参数 log.info("ARGS : "+ Arrays.toString(joinPoint.getArgs())); } //捕获,并打印响应相关的信息 @AfterReturning(returning = "res",pointcut = "webLog()") public void doAfterReturning(Object res) throws JsonProcessingException { //处理完请求,返回内容 log.info("RESPONSE : "+new ObjectMapper().writeValueAsString(res)); } }
浏览器运行,访问一下/test接口,控制台打印了请求和响应的相关信息。
三.用户模块的开发
1.用户模块整体介绍
这个模块会有登录,注册,注册会有重名校验,用户名不允许重复。同时,为了更加安全,我们还会对密码进行加密存储。在登录的时候我们是用session,可以把用户登录的信息给保存下来。还会进行越权校验,比如说没有登陆的情况下,它不允许修改,以及只能修改自己用户的一些信息等。之后我们还会学习统一响应对象的概念,这个概念非常的重要,他会贯穿我们整个项目。而在构建这个对象的时候,我们也会对异常进行处理,会构建java异常体系。之后会对postman进行一些使用上的讲解。以及统一异常处理的功能,因为异常我们如果不对其进行处理的话会有安全风险。所以我们用过滤器对其统一处理。
接口设计,我们会用到一个接口文档,我们可以先用这个文档来开发,后面会学习如何自己生成接口文档。
2.API统一返回对象
我们要创建一个包叫common包,这个包的作用就是用来存储一些统一的、通用的类,里面创建一个ApiRestResponse类,这个类就是我们的统一响应对象。
package com.haiexijun.mall.common; import com.haiexijun.mall.exception.MallExceptionEnum; /** * 描述:通用返回对象 */ public class ApiRestResponse<T> { // 状态码 private Integer status; // 响应信息 private String msg; // 响应的数据data private T data; //响应成功为10000。这个状态数是我们自己定义的。 private static final int OK_CODE =10000; private static final String OK_MSG="SUCCESS"; public ApiRestResponse() { //无参构造方法为默认的响应信息 this(OK_CODE,OK_MSG); } public ApiRestResponse(Integer status, String msg, T data) { this.status = status; this.msg = msg; this.data = data; } public ApiRestResponse(Integer status, String msg) { this.status = status; this.msg = msg; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } // 为了方便调试,打印出来,故重写toString方法 @Override public String toString() { return "ApiRestResponse{" + "status=" + status + ", msg='" + msg + '\'' + ", data=" + data + '}'; } //请求成功时调用,无返回结果 public static <T> ApiRestResponse<T> success(){ //成功就调用无参的构造方法就行 return new ApiRestResponse<T>(); } // 请求成功调用的重载,有返回结果 public static <T> ApiRestResponse<T> success(T result){ ApiRestResponse<T> response=new ApiRestResponse<T>(); response.setData(result); return response; } public static <T> ApiRestResponse<T> error(MallExceptionEnum ex){ return new ApiRestResponse<T>(ex.getCode(), ex.getMsg()); } public static <T> ApiRestResponse<T> error(Integer code,String msg){ return new ApiRestResponse<T>(code,msg); } }
我们还要考虑到用户输入了错误的信息之后,会返回错误信息和异常。所以我们要创建一个异常处理类,创建exception包,先在里面创建一个异常枚举类,里面有一些异常的枚举。
package com.haiexijun.mall.exception; /** * 描述类:异常枚举类 */ public enum MallExceptionEnum { NEED_USER_NAME(10001,"用户名不能为空"), PASSWORD_TOO_SHORT(10003,"密码长度不能小于八位"), NEED_USER_PASSWORD(10002,"用户密码不能为空"), NAME_EXISTED(10004,"用户名已存在,注册失败"), INSERT_FAILED(10005,"插入失败,请重试"), SYSTEM_ERROR(20000,"系统异常"); //异常码 Integer code; //异常信息 String msg; MallExceptionEnum(Integer code, String msg) { this.code = code; this.msg = msg; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } }
然后还要在里面创建一个异常类:
package com.haiexijun.mall.exception; /** * 描述: 统一异常描述类 */ public class MallException extends Exception{ private final Integer code; private final String message; public MallException(Integer code, String message) { this.code = code; this.message = message; } public MallException(MallExceptionEnum exceptionEnum){ this(exceptionEnum.code, exceptionEnum.msg); } public Integer getCode() { return code; } @Override public String getMessage() { return message; } }
关于异常的响应,我们系统接受到请求后,会对进行一系列操作,其中就可能会有异常产生,并返回,我们要对异常进行处理一下在返回给用户。这就是统一处理异常的类GlobalExceptionHandler。
package com.haiexijun.mall.exception; import com.haiexijun.mall.common.ApiRestResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; /** * 描述 : 用于统一处理异常的类 */ //@ControllerAdvice的作用就是拦截异常的 @ControllerAdvice public class GlobalExceptionHandler { private final Logger log= LoggerFactory.getLogger(GlobalExceptionHandler.class); //系统错误的异常 @ExceptionHandler(Exception.class) @ResponseBody public Object handleException(Exception e){ log.error("Default Exception : " +e); return ApiRestResponse.error(MallExceptionEnum.SYSTEM_ERROR); } //处理自定义的异常 @ExceptionHandler(MallException.class) @ResponseBody public Object MallException(MallException e){ log.error("MallException : " +e); return ApiRestResponse.error(e.getCode(),e.getMessage()); } }
3.注册接口的开发
我们注册就要进行插入操作,在UserService类中添加一个注册的方法:
void register(String userName,String password) throws MallException;
并在他的实现类UserServiceImpl中对其进行实现:
@Override public void register(String userName, String password) throws MallException { //查询用户名是否存在,不允许重名 User result=userMapper.selectByName(userName); if (result !=null){ //如果重名就抛出异常,把它作为异常进行抛出去 throw new MallException(MallExceptionEnum.NAME_EXISTED); } //如果没有重名,就把注册的用户信息写到数据库中,注册成功 User user=new User(); user.setUsername(userName); user.setPassword(password); //userMapper的insertSelective方法会先判断插入数据的字段是否为空,如果不是空,才对他进行插入。insert方法则全部插入 //方法返回插入成功的条数 int count= userMapper.insertSelective(user); // 下面判断一下是否插入成功 if (count==0){ throw new MallException(MallExceptionEnum.INSERT_FAILED); } }
我们注册涉及到要对比用户名是否有重复,所以,我们要在UserMapper类中添加一个新的查询方法,并在对应的xml文件进行配置:
User selectByName(String userName);
<select id="selectByName" parameterType="java.lang.String" resultMap="BaseResultMap"> select <include refid="Base_Column_List"/> from mall_user where username=#{userName,jdbcType=VARCHAR} </select>
最后就可以在UserController中编写注册的接口了:
package com.haiexijun.mall.controller; import com.haiexijun.mall.common.ApiRestResponse; import com.haiexijun.mall.exception.MallException; import com.haiexijun.mall.exception.MallExceptionEnum; import com.haiexijun.mall.model.pojo.User; import com.haiexijun.mall.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; /** * 描述:用户控制器 */ @Controller public class UserController { @Autowired UserService userService; @GetMapping("/test") @ResponseBody public User personalPage(){ return userService.getUser(); } @PostMapping("/register") @ResponseBody public ApiRestResponse register(@RequestParam("userName") String userName, @RequestParam("password") String password) throws MallException { //注册校验 //StringUtils是springboot提供的字符串判断类 if(StringUtils.isEmpty(userName)){ //如果用户名为空 return ApiRestResponse.error(MallExceptionEnum.NEED_USER_NAME) ; } if (StringUtils.isEmpty(password)){ //如果密码为空 return ApiRestResponse.error(MallExceptionEnum.NEED_USER_PASSWORD); } if (password.length()<8){ //如果密码长度太小,也要报异常,这里为不小于八位 return ApiRestResponse.error(MallExceptionEnum.PASSWORD_TOO_SHORT); } //如果密码不小于八位,而且一切正常的话 userService.register(userName,password); return ApiRestResponse.success(); } }