SpringBoot项目(四)完善通用模块

作者:陆金龙    发表时间:2019-07-19 18:22   

关键词:java编译版本   java.util.Date json的转换  静态资源 webjars  @EnableWebMvc  WebMvcConfigurer  登录拦截  集成CKEditor  文件上传  AOP日志  

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;
		Map fileMap = mRequest.getFileMap();
		for(Map.Entry 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>window.parent.CKEDITOR.tools.callFunction(" + CKEditorFuncNum + ", \"" + relativePath + "\",\"\");</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());

    }

}