一. 新建项目,创建UserController,增加test方法,能正常调用接口
二. 集成mybatis,打通数据库连接
-
添加mybatis 和 mysql依赖
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> <dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
-
在application.properties中配置连接信息
spring.datasource.name=imooc_mall_datasource spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.username=root spring.datasource.password=root12345
-
生成跟表格一一对应的pojo model(pojo每个字段都跟数据库完全对应) (准备工作)
-
编写 UserMapper 接口 和 UserMapper.xml, 并关联起来
·在Application上添加mapper扫描, 指定mapper接口的路径
@SpringBootApplication @MapperScan(basePackages = {"com.lc.mall.model.dao"}) public class MallApplication { public static void main(String[] args) { SpringApplication.run(MallApplication.class, args); } }
·在application.properties中指定 mapper.xml的路径
mybatis.mapper-locations=classpath:mappers/*.xml
·每个mapper和对应的mapper.xml 使用同样的文件名, 且在mapper.xml中指定namespace,跟mapper接口绑定
@Repository public interface UserMapper { User findById(Integer id); }
<?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.lc.mall.model.dao.UserMapper"> <select id="findById" parameterType="int" resultType="com.lc.mall.model.pojo.User"> select * from imooc_mall_user where id = #{id} </select> </mapper>
至此,实现mybatis的数据库打通。
三.封装接口统一返回对象(ApiResponse), 自定义Exception, 添加全局异常处理类,定义错误枚举
public class ApiRestResponse {
private Integer status;
private String message;
private Object data;
private static final Integer OK_CODE = 10000;
private static final String OK_MSG = "SUCCESS";
public ApiRestResponse(Integer status, String message) {
this.status = status;
this.message = message;
}
public static ApiRestResponse success(Object object) {
ApiRestResponse resp = new ApiRestResponse(OK_CODE, OK_MSG);
resp.setData(object);
return resp;
}
}
public class ImoocMallException extends RuntimeException {
private Integer code;
private String message;
public ImoocMallException(Integer code, String message) {
this.code = code;
this.message = message;
}
public ImoocMallException(ImoocMallExceptionEnum exceptionEnum) {
this.code = exceptionEnum.getCode();
this.message = exceptionEnum.getMessage();
}
}
public enum ImoocMallExceptionEnum {
NEED_USERNAME(10001, "用户名不能为空"),
NEED_PASSWORD(10002, "密码不能为空"),
PASSWORD_TOO_SHORT(10003, "密码长度不能小于8"),
NAME_EXISTED(10004, "不允许重名"),
INSERT_FAILED(10005, "插入失败, 请重试"),
;
private Integer code;
private String message;
ImoocMallExceptionEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}
四.日志配置(集成log4j2)
- pom.xml中添加依赖
<!-- 移除logback依赖, 后面新增log4j2依赖, 不然类名有冲突-->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
- log4j2配置文件 log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="fatal">
<Properties>
<!-- 指定日志保存路径-->
<Property name="baseDir" value="/Users/luomeng/Desktop/Backend/java/images/"/>
</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>
- 在需要记录日志的地方获取logger并记录日志
private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
logger.error("Default Exception:", exception);
五. 集成swagger实现接口文档
- 在pom.xml中添加依赖
<!-- swagger展示接口文档-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
- 在Application上添加注解
@SpringBootApplication
@MapperScan(basePackages = {"com.lc.mall.model.dao"})
@EnableSwagger2
public class MallApplication {
public static void main(String[] args) {
SpringApplication.run(MallApplication.class, args);
}
}
- 添加swagger配置类
@Configuration
public class SpringFoxConfig {
//访问http://localhost:8083/swagger-ui.html可以看到API文档
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("慕慕生鲜")
.description("")
.termsOfServiceUrl("")
.build();
}
}
- 运行项目后访问 http://localhost:8080/swagger-ui.html
- spring-boot 2.6.3 集成swagger时, 运行报错 Failed to start bean 'documentationPluginsBootstrapper';
是因为版本冲突,
解决方案是:springboot2.6.x之后将spring MVC默认路径匹配策略从ANT_PATH_MATCHER模式改为PATH_PATTERN_PARSER模式导致出错,解决方法是切换会原先的ANT_PATH_MATCHER模式
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
六. 不常修改的数据, 可以对接口做缓存
- 使用spring-boot内置缓存
·· 先在Application上打开缓存
@SpringBootApplication
@MapperScan(basePackages = {"com.lc.mall.model.dao"})
@EnableSwagger2
@EnableCaching
public class MallApplication {
public static void main(String[] args) {
SpringApplication.run(MallApplication.class, args);
}
}
· 在需要缓存的方法上加上注解
// CategoryServiceImpl 中
@Cacheable(value = "listCategoryForCustomer")
public List<CategoryVO> listCategoryForCustomer(Integer parendId) {
List<CategoryVO> result = new ArrayList<>();
findList(parendId, result);
return result;
}
- 集成redis缓存
· pom.xml中添加redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.3</version>
</dependency>
· 在application.properties中配置redis连接信息
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
· 增加缓存配置类
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.lockingRedisCacheWriter(connectionFactory);
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
cacheConfiguration = cacheConfiguration.entryTtl(Duration.ofSeconds(30));
RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter, cacheConfiguration);
return redisCacheManager;
}
}
· 在Application中打开缓存 和 在需要缓存的地方添加注解, 配置跟使用内置缓存相同
七. 使用aop记录所有请求的入参和返回值
· 在pom.xml中添加aop依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
· 添加aop配置类
@Aspect
@Component
public class WebLogAspect {
private final Logger log = LoggerFactory.getLogger(WebLogAspect.class);
@Pointcut("execution(public * com.lc.mall.controller.*.*(..))")
public void webLog(){
}
@Before("webLog()")
public void doBefore(JoinPoint joinpoint){
//收到请求,记录请求的信息
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
log.info("URL:"+request.getRequestURL());
log.info("HTTP_METHOD:"+request.getMethod());
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 doAfter(Object res) throws JsonProcessingException {
//处理完请求返回内容
log.info("RESPONSE:"+ new ObjectMapper().writeValueAsString(res));
}
}
八. 查询功能增加分页功能
· 增加pagehelper依赖 (注:版本需要跟mybatis版本兼容, 不然启动时会报错com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration)
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.1</version>
</dependency>
· 在需要实现分页的地方调用
public PageInfo listForAdmin(Integer pageNum, Integer pageSize) {
PageHelper.startPage(pageNum, pageSize, "type, order_num");
List<Category> categoryList = categoryMapper.selectList();
PageInfo pageInfo = new PageInfo(categoryList);
return pageInfo;
}
九. 请求传入接口参数校验
有的springboot版本自动集成了 spring-boot-starter-validation,但是有的版本需要自己添加依赖(springboot2.6.3就需要手动添加)
- 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
- 使用注解进行校验
@ApiOperation("更新")
@PostMapping("/update")
public ApiRestResponse update(@RequestParam("signature") @NotNull(message = "签名信息不能为空") String signature, HttpSession session) {
}
public class AddCategoryReq {
@Size(min = 3, max = 8, message = "名称长度在3-8个字符")
@NotNull(message = "名称不能为空")
private String name;
private Integer orderNum;
private Integer parentId;
@NotNull(message = "orderNum不能为null")
@Max(value = 3)
private Integer type;
}
- 当参数校验不通过时, 会抛出异常 MethodArgumentNotValidException, 需要在全局异常处理中进行处理
@RestControllerAdvice
public class GlobalExceptionHandler {
private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(value = Exception.class)
public ApiRestResponse handleException(Exception exception) {
logger.error("Default Exception:", exception);
return new ApiRestResponse(20000, "系统异常");
}
@ExceptionHandler(value = ImoocMallException.class)
public ApiRestResponse handleMallException(ImoocMallException exception) {
logger.error("ImoocMallException:", exception);
return new ApiRestResponse(exception.getCode(), exception.getMessage());
}
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ApiRestResponse handleValidException(MethodArgumentNotValidException e) {
logger.error("MethodArgumentNotValidException:", e);
return handleBindingResult(e.getBindingResult());
}
// 从异常提示中提取信息返回给前端
private ApiRestResponse handleBindingResult(BindingResult result){
//把异常处理为对外暴露的提示
List<String> list = new ArrayList<>();
if (result.hasErrors()) {
List<ObjectError> allErrors = result.getAllErrors();
for (int i = 0; i < allErrors.size(); i++) {
ObjectError objectError = allErrors.get(i);
String message = objectError.getDefaultMessage();
list.add(message);
}
}
if (list.size() == 0){
return ApiRestResponse.error(ImoocMallExceptionEnum.REQUEST_PARAM_ERROR);
}
return ApiRestResponse.error(ImoocMallExceptionEnum.REQUEST_PARAM_ERROR.getCode(), list.toString());
}
}
十. 过滤器使用
1.编写过滤器 继承 Filter
/**
* 用户过滤器
* @author Rex
* @create 2021-02-09 13:52
*/
public class UserFilter implements Filter {
public static User currentUser;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpSession session = request.getSession();
currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
if (currentUser == null) {
PrintWriter out = new HttpServletResponseWrapper((HttpServletResponse) servletResponse).getWriter();
out.write("{\n" +
" \"status\": 10007,\n" +
" \"msg\": \"NEED_LOGIN\",\n" +
" \"data\": null\n" +
"}");
out.flush();
out.close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
- 添加过滤器配置
/**
* User过滤器的配置
* @author Rex
* @create 2021-02-09 13:59
*/
@Configuration
public class UserFilterConfig {
@Bean
public UserFilter userFilter(){
return new UserFilter();
}
@Bean(name = "userFilterConf")
public FilterRegistrationBean userFilterConfig(){
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(userFilter());
filterFilterRegistrationBean.addUrlPatterns("/cart/*");
filterFilterRegistrationBean.addUrlPatterns("/order/*");
filterFilterRegistrationBean.setName("userFilterConfig");
return filterFilterRegistrationBean;
}
}
一般每个过滤器只做一件事,需要多个过滤器时, 使用相同的步骤进行配置即可
十一. 图片上传接口
- 定义接口
file.upload.dir=/Users/luomeng/Desktop/Backend/java/images/
file.upload.ip=127.0.0.1
@Component
public class Constant {
public static String FILE_UPLOAD_DIR;
@Value("${file.upload.dir}")
public void setFileUploadDir(String fileUploadDir) {
FILE_UPLOAD_DIR = fileUploadDir;
}
}
/**
* 上传文件, 保存文件到指定路径并返回图片url
* */
@PostMapping("/upload/file")
public ApiRestResponse upload(HttpServletRequest httpRequest, @RequestParam("file") MultipartFile file) {
String fileName = file.getOriginalFilename();
String fileType = fileName.substring(fileName.lastIndexOf("."));
String newFileName = UUID.randomUUID().toString() + fileType;
File fileDir = new File(Constant.FILE_UPLOAD_DIR);
File destFile = new File(Constant.FILE_UPLOAD_DIR + newFileName);
if (!fileDir.exists()) {
if (!fileDir.mkdir()) {
throw new ImoocMallException(ImoocMallExceptionEnum.MKDIR_FAILED);
}
}
try {
// 文件写入
file.transferTo(destFile);
} catch (IOException e) {
throw new ImoocMallException(ImoocMallExceptionEnum.FILE_UPLOAD_FAILED);
}
// 拼接url并返回
try {
String url = getHost(new URI(httpRequest.getRequestURL()+"")) + "/images/" + newFileName;
return ApiRestResponse.success(url);
} catch (URISyntaxException e) {
e.printStackTrace();
return ApiRestResponse.error(ImoocMallExceptionEnum.FILE_UPLOAD_FAILED);
}
}
private URI getHost(URI uri){
URI effectiveURI;
try {
effectiveURI = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), null,null, null);
} catch (URISyntaxException e) {
e.printStackTrace();
effectiveURI = null;
}
return effectiveURI;
}
- 增加配置,将返回的图片地址指定到本机图片存储的目录(配置后才能通过返回的图片链接访问图片)
@Configuration
public class ImoocMallWebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/admin/**")
.addResourceLocations("classpath:/static/admin/");
registry.addResourceHandler("/images/**")
.addResourceLocations("file:"+ Constant.FILE_UPLOAD_DIR);
registry.addResourceHandler("swagger-ui.html").addResourceLocations(
"classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations(
"classpath:/META-INF/resources/webjars/");
}
}
十二. 当遇到查询结果与预期不符, 想查看执行的sql时,如下配置可打印出mybatis最终运行的sql语句
在application.properties中配置mybatis
mybatis.mapper-locations=classpath:mappers/*.xml
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
十三. 项目中配置多环境(如:开发环境dev, 生产环境prod等)
在application.properties同级目录中分别创建各个环境的配置文件,文件名格式:application
-xx.properties, (如:application-dev.xml, application-prod.xml)
在application.properties文件中指定使用环境
spring.profiles.active=prod
一些坑:
- 当数据库的字段和pojo模型中的属性不一致时,需要在mapper.xml中指定映射关系
<resultMap id="userMap" type="com.lc.mall.model.pojo.User">
<result column="personalized_signature" property="personalizedSignature"></result>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
</resultMap>
<select id="findById" parameterType="int" resultType="com.lc.mall.model.pojo.User" resultMap="userMap">
select * from imooc_mall_user where id = #{id}
</select>
- 当像数据库插入数据时,需要同步获取自动生成的id主键时,使用 useGeneratedKeys="true" 和 keyProperty="id"同时使用
<insert id="insert" parameterType="com.lc.mall.model.pojo.User" useGeneratedKeys="true" keyProperty="id">
insert into imooc_mall_user (xxx, xxx,xxx) values (xxx,xxx,xxx)
</insert>
网友评论