48. 在父项目中管理子模块项目使用的依赖
在一个项目中,如果某些依赖只是部分子模块项目需要使用的,应该将这些依赖配置在<dependencyManagement>
节点中,凡配置在这个节点中的依赖,任何子模块项目中都不会直接拥有,如果某个子模块项目需要使用这些依赖,依然需要使用<dependency>
节点来添加!与在子模块项目中直接添加<denpendency>
(父级的<dependencyManagement>
没有配置某个依赖而子模块项目中直接添加)的区别在于:如果事先使用父级项目的<dependencyManagement>
进行了配置,则子模块项目在添加时,不需要指定版本号,直接使用父级项目配置的版号,以便于在父级项目中统一管理依赖的版本 !
注意:在父级项目中,添加许多依赖都是不需要指定版本号的,但是,如果将这些依赖配置到<dependencyManagement>
中用于指导子模块项目所使用的依赖的版本时,必须显式的指定版本号,否则,子模块项目将不明确需要使用的是哪个版本!
则在父级项目中,关于依赖的管理:
< properties>
<!-- Java Version -->
< java.version> 1.8</ java.version>
<!-- Dependency Version -->
< mysql.version> 8.0.20</ mysql.version>
< mybatis.version> 2.1.3</ mybatis.version>
< mybatis.plus.version> 3.3.2</ mybatis.plus.version>
< druid.version> 1.1.23</ druid.version>
< pagehelper.version> 1.2.13</ pagehelper.version>
< thymeleaf.springsecurity5.version> 3.0.4.RELEASE</ thymeleaf.springsecurity5.version>
< spring.boot.starter.version> 2.3.1.RELEASE</ spring.boot.starter.version>
< lombok.version> 1.18.12</ lombok.version>
</ properties>
<!-- 直接添加在dependencies节点的中的依赖是每个子模块项目都直接拥有的 -->
< dependencies>
< dependency>
< groupId> org.springframework.boot</ groupId>
< artifactId> spring-boot-starter-web</ artifactId>
< version> ${spring.boot.starter.version}</ version>
</ dependency>
< dependency>
< groupId> org.springframework.boot</ groupId>
< artifactId> spring-boot-starter-test</ artifactId>
< scope> test</ scope>
< exclusions>
< exclusion>
< groupId> org.junit.vintage</ groupId>
< artifactId> junit-vintage-engine</ artifactId>
</ exclusion>
</ exclusions>
</ dependency>
</ dependencies>
<!-- 添加在dependencyManagement中的依赖只是为了管理子模块项目使用依赖时的版本 -->
< dependencyManagement>
< dependencies>
< dependency>
< groupId> com.github.pagehelper</ groupId>
< artifactId> pagehelper-spring-boot-starter</ artifactId>
< version> ${pagehelper.version}</ version>
</ dependency>
< dependency>
< groupId> org.thymeleaf.extras</ groupId>
< artifactId> thymeleaf-extras-springsecurity5</ artifactId>
< version> ${thymeleaf.springsecurity5.version}</ version>
</ dependency>
< dependency>
< groupId> org.springframework.boot</ groupId>
< artifactId> spring-boot-starter-thymeleaf</ artifactId>
< version> ${spring.boot.starter.version}</ version>
</ dependency>
< dependency>
< groupId> org.springframework.boot</ groupId>
< artifactId> spring-boot-starter-validation</ artifactId>
< version> ${spring.boot.starter.version}</ version>
</ dependency>
< dependency>
< groupId> org.springframework.boot</ groupId>
< artifactId> spring-boot-starter-security</ artifactId>
< version> ${spring.boot.starter.version}</ version>
</ dependency>
< dependency>
< groupId> com.alibaba</ groupId>
< artifactId> druid-spring-boot-starter</ artifactId>
< version> ${druid.version}</ version>
</ dependency>
< dependency>
< groupId> org.mybatis.spring.boot</ groupId>
< artifactId> mybatis-spring-boot-starter</ artifactId>
< version> ${mybatis.version}</ version>
</ dependency>
< dependency>
< groupId> com.baomidou</ groupId>
< artifactId> mybatis-plus-boot-starter</ artifactId>
< version> ${mybatis.plus.version}</ version>
</ dependency>
< dependency>
< groupId> mysql</ groupId>
< artifactId> mysql-connector-java</ artifactId>
< scope> runtime</ scope>
< version> ${mysql.version}</ version>
</ dependency>
< dependency>
< groupId> org.projectlombok</ groupId>
< artifactId> lombok</ artifactId>
< optional> true</ optional>
< version> ${lombok.version}</ version>
</ dependency>
</ dependencies>
</ dependencyManagement>
由于大量的依赖都已经添加在<dependencyManagement>
中了,则straw-generator
和straw-portal
项目都不会直接拥有这些依赖,则需要在这2个子模块项目中自行添加所需的依赖!
在straw-generator
项目中(关于代码生成器的相关依赖由于过于特殊,一定只有当前项目需要使用,所以,对版本的管理方式可以不严格要求):
< dependencies>
< dependency>
< groupId> mysql</ groupId>
< artifactId> mysql-connector-java</ artifactId>
</ dependency>
< dependency>
< groupId> com.baomidou</ groupId>
< artifactId> mybatis-plus-generator</ artifactId>
< version> 3.3.2</ version>
</ dependency>
< dependency>
< groupId> com.baomidou</ groupId>
< artifactId> mybatis-plus-extension</ artifactId>
< version> 3.3.2</ version>
</ dependency>
< dependency>
< groupId> org.springframework.boot</ groupId>
< artifactId> spring-boot-starter-freemarker</ artifactId>
< version> 2.3.1.RELEASE</ version>
</ dependency>
</ dependencies>
在straw-portal
项目中:
< dependencies>
< dependency>
< groupId> com.github.pagehelper</ groupId>
< artifactId> pagehelper-spring-boot-starter</ artifactId>
</ dependency>
< dependency>
< groupId> org.thymeleaf.extras</ groupId>
< artifactId> thymeleaf-extras-springsecurity5</ artifactId>
</ dependency>
< dependency>
< groupId> org.springframework.boot</ groupId>
< artifactId> spring-boot-starter-thymeleaf</ artifactId>
</ dependency>
< dependency>
< groupId> org.springframework.boot</ groupId>
< artifactId> spring-boot-starter-validation</ artifactId>
</ dependency>
< dependency>
< groupId> org.springframework.boot</ groupId>
< artifactId> spring-boot-starter-security</ artifactId>
</ dependency>
< dependency>
< groupId> com.alibaba</ groupId>
< artifactId> druid-spring-boot-starter</ artifactId>
</ dependency>
< dependency>
< groupId> org.mybatis.spring.boot</ groupId>
< artifactId> mybatis-spring-boot-starter</ artifactId>
</ dependency>
< dependency>
< groupId> com.baomidou</ groupId>
< artifactId> mybatis-plus-boot-starter</ artifactId>
</ dependency>
< dependency>
< groupId> mysql</ groupId>
< artifactId> mysql-connector-java</ artifactId>
< scope> runtime</ scope>
</ dependency>
< dependency>
< groupId> org.projectlombok</ groupId>
< artifactId> lombok</ artifactId>
< optional> true</ optional>
</ dependency>
</ dependencies>
50. 创建静态资源子模块项目
创建新的straw-resource
子模块项目,用于管理用户上传的文件等静态资源。
创建出来后,在straw-resource
的pom.xml
中,自行将父级项目由SpringBoot
改为straw
项目,删除<dependencies>
和<build>
节点(因为没有存在的必要,在父项目中已经配置好了)。
在straw
项目中的<mudules>
中添加子模块项目。
在straw-resource
的application.properties
中显式的配置端口号,必须与straw-portal
的不同:
server.port=8081
全部完成后,更新Maven,straw-portal
和straw-resource
这2个项目是可以同时启动的!
51. 设置straw-resource子模块项目的静态目录
在straw-resource
项目的application.properties
中添加配置:
spring.resources.static-locations=file:D:/IdeaProjects/straw-static-resource
则straw-resource
项目的静态目录就是以上指定的位置,后续straw-portal
项目中涉及上传操作时,上传的文件也应该存放到以上位置。
52.设置straw-resource子模块项目的静态目录
在straw-portal
项目的application.properties
中添加配置:
# 发布问题时,将图片上传到哪里,需要与straw-resource项目的静态资源目录保持一致
project.question.image-upload-path=D:/IdeaProjects/straw-static-resource
# 发布问题时,上传的图片通过哪个服务器提供访问,配置的端口号需要与straw-resource项目保持一致
project.question.image-host=http://localhost:8081/
# 发布问题时,允许上传的文件的最大大小
project.question.image-max-size=307200
# 发布问题时,允许上传的图片文件的类型
project.question.image-content-types=image/jpeg, image/png, image/bmp
并且,在straw-portal
中调整默认限制的文件大小:
@Bean
public MultipartConfigElement multipartConfigElement ( ) {
MultipartConfigFactory factory = new MultipartConfigFactory ( ) ;
factory. setMaxFileSize ( DataSize. ofMegabytes ( 500 ) ) ;
factory. setMaxRequestSize ( DataSize. ofMegabytes ( 500 ) ) ;
return factory. createMultipartConfig ( ) ;
}
53. 开发简易上传功能
说明:由于上传功能不可以通过在URL上填写参数直接进行测试,为了更快的进行测试并体验上传的效果,暂且忽略不必要的代码,例如上传文件的相关检查等细节问题,当然,测试时也应该使用正确的文件和数据进行测试。当简单的上传已经完成后,再补全细节部分。
在QuestionController
中开发服务器端的简易上传处理:
@Value ( "${project.question.image-upload-path}" )
private String imageUploadPath;
@Value ( ( "${project.question.image-host}" ) )
private String imageHost;
@PostMapping ( "/upload-image" )
public R< String> uploadImage ( MultipartFile imageFile) {
File dest = new File ( imageUploadPath, "1.jpg" ) ;
try {
imageFile. transferTo ( dest) ;
} catch ( IOException e) {
e. printStackTrace ( ) ;
}
String imageUrl = imageHost + "1.jpg" ; // http://localhost:8081/1.jpg
log. debug ( "image url >>> {}" , imageUrl) ;
return R. ok ( imageUrl) ;
}
本次需要处理的页面是“发表问题”的question/create.html ,在发表问题时,使用的富文本编辑Summernote提供了名为callbacks
的回调机制,其中,存在名为onImageUpload
的回调属性,该属性值是函数,所以,可以自定义函数配置到这个回调属性中,则后续上传图片时,就会自动触发自定义的函数,通过自定义函数实现图片的上传,并返回上传图片的URL,生成图片插入到Summernote富文本编辑器中即可。
在question/create.html 中,先将底部关于Summernote的JavaScript代码移到新创建的commons/init_summernote.js 中,并调整这段代码:
$ ( document) . ready ( function ( ) {
$ ( '#summernote' ) . summernote ( {
height: 300 ,
tabsize: 2 ,
lang: 'zh-CN' ,
placeholder: '请输入问题的详细描述...' ,
callbacks: {
onImageUpload: function ( ) {
alert ( "准备上传图片!" ) ;
}
}
} ) ;
} ) ;
完成后,重启项目,打开“发布问题”页面,插入图片,选择图片文件就会弹出对话框!
然后,在以上回调中,使用$.ajax()
提交异步请求,在处理结果时,创建Image
对象,将结果中的图片URL作为Image
对象的src
属性值,并将整个Image
对象(就是一个<src>
标签)插入到富文本编辑器中:
$ ( document) . ready ( function ( ) {
$ ( '#summernote' ) . summernote ( {
height: 300 ,
tabsize: 2 ,
lang: 'zh-CN' ,
placeholder: '请输入问题的详细描述...' ,
callbacks: {
onImageUpload: function ( files) {
// ---------------------------------------
// 当前函数的参数名称是自定义,它表示用户选择的若干个文件
// Summernote在调用该函数时,会把用户选择的文件作为函数的参数
// ---------------------------------------
if ( ! files || files. length < 1 ) {
alert ( "请选择您要上传的文件!" ) ;
return ;
}
if ( files. length > 1 ) {
alert ( "一次只允许上传1个文件!" ) ;
return ;
}
let formData = new FormData ( ) ;
let file = files[ 0 ] ;
formData. append ( "imageFile" , file) ;
console. log ( "form data >>> " + formData) ;
$. ajax ( {
url: '/api/v1/questions/upload-image' ,
type: 'post' ,
data: formData,
contentType: false ,
processData: false ,
success: function ( json) {
if ( json. state == 2000 ) {
// alert(json.data);
let img = new Image ( ) ; // <img>
img. src = json. data; // <img src="xxx">
$ ( '#summernote' ) . summernote ( 'insertNode' , img) ;
} else {
alert ( json. message) ;
}
}
} ) ;
}
}
} ) ;
} ) ;
54. 完善服务器端的上传功能
先创建关于文件上传的异常类型:
public class FileUploadException extends RuntimeException {
}
public class FileEmptyException extends FileUploadException {
}
public class FileSizeException extends FileUploadException {
}
public class FileTypeException extends FileUploadException {
}
public class FileIOException extends FileUploadException {
}
在GlobalExceptionHandler
中处理以上异常,完整代码如下(需在R.State
中添加常量):
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler
public R handleException ( Throwable e) {
if ( e instanceof ParameterValidationException ) {
return R. failure ( R. State. ERR_PARAMETER_INVALIDATION, e) ;
} else if ( e instanceof InviteCodeException ) {
return R. failure ( R. State. ERR_INVITE_CODE, e) ;
} else if ( e instanceof ClassDisabledException ) {
return R. failure ( R. State. ERR_CLASS_DISABLED, e) ;
} else if ( e instanceof PhoneDuplicateException ) {
return R. failure ( R. State. ERR_PHONE_DUPLICATE, e) ;
} else if ( e instanceof InsertException ) {
return R. failure ( R. State. ERR_INSERT, e) ;
} else if ( e instanceof FileEmptyException ) {
return R. failure ( R. State. ERR_UPLOAD_EMPTY, e) ;
} else if ( e instanceof FileSizeException ) {
return R. failure ( R. State. ERR_UPLOAD_FILE_SIZE, e) ;
} else if ( e instanceof FileTypeException ) {
return R. failure ( R. State. ERR_UPLOAD_FILE_TYPE, e) ;
} else if ( e instanceof FileIOException ) {
return R. failure ( R. State. ERR_UPLOAD_FILE_IO, e) ;
} else if ( e instanceof AccessDeniedException ) {
return R. failure ( R. State. ERR_ACCESS_DENIED, e) ;
} else {
log. debug ( "Unknown Exception" , e) ;
return R. failure ( R. State. ERR_UNKNOWN, e) ;
}
}
}
在处理上传请求之前,先声明2个全局属性,用于读取配置中的“文件最大大小”和“文件类型”:
@Value ( "${project.question.image-max-size}" )
private long imageMaxSize;
@Value ( ( "${project.question.image-content-types}" ) )
private List< String> imageContentTypes;
在处理上传请求的过程中:
应该创建子级文件夹,避免所有的文件都传到指定的同一个文件夹中,推荐使用“年”和“月”分别创建2级子文件夹,上传的图片应该放在“月”的文件夹中; 可以使用UUID作为文件名; 不需要判断原始扩展名,而是直接从原始文件全名中截取即可; 及时打桩,输出关键信息,例如保存文件的文件夹路径、文件名、完整路径等,便于出错时排查问题。
具体代码:
@Value ( "${project.question.image-upload-path}" )
private String imageUploadPath;
@Value ( ( "${project.question.image-host}" ) )
private String imageHost;
@Value ( "${project.question.image-max-size}" )
private long imageMaxSize;
@Value ( ( "${project.question.image-content-types}" ) )
private List< String> imageContentTypes;
@PostMapping ( "/upload-image" )
public R< String> uploadImage ( MultipartFile imageFile) {
// 判断上传的文件是否为空
if ( imageFile. isEmpty ( ) ) {
throw new FileEmptyException ( "上传图片失败!请选择有效的图片文件!" ) ;
}
// 判断上传的文件大小是否超标
if ( imageFile. getSize ( ) > imageMaxSize) {
throw new FileSizeException ( "上传图片失败!不允许使用超过" + ( imageMaxSize / 1024 ) + "KB的图片文件!" ) ;
}
// 判断上传的文件类型是否超标
if ( ! imageContentTypes. contains ( imageFile. getContentType ( ) ) ) {
throw new FileTypeException ( "上传图片失败!图片类型错误!允许上传的图片类型有:" + imageContentTypes) ;
}
// 确定本次上传时使用的文件夹
String dir = DateTimeFormatter. ofPattern ( "yyyy/MM" ) . format ( LocalDateTime. now ( ) ) ;
File parent = new File ( imageUploadPath, dir) ;
if ( ! parent. exists ( ) ) {
parent. mkdirs ( ) ;
}
log. debug ( "dir >>> {}" , parent) ;
// 确定本次上传时使用的文件名
String filename = UUID. randomUUID ( ) . toString ( ) ;
String originalFilename = imageFile. getOriginalFilename ( ) ;
String suffix = originalFilename. substring ( originalFilename. lastIndexOf ( "." ) ) ;
String child = filename + suffix;
// 创建最终保存时的文件对象
File dest = new File ( parent, child) ;
// 执行保存
try {
imageFile. transferTo ( dest) ;
} catch ( IOException e) {
throw new FileIOException ( "上传图片失败!当前服务器忙,请稍后再次尝试!" ) ;
}
// 确定网络访问路径
String imageUrl = imageHost + dir + "/" + child; // http://localhost:8081/1.jpg
log. debug ( "image url >>> {}" , imageUrl) ;
// 返回
return R. ok ( imageUrl) ;
}
55. 显示老师主页
老师的主页文件是index_teacher.html
,原本在static
文件夹中,先把它拖拽到templates
文件夹中。
在SystemController
中,修改原有访问/index.html
路径的处理方法:
@GetMapping ( "/index.html" )
public String index ( @AuthenticationPrincipal UserInfo userInfo) {
if ( userInfo. getType ( ) == 0 ) {
return "index" ;
} else {
return "index_teacher" ;
}
}
需要注意:以上判断用户身份时,会判断用户数据的type
属性,此前,在UserServiceImpl.login()
方法中已经向返回的UserInfo
中设置了从数据库中读取到的type
属性,则以上代码可以正常获取type
值!
56. 老师主页显示问题列表-持久层
(a) 规范需要执行的SQL语句
老师主页显示的问题列表应该显示出老师自己发表的问题,和学生指定该老师回答的问题。
这样的列表数据可以使用此前的QuestionVO
来表示每一个问题的数据,列表则使用List<QuestionVO>
来表示。
需要执行的SQL语句大致是:
select question.*
from question
left join user_question
on question.id=user_question.question_id
where question.user_id=? or user_question.user_id=? and is_delete=0
order by status, modified_time desc;
(b) 在接口中添加抽象方法
List< QuestionVO> findTeacherQuestions ( Integer teacherId) ;
© 配置SQL映射
(d) 单元测试
57. 老师主页显示问题列表-业务层
(a)
(b) 接口与抽象方法
原本存在抽象方法:
PageInfo< QuestionVO> getQuestionsByUserId ( Integer userId, Integer page) ;
改为:
PageInfo< QuestionVO> getQuestionsByUserId ( Integer userId, Integer type, Integer page) ;
© 实现业务方法
在原本存在的getQuestionsByUserId()
方法的参数列表中添加参数,与以上抽象方法保持一致,然后,在实现过程中:
// 设置分页参数
PageHelper. startPage ( page, pageSize) ;
// 调用持久层方法查询问题列表,该列表中的数据只有标签的id,并不包括标签数据
List< QuestionVO> questions;
if ( type == 0 ) {
questions = questionMapper. findListByUserId ( userId) ;
} else {
questions = questionMapper. findTeacherQuestions ( userId) ;
}
// 后续代码不变
(d) 单元测试
由于修改了业务方法的声明,当前控制器层的调用会因为参数不匹配而报错,将无法进行单元测试,所以,先处理完控制器层再测试。
58. 老师主页显示问题列表-控制器层
在原来的获取学生问题列表的方法中,调用业务方法时多添加type
值即可,该值来自UserInfo
参数:
@GetMapping ( "/my" )
public R< PageInfo< QuestionVO> > getMyQuestions ( Integer page,
@AuthenticationPrincipal UserInfo userInfo) {
if ( page == null || page < 1 ) {
page = 1 ;
}
PageInfo< QuestionVO> questions = questionService. getQuestionsByUserId ( userInfo. getId ( ) , userInfo. getType ( ) , page) ;
return R. ok ( questions) ;
}
完成后,应该分别测试学生账号登录后显示列表和老师账号登录后显示列表。
59. 老师主页显示问题列表-前端页面
引用question/create.html
中的处理即可!也就是说:在question/create.html
中将列表区域设置为th:fragment
,然后在index_teacher.html
中通过th:replace
直接引用即可!