SpringBoot项目(四)完善通用模块
4.1 配置java编译版本
klcms-parent的pom.xml添加以下配置
<properties>
<!-- 解决报错:Lambda expressions are allowed only at source level 1.8 or above-->
<java.version>1.8</java.version>
</properties>
4.2 配置java.util.Date到json的转换
Restful接口,默认情况下 Date类型字段返回时间戳(毫秒值)结果,可通过以下两种方式配置为返回格式化的字符串。
如果同时配置两种方式:方式二优先,会覆盖方式一。
4.2.1 方式一:全局配置
在application.properties配置文件中增加下面两个配置:
#时区设置
spring.jackson.time-zone=GMT+8
#日期期时格式设置置
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
4.2.2 方式二:局部设置
使用@Jsonformat、@datetimeformat两个注解局部设置。
与全局配置相比较,使用这两个注解就更加的精细,只需要在你所需要的地方使用这两个注解即可,并且可以设置不同的时间格式。
使用@JsonFormat解决后台到前台时间格式保持一致的问题
使用@DateTimeFormat设置后台接收前台的时间格式。
使用@JsonFormat需要maven引入com.fasterxml.jackson.core下jackson-annotations
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
4.3 静态资源映射
4.3.1 映射规则
WebMvcAuotConfiguration:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Integer cachePeriod = this.resourceProperties.getCachePeriod();
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(
registry.addResourceHandler("/webjars/**")
.addResourceLocations(
"classpath:/META-INF/resources/webjars/")
.setCachePeriod(cachePeriod));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
//静态资源文件夹映射
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(
registry.addResourceHandler(staticPathPattern)
.addResourceLocations(
this.resourceProperties.getStaticLocations())
.setCachePeriod(cachePeriod));
}
}
//配置欢迎页映射
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(
ResourceProperties resourceProperties) {
return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
}
//配置喜欢的图标
@Configuration
@ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true)
public static class FaviconConfiguration {
private final ResourceProperties resourceProperties;
public FaviconConfiguration(ResourceProperties resourceProperties) {
this.resourceProperties = resourceProperties;
}
@Bean
public SimpleUrlHandlerMapping faviconHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
//所有 **/favicon.ico
mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",
faviconRequestHandler()));
return mapping;
}
@Bean
public ResourceHttpRequestHandler faviconRequestHandler() {
ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
requestHandler
.setLocations(this.resourceProperties.getFaviconLocations());
return requestHandler;
}
}
4.3.2 静态资源映射
1)webjars 将jquery、bootstrap等以maven依赖的方式引入
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.3.1</version>
</dependency>
访问测试:http://localhost:8081/webjars/jquery/3.3.1/jquery.js
2)"/**" 访问当前项目的任何资源,如果没有处理,都去静态资源的文件夹找映射:
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
"/":当前项目的根路径
示例:访问网站根目录,默认会去找控制器中path为index的方法,若没有则去静态资源目录找index.html、index.htm进行响应。
在以上各路径都没有index.html、index.htm,并且控制器中没有path为index的方法时,访问站点,报404错误。如下图:
在static下添加index.html,再访问网站跟路径,则可显示该网页内容,如下图:
4.4 扩展Mvc
4.4.1 xml配置文件方式
与在resources下springmvc.xml的如下配置等效
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:view-controller path="/login" view-name="login"/>
<mvc:interceptors>
<!--拦截器-->
<mvc:interceptor>
<mvc:mapping path="/hello"/>
<bean></bean>
</mvc:interceptor>
</mvc:interceptors>
</beans>
4.4.2 注解方式
com.klfront.klcms.config下添加一个配置类,实现其方法,即保留默认的自动配置,又能进行扩展。注:不能标注@EnableWebMvc,否则自动配置失效。
例如实现addViewControllers方法自定义请求的视图映射:
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//浏览器发送 /login 请求来到 login.ftl
registry.addViewController("/login").setViewName("login");
}
}
配置类如果直接实现WebMvcConfigurer接口,即便至扩展一个功能,代码上也需要实现多个方法,比较繁琐。
抽象类WebMvcConfigurerAdapter 实现了 WebMvcConfigurer接口。子类继承WebMvcConfigurerAdapter,可根据业务需要对只对关注的方法进行重写。
4.4.3 原理分析
1) 为什么既保留了自动配置,同时又可以让扩展的配置生效?
因为还有一个抽象类也继承了WebMvcConfigurationAdapter。
WebMvcAutoConfigurationAdapter extends WebMvcConfigurationAdapter
WebMvcAutoConfigurationAdapter 的一个子类WebMvcAutoConfiguration,实现SpringMVC的自动配置功能。
WebMvcAutoConfiguration和自定义的MvcConfig都继承自WebMvcConfigurationAdapter,它们的存在实现了自动配置和扩展的配置共同生效。
1)WebMvcAutoConfiguration是SpringMVC的自动配置类,继承自WebMvcAutoConfigurationAdapter 。
2)WebMvcAutoConfiguration在做自动配置时会导入;@Import(EnableWebMvcConfiguration.class)
@Configuration
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
......
}
DelegatingWebMvcConfiguration 内部:
//从容器中获取所有的WebMvcConfigurer
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
2)为什么@EnableWebMvc,自动配置就失效了?
(1)@EnableWebMvc的核心
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
(2)DelegatingWebMvcConfiguration
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
(3)WebMvcConfigurationSupport
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class,
WebMvcConfigurerAdapter.class })
//容器中没有WebMvcConfigurationSupport组件的时候,自动配置类才生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
(4)@EnableWebMvc将WebMvcConfigurationSupport组件导入进来,导致了WebMvcAutoConfiguration 失效;
(5)WebMvcConfigurationSupport只是SpringMVC最基本的功能;
4.5 拦截器实现登录检查
4.5.1 编写拦截器
/**
* 登陆检查
*/
public class LoginHandlerInterceptor implements HandlerInterceptor {
//目标方法执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object user = request.getSession().getAttribute("loginUser");
if(user == null){
//未登陆,返回登陆页面
request.setAttribute("msg","没有权限请先登陆");
request.getRequestDispatcher("/login").forward(request,response);
return false;
}else{
//已登陆,放行请求
return true;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
4.5.2 注册拦截器
//使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能
//@EnableWebMvc 不要接管SpringMVC
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//浏览器发送 /login 请求来到 login.ftl
registry.addViewController("/login").setViewName("login");
}
//所有的WebMvcConfigurerAdapter组件都会一起起作用
@Bean //将组件注册在容器
public WebMvcConfigurerAdapter webMvcConfigurerAdapter(){
WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() {
//注册登录拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截所有页面检查是否登录
//排除对主页、文章详情页和登录相关页面的拦截
registry.addInterceptor(new LoginHandlerInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/","/index.html","/item/*","/login","/user/login");
}
};
return adapter;
}
}
4.5.3 登录逻辑
@PostMapping(value = "/user/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
Map<String, Object> map, HttpSession session) {
if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
// 登陆成功,防止表单重复提交,可以重定向到主页
session.setAttribute("loginUser", username);
return "redirect:/index.html";
} else {
// 登陆失败
map.put("msg", "用户名或密码错误");
return "login";
}
}
4.5.4 页面显示提示信息
Freemarker使用Request["属性名"]来获取request的属性进行显示,相当于request.getAtrribute("属性名");
<p style="color: red" >${Request["msg"]}</p>
<label class="sr-only" >用户名</label>
<input type="text" name="username" class="form-control" placeholder="用户名" required autofocus="">
<label class="sr-only" >密码</label>
<input type="password" name="password" class="form-control" placeholder="密码" required>
4.5.5 根据登录状态显示登录或退出
<#if Session.loginUser?exists> <a href="/user/logout">退出</a> <#else>
<a href="/login">登录</a></#if>
4.6 集成CKEditor编辑器
4.6.1项目中static/plugins目录下添加ckeditor资源
4.6.2 修改CKEditor的配置文件config.js
CKEDITOR.editorConfig = function( config )
{
// 服务端的文件上传接口,后续再CKEditor上集成上传功能需要用到
config.filebrowserUploadUrl = "/ckEditorUpload";
// UI颜色
config.uiColor = '#d1ecf1';
// config.width = 500; //编辑器宽度
config.height = 280; //编辑器高度
config.entities = false;
};
4.6.3 页面引用和初始化
JS代码
<script src="/webjars/jquery/3.3.1/jquery.js"></script>
<script src="/plugins/ckeditor/ckeditor.js"></script>
$(function() {
//让编辑器生效,作用在id为txtMsg的textarea元素上。
var editor = CKEDITOR.replace('txtMsg');
//以下代码解决XML内容在编辑器中不显示的问题
var id = $("#articleId").val();
setEditData(id);
});
HTML页面元素
<textarea id="txtMsg" name="content" class="control-textarea" >
<!-- 如果内容中包含标签 & lt;dependencies& gt;会被自动反转义为<dependencies>,导致编辑器中不显示 -->
${article.content!}
</textarea>
4.6.4 解决XML内容在编辑器中不显示问题
/**
* 通过后台传递给模板的数据赋值,转义过的标签& lt;dependencies& gt;会被自动反转义为<dependencies>,导致再次保存后,页面无法正常显示。
* 因此改为通过ajax请求接口,得到的文章内容(不会被反转义)通过js赋值给CKEditor编辑器。
*/
function setEditData (id){
$.get("/getContent/"+id, function(content) {
if (content != null) {
CKEDITOR.instances.txtMsg.setData(content);
} else {
alert("获取数据失败!异常信息:");
}
});
}
4.7 文件上传
4.7.1 保存上传文件的方法
class FileUtils中的saveFile方法:
/** * 保存上传的文件 * * @author Lu Jinlong * * @param file 上传的文件对象 * @param folderPath 文件要保存的目录 * @return 返回保存的路径 * @throws Exception */ public static String saveFile(MultipartFile file, String folderPath) throws Exception { OutputStream os = null; InputStream inputStream = null; String filePath = null; try { inputStream = file.getInputStream(); // 1K的字节池 byte[] bs = new byte[1024]; int len; File folder = new File(folderPath); if (!folder.exists()) folder.mkdirs(); String fileType = getFileExt(file); if(fileType.isEmpty()) { fileType = ".tmp"; } filePath = folder.getPath() + File.separator +UUID.randomUUID().toString().replaceAll("-", "") + "." + fileType; os = new FileOutputStream(filePath); // 读取数据写入文件 while ((len = inputStream.read(bs)) != -1) { os.write(bs, 0, len); } } finally { if (os != null) os.close(); if (inputStream != null) inputStream.close(); } return filePath; }
4.7.2 控制器文件上传方法
@Value("${upload.root-path}") private String rootFolderPath; @RequestMapping(value = "/ckEditorUpload", method = RequestMethod.POST) public void uploadImage(HttpServletRequest request, HttpServletResponse response, @RequestParam("CKEditorFuncNum") String CKEditorFuncNum ) throws IOException { try { MultipartFile file=null; MultipartHttpServletRequest mRequest = (MultipartHttpServletRequest) request; MapfileMap = mRequest.getFileMap(); for(Map.Entry script>window.parent.CKEDITOR.tools.callFunction(" + CKEditorFuncNum + ", \"" + relativePath + "\",\"\");en :fileMap.entrySet()) { file=en.getValue(); break; } if(file==null) { response.getWriter().write("error:no file uploaded"); return; } String ext = FileUtil.getFileExt(file); if (!ext.equals(".jpg") && !ext.equals(".png")){ response.getWriter().write("error:filetypeerror"); return; } Calendar cal = Calendar.getInstance(); String folderPath = rootFolderPath +"/upload/images"+ File.separator + cal.get(Calendar.YEAR) + File.separator+ cal.get(Calendar.MONTH + 1) + File.separator + cal.get(Calendar.DATE); String saveFilePath = FileUtil.saveFile(file, folderPath); String relativePath =saveFilePath.replace(rootFolderPath, ""); response.getWriter().write("< </ script>");
} catch (Exception e) { response.getWriter().write("error:" + e.getMessage()); } }
4.7.3 配置CKEditor的文件上传路径
修改CKEditor的配置文件config.js
CKEDITOR.editorConfig = function( config )
{
// 服务端的文件上传接口
config.filebrowserUploadUrl = "/ckEditorUpload";
// 其他配置......
};
4.8 AOP拦截记录日志
klcms-provider工程中配置环境和添加切面类。
4.8.1添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
4.8.2application.properties中加入配置
spring.aop.auto = true
4.8.3添加切面类,配置切点
@Slf4j
@Aspect
@Component
public class LoggerAop {
/**
* 定义切点
*/
@Pointcut("execution(* com.klfront.klcms.service.impl.*.*(..))")
public void serviceMethod() {
}
/**
* 切面的前置方法 即方法执行前拦截到的方法 记录并输出
* 在目标方法执行之前的通知
* @param joinPoint
*/
@Before("execution(* com.klfront.klcms.service.impl.*.*(..))")
public void before(JoinPoint joinPoint) {
Object method = joinPoint.getSignature();
String methodName = joinPoint.getSignature().getName();
List<Object> list = Arrays.asList(joinPoint.getArgs());
log.info("开始执行"+joinPoint.getTarget()+"的方法:"+ methodName);
log.info("参数:"+list);
}
/**
* 切面的后置方法,不管抛不抛异常都会走此方法
* 在目标方法执行之后的通知
* @param joinPoint
*/
@After("serviceMethod()")
public void afterMethod(JoinPoint joinPoint){
Object method = joinPoint.getSignature();
log.info("执行"+method+"方法结束");
}
/**
* 在方法正常执行通过之后执行的通知叫做返回通知
* 可以返回到方法的返回值 在注解后加入returning
* @param joinPoint 连接点
* @param result 返回结果
*/
@AfterReturning(pointcut = "serviceMethod()",returning="result")
public void afterReturn(JoinPoint joinPoint,Object result ){
Object method = joinPoint.getSignature();
log.info("执行"+method+"方法正常执行结束,返回结果:"+result);
}
@AfterThrowing(throwing = "ex", pointcut = "serviceMethod()")
public void afterThrowing(JoinPoint joinPoint,Throwable ex) {
Object method = joinPoint.getSignature();
log.info("执行"+method+"方法出现异常:" + ex.getMessage());
}
}