顯示具有 SpringBoot 標籤的文章。 顯示所有文章
顯示具有 SpringBoot 標籤的文章。 顯示所有文章

2023年12月10日 星期日

JSR-303 Bean Validation

 在以往Bean Validation通常會在service的function寫上好長一段判斷如下


//合法性校驗
  if (StringUtils.isBlank(dto.getName())) {
   throw new XueChengPlusException("課程名稱為空");
  }

  if (StringUtils.isBlank(dto.getMt())) {
   throw new XueChengPlusException("課程分類為空");
  }

  if (StringUtils.isBlank(dto.getSt())) {
   throw new XueChengPlusException("課程分類為空");
  }

  if (StringUtils.isBlank(dto.getGrade())) {
   throw new XueChengPlusException("課程等級為空");
  }

然而當欄位多起來的時候,code看起來就會變得冗長

且如果不同的function都要來一段這個,這些也有許多重複的程式碼其實也有改進空間

而在JavaEE6規範中就定義了參數校驗的規範

它就是JSR-303,它定義了Bean Validation,即對bean屬性進行校驗

SpringBoot提供了JSR-303的支持,它就是spring-boot-starter-validation,它的底層使用Hibernate Validator,Hibernate Validator是Bean Validation 的參考實現。

所以,我們準備在Controller層使用spring-boot-starter-validation完成對請求參數的基本合法性進行校驗。

以下的組合是可以讓不同的controller使用不同組的校驗文字
我們在專案中的exception包定出GlobalExceptionHandler和ValidationGroups



GlobalExceptionHandler

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
    @ResponseBody
    @ExceptionHandler(XueChengPlusException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public RestErrorResponse customException(XueChengPlusException e) {
        log.error("【系統異常】{}",e.getErrMessage(),e);
        return new RestErrorResponse(e.getErrMessage());

    }

    @ResponseBody
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public RestErrorResponse exception(Exception e) {

        log.error("【系統異常】{}",e.getMessage(),e);

        return new RestErrorResponse(CommonError.UNKOWN_ERROR.getErrMessage());

    }

    @ResponseBody
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public RestErrorResponse methodArgumentNotValidException(MethodArgumentNotValidException e) {

        BindingResult bindingResult = e.getBindingResult();
        List msgList = new ArrayList<>();
        bindingResult.getFieldErrors().stream().forEach(item->{
            msgList.add(item.getDefaultMessage());
        });
        String msg = StringUtils.join(msgList, ",");
        log.error("【系統異常】{}",msg);
        return new RestErrorResponse(msg);

    }
}


ValidationGroups

public class ValidationGroups {
    public interface Insert{};
    public interface Update{};
    public interface Delete{};
}

目前將ValidationGroups分成新增, 更新, 刪除三個類別

在bean上設置的檢核訊息如下


@Data
@ApiModel(value="AddCourseDto", description="新增課程基本信息")
public class AddCourseDto {

 @NotEmpty(groups = {ValidationGroups.Insert.class},message = "新增課程名稱不能為空")
 @NotEmpty(groups = {ValidationGroups.Update.class},message = "修改課程名稱不能為空")
 @ApiModelProperty(value = "課程名稱", required = true)
 private String name;
 ...
 }

然後使用上, 我們在controller就可以這樣寫, 比方說我們此類為新增 就使用Insert @Validated({ValidationGroups.Insert.class})


    @ApiOperation("新增課程基礎信息")
    @PostMapping("/course")
    public CourseBaseInfoDto createCourseBase(@RequestBody @Validated({ValidationGroups.Insert.class}) AddCourseDto addCourseDto){
        //獲取到用戶所屬機構id
        Long companyId = 1232141425L;
        return courseBaseInfoService.createCourseBase(companyId,addCourseDto);
    }

2023年10月17日 星期二

全局date time

 當回應的日期格式中有T還有毫秒的時候














或許我們會思考如何改它的格式

有一個方法, 可以做全局的統一



@Configuration
public class LocalDateTimeConfig {

    /*
     * 序列化内容
     *   LocalDateTime -> String
     * 服务端返回给客户端内容
     * */
    @Bean
    public LocalDateTimeSerializer localDateTimeSerializer() {
        return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }

    /*
     * 反序列化内容
     *   String -> LocalDateTime
     * 客户端传入服务端数据
     * */
    @Bean
    public LocalDateTimeDeserializer localDateTimeDeserializer() {
        return new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }


    // 配置
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
        return builder -> {
            builder.serializerByType(LocalDateTime.class, localDateTimeSerializer());
            builder.deserializerByType(LocalDateTime.class, localDateTimeDeserializer());
        };
    }

}

透過上面的方式, 就能統一格式

結果:

2022年3月31日 星期四

React打包排除

當React資料夾, 放在SpringBoot專案下面的時候

他在run Maven install的時候

整包會被複製到Target中

這樣整個操作過程會相當緩慢

如果要避免這個複製的動作可以在pom檔中build加入以下






<resources>

<resource>

<directory>src/main/resources</directory>

<excludes>

<exclude>react/**</exclude>

</excludes>

</resource>

</resources>

2022年3月9日 星期三

資料表存JSON格式文字

把JSON存進資料庫的欄位, 要做以下的設置



@Entity(name = "patient_xxx")
@TypeDef(name = "json", typeClass = JsonType.class)
public class PatientXXX {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    // 傳送Json內容
    @Type(type = "json")
    @Column(name = "content", columnDefinition = "json")
    private String content;
...
}

另外, 接值進來的時候, 要用ObjectMapper轉成字串




    @PostMapping("/lab/PutStatus")
	 public JwtResponseResult putStatus(@RequestBody PutStatusRequestVo putStatusRequestVo) {
    	
    	try {
    		ObjectMapper mapper = new ObjectMapper();
       	  String jsonString = mapper.writeValueAsString(putStatusRequestVo);
       
        	labService.updatePatientExamineOrder(putStatusRequestVo.getExamList());
        	labService.recordCallApiLog(
        		putStatusRequestVo.getLab_id(), 
        		jsonString, 
        		PatientExamineLogEnum.Receive.getCode(),
        		"OK");
        
		} catch (Exception ex) {
			ex.printStackTrace();
			return new JwtResponseResult("fail", messageHelper, "common.error", ex.getMessage());
		}
		return new JwtResponseResult("ok", null);
	 }

***如果直接VO.toString()的話, 是沒辦法列印成做成JSON結構的

2022年3月6日 星期日

使用RestTemplate請求Token

參考了幾篇文章, 直接拿來用, 然後就.....掛掉

真是不靠譜, 還是自己寫一篇好

根據下列這篇

https://www.baeldung.com/spring-resttemplate-post-json

基本上傳過去的內容是可以用Json物件

不過~他其實也可以傳自定義物件

所以我們就拿自定義物件來做示範


這是要傳過去的物件


public class JwtUserExternal {
	
	private String account;
	private String pwd;
	...
}

在開始的地方先注入


	@Autowired
	RestTemplate restTemplate;

要在Config也加入Bean就是, 不然會報java.lang.IllegalStateException: Failed to load ApplicationContext錯誤


	@Bean
	public RestTemplate getRestTemplate() {
		return new RestTemplate();
	}

因為URI是從資料庫取出來, 我們化名為Target

然後從Target對象中取出帳號密碼, 生出JwtUserExternal物件

然後就~call sendRequestForToken function, 把Url和物件傳過去, 取得Token


	public String getToken(String LabId) {
		Target target = targetRepo.findById(Id);
		if (target == null) {
			throw new ICUmodelJwtException(HttpStatus.NOT_FOUND, "patientExamin not found");
		}
		JwtUserExternal jwtUser = new JwtUserExternal(target.getTokenId(), target.getTokenPassword());
		String token = sendRequestForToken(patientExamine.getTokenUrl(), jwtUser);
		return token;
	}

打對方API細節

這裡先生出HttpHeader, 再用它和傳送物件生出HttpEntity為request

然後使用restTemplate.postForObject(url, request, String.class)

將回傳值用使用ObjectMapper轉成JsonNode

目前回傳格式是這樣

{
    "CODE": "OK",
    "TOKEN": "eyJhbGciOxxxxxxxxx"
}

因此就root.path("TOKEN").asText()就能拿出Token


	public String sendRequestForToken(String url, JwtUserExternal requestObject) {
		try {
			HttpHeaders headers = new HttpHeaders();
			headers.setContentType(MediaType.APPLICATION_JSON);
			HttpEntity<jwtuserexternal> request = new HttpEntity<jwtuserexternal>(requestObject, headers);
			String personResultAsJsonStr = restTemplate.postForObject(url, request, String.class);
			ObjectMapper objectMapper = new ObjectMapper();
			JsonNode root = objectMapper.readTree(personResultAsJsonStr);
			return root.path("TOKEN").asText();
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}
	

完整的類別如下



@Service
public class JwtLogin {

	@Autowired
	RestTemplate restTemplate;

	@Autowired
	TargetRepo targetRepo;

	private String targetId;

	public String getToken() {
		String token = getToken(this.getLabId());
		return token;
	}

	public String getToken(String id) {
		Target target = targetRepo.findByTargetId(id);
		if (target == null) {
			throw new ICUmodelJwtException(HttpStatus.NOT_FOUND, "target not found");
		}
		JwtUserExternal jwtUser = new JwtUserExternal(target.getTokenId(), target.getTokenPassword());
		String token = sendRequestForToken(target.getTokenUrl(), jwtUser);
		return token;
	}

	public String sendRequestForToken(String url, JwtUserExternal requestObject) {
		try {
			HttpHeaders headers = new HttpHeaders();
			headers.setContentType(MediaType.APPLICATION_JSON);
			HttpEntity<JwtUserExternal> request = new HttpEntity<JwtUserExternal>(requestObject, headers);
			String personResultAsJsonStr = restTemplate.postForObject(url, request, String.class);
			ObjectMapper objectMapper = new ObjectMapper();
			JsonNode root = objectMapper.readTree(personResultAsJsonStr);
			return root.path("TOKEN").asText();
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	public String getTargetId() {
		return targetId;
	}

	public void setTargetId(String targetId) {
		this.targetId = targetId;
	}

}

重構以上程式碼

差異部分,直接傳Json物件過去

重構的點是因為, 有可能來源來自於不同Table

我們對前面邏輯, 開放新增方法, 可以自行新增將JwtUserExternal set進來

也就是如果他從其他來源來, 可以新增一個類似setTargetId, 只要將JwtUserExternal設到成員變數就可

對後面邏輯封閉, 傳參JwtUserExternal類讓他可以打API, 來取回Token

新增方法上符合開閉原則

JwtUserExternal類別


public class JwtUserExternal {
	
	private String account;
	private String pwd;
	private String url;
	...
		
}

主程式


@Service
public class JwtLogin {

	@Autowired
	RestTemplate restTemplate;

	@Autowired
	PatientExamineRepo patientExamineRepo;

	private JwtUserExternal jwtUserExternal;
	

	public String getToken() {
		String token = sendRequestForToken(this.getJwtUserExternal());
		return token;
	}
	
	public void setTargetId(String targetId) {
		Target target = targetRepo.findByTargetId(targetId);
		if (target == null) {
			throw new ICUmodelJwtException(HttpStatus.NOT_FOUND, "target not found");
		}
		this.setJwtUserExternal(new JwtUserExternal(
						target.getTokenId(),
						target.getTokenPassword(),
						target.getTokenUrl())
				);
	}

	public String sendRequestForToken(JwtUserExternal jwtUser) {
		try {
			HttpHeaders headers = new HttpHeaders();
			headers.setContentType(MediaType.APPLICATION_JSON);
			JSONObject requestObject = new JSONObject();
			requestObject.put("account", jwtUser.getAccount());
			requestObject.put("pwd", jwtUser.getPwd());
			HttpEntity<String> request = new HttpEntity<String> (requestObject.toString(), headers);
			String personResultAsJsonStr = restTemplate.postForObject(jwtUser.getUrl(), request, String.class);
			ObjectMapper objectMapper = new ObjectMapper();
			JsonNode root = objectMapper.readTree(personResultAsJsonStr);
			return root.path("TOKEN").asText();
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	public JwtUserExternal getJwtUserExternal() {
		return jwtUserExternal;
	}

	public void setJwtUserExternal(JwtUserExternal jwtUserExternal) {
		this.jwtUserExternal = jwtUserExternal;
	}	

}

2022年3月4日 星期五

spring boot 後端 i18n

Spring boot 其實有自動配置好

因此只需要注入MessageSource就可以使用了



而~i18n檔案的目錄

可以從application.properties檔案中做控制

例如:

spring.messages.basename=static/i18n/report

這個是從resource下面開始的路徑








後面~我們檔案名稱就為report

多國檔就會為report_xxx


@Autowired
    private MessageSource messageSource;
    
    @Test 
    void test() {
    	String s1 = messageSource.getMessage(
            "demo.message", 
            null, 
            Locale.ENGLISH);
    	System.out.println(s1);

       	String s2 = messageSource.getMessage(
                "demo.message", 
                null, 
                Locale.TAIWAN);
        System.out.println(s2);
  
    }

在多國文字檔案的部分如下










run的結果如下





另外~我們可以發現, 他原生的用法, 參數多又麻煩
所以, 我另外寫了一個helper把它簡化
只要一開始設定完語言, 使用的時候就只要getMessage一個參數就搞定
寫好的內容如下, 裡面還有一個邏輯是圖片的邏輯
比方說中文版, 我們就原圖, 英文版我們使用英文版的圖, 圖檔名結尾為_en
這樣, 方便我們getImage取得相對應的路徑
程式碼如下




@Service
public class ReportHelper {
	
	   @Autowired
	   private MessageSource messageSource;
	   
	   private Locale locale;
	   
	   public String getMessage(String name) {   	
	    	return messageSource.getMessage(name, null, locale);
	    }

		public Locale getLocale() {
			return locale;
		}

		public void setLocale(Locale locale) {
			this.locale = locale;
		}   
		
		public String getImagePath(String path) {
			String suffix="";
			if(locale.equals(Locale.TAIWAN)) {
	        	suffix="";
	        } else {
	        	suffix="_en";
	        }
			return path + suffix;
		}

}

使用如下


    @Autowired 
    private ReportHelper reportMessage;
    
    @Test
    void utilTest() {
    	reportMessage.setLocale(Locale.TAIWAN);
    	String s2 = reportMessage.getMessage("demo.message");
    	System.out.println(s2);
    }

最後~如果有超過一種以上的多國檔要注入只要在application.properties檔案用逗號隔開就好

ex


# i18n
spring.messages.basename=static/i18n/report, static/i18n/message

2021年5月18日 星期二

[Spring Boot]Thymleaf使用fragment及遇到中文亂碼問題

 如果每個畫面都有固定片段的話

可以將該片段提取出來當作一個fragment

fragment設以下標籤

 <head th:fragment="head(title)">

...裡面為重複的程式片段

 </head>

然後在引用的部分, 用replace來帶掉

  <meta charset="UTF-8"/>

  <head th:replace="_fragments :: head(~{::title})">

    <title>AIoT智能腎臟病照護加值服務平台login</title>

  </head>

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

如果引用之後, 本來的頁面的中文變成亂碼呈現

可以在內容加入<meta charset="UTF-8"/>

如下圖



[Spring Boot] Thymeleaf 引用靜態資源

 拿到新的前端包,

要進到spring boot使用Thymleaf引入, 遇到了問題

還有爆

Refused to apply style from 'OOXXX' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.

查了一下得知, 原來是資源沒有引入的問題,

後來查了一下才知道

放在static下面的靜態資源要這樣引入





引入程式碼如下



<script th:src="@{plugin/jquery.i18n-1.0.7/jquery.i18n.emitter.min.js}"></script>

 <script th:src="@{plugin/jquery.i18n-1.0.7/jquery.i18n.language.min.js}"></script>

<link rel="stylesheet" th:href="@{plugin/flatpickr-4.6.6/flatpickr.min.css}">

<link rel="stylesheet" th:href="@{plugin/flatpickr-4.6.6/themes/wistron.css}">


另外解決一直跳轉到login問題

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

 

@Configuration

@EnableWebSecurity

public class SecurityConfigSEVEN extends WebSecurityConfigurerAdapter {

 

    @Override

    protected void configure(HttpSecurity http) throws Exception {

        //super.configure(http);

        //配置不需要登陆验证

        http.authorizeRequests().anyRequest().permitAll().and().logout().permitAll();

    }

 

}

參考網址:

https://blog.csdn.net/SEVENY_/article/details/104660418


參考網址: 

https://blog.csdn.net/chenbetter1996/article/details/84994801

https://stackoverflow.com/questions/48248832/stylesheet-not-loaded-because-of-mime-type

2021年4月28日 星期三

The server time zone value '¥x¥_¼Ð·Ç®É¶¡' is unrecognized

這是使用Spring boot 建立資料庫連線遇到的問題

解決方法

在後方加入?serverTimezone=UTC

如果要解決中文亂碼問題

也可以加入?useUnicode=true&characterEncoding=UTF-8

合起來可以 ?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC



2021年4月8日 星期四

[SpringBoot] properties檔案出現亂碼, Unicode的解決方式

我自己遇到問題的情況

是在接手的專案中

多國語言中有message.properties檔案的問題

內容的字發生問題如下

\u4e2d\u6587\u5b57

解決方式如下

必須去idea中的 file>Settings







記得要將Default encoding改為UTF-8
後面的Transparent native-to-ascii conversion也要勾選起來就能解決

另外idea使用上, ctrl + shift + F




2021年2月11日 星期四

[SpringBoot 1.5] 引入靜態頁面相關問題及使用fragment

引入靜態頁面的時候

我們原本可能會使用

<link rel="stylesheet" href="../../static/css/me.css">

這個語法在模板templates(有引入thymeleaf)下, 並不能讀取到









因此~需要改為

<link rel="stylesheet" href="../../static/css/me.css" th:href="@{/css/me.css}">

用th:href="@{}"來引入資源

然後重新build再刷新頁面就沒問題了






---------------------------------------------------------------------------------------

使用fragment

1 首先產生一個html檔案命名為_fragments

2 然後我們將大部分頁面上會重複的程式碼複製到這頁面

ex

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>博客詳情</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<link rel="stylesheet" href="../static/css/typo.css">
<link rel="stylesheet" href="../static/css/animate.css">
<link rel="stylesheet" href="../static/lib/prism/prism.css">
<link rel="stylesheet" href="../static/lib/tocbot/tocbot.css">
<link rel="stylesheet" href="../static/css/me.css">
</head>


3 將引入方式修改為th:href="@{}"

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<link rel="stylesheet" href="../static/css/typo.css" th:href="@{css/typo.css}">
<link rel="stylesheet" href="../static/css/animate.css" th:href="@{css/animate.css}">
<link rel="stylesheet" href="../static/lib/prism/prism.css" th:href="@{/lib/prism/prism.css}">
<link rel="stylesheet" href="../static/lib/tocbot/tocbot.css" th:href="@{/lib/tocbot/tocbot.css}">
<link rel="stylesheet" href="../static/css/me.css" th:href="@{/css/me.css}">


4 將重複的片段掛上fragment

<head th:fragment="head(title)">


5 如果遇到每個頁面有共通元素, 但是內容不一樣,

例如 <title>博客詳情</title>

這時候我們就要使用傳參數的方式(注意取參數要用$)

<head th:fragment="head(title)">
<title th:replace="${title}">博客詳情</title>
</head>

這樣來做搭配


6 而在要引入的頁面(比方說index.html)

我們可以這樣做, 這樣就是將title裡面的參數傳遞過去

<head th:replace="_fragments :: head(~{::title})">


完整的程式碼:

_fragments.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head th:fragment="head(title)">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title th:replace="${title}">博客詳情</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<link rel="stylesheet" href="../static/css/typo.css" th:href="@{css/typo.css}">
<link rel="stylesheet" href="../static/css/animate.css" th:href="@{css/animate.css}">
<link rel="stylesheet" href="../static/lib/prism/prism.css" th:href="@{/lib/prism/prism.css}">
<link rel="stylesheet" href="../static/lib/tocbot/tocbot.css" th:href="@{/lib/tocbot/tocbot.css}">
<link rel="stylesheet" href="../static/css/me.css" th:href="@{/css/me.css}">
</head>
<body>

</body>
</html>


index.html 的header

<head th:replace="_fragments :: head(~{::title})">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>博客</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<link rel="stylesheet" href="../static/css/me.css" >
</head>


2021年2月2日 星期二

[SpringBoot 1.5] AOP概念及日誌範例

關於橫切面AOP的思維

個人的領會是, 在程式進行的流程為一個直向的流層, 但是, 如果要在其中幾個物件被使用的時候,都插入一段程式碼, 這種的思想就是橫切面設計, 

具體的範例, 是一種鑲嵌, 縫合程式碼的思維, 比方說在每一次從某些controller的時候都警示該功能已經被呼叫, 就是都額外run一段程式碼, 如果說要在每個controller上面加上這段, 就是一個麻煩的事, 如果我們在後面才設計出要被加入的這段程式碼, 那還不如能用縫合的, 將其呼叫的時機設定好, 然後不用每個controller都重新鑲入該程式碼, 那這下面會有些關鍵字的思維可以釐清


1 切面Aspect

切面是一個模塊關注點的模塊化, 他由切入點PointCut和通知Advice組成

@Aspect
@Component
public class LogAspect {
}


2 目標Target

將要被縫合的類別, 比方說, 我們就設計了一個controller, 等下測試縫合程式碼的執行順序

@Controller
public class IndexController {

@GetMapping("/{id}/{name}")
public String index(@PathVariable Integer id, @PathVariable String name){
System.out.println("~~~~~~~~~~~~index~~~~~~~~~~");
return "index";
}
}


3 連接點JoinPoint

被視為切入連接點, 是下列範例中的log()方法


@Before("log()")
public void doBefore(JoinPoint joinPoint){
 logger.info("~~~~~~~Before~~~~~~~");
}


4 切入點PointCut

而切入點則是下面括號中指定execution中的東西,* 是指說返回任何東西

 com.lrm.web.*.*(..)是AOP要切入的對象, web後面*表示任何類,第二個*表示任何方法

而(..)表示任何參數, 當然也可以使用web.IndexController 這樣就只有這個Controller會被

橫切插入程式

@Pointcut("execution(* com.lrm.web.*.*(..))")
public void log(){
}


5 通知Advice

有不同被通知的種類, 比方說Before, After, 

@Before("log()")
public void doBefore(JoinPoint joinPoint){
 logger.info("~~~~~~~Before~~~~~~~");
}

@After("log()")
public void doAfter(){
logger.info("~~~~~~~After~~~~~~~");

}


6 織入Weaving

將切面Aspect與對象Target連接起來, 織入可以在類別加載的的時候完成, 也可以在編譯, 或是運行的時候完成...如果是編譯時完成就是靜態代理, 如果在運行時完成就是動態代理

--------------------------------------------------------------------------------------

以下是一個完整的程式碼

會設計一個內部類別, 然後將其Url, ip, 目標的物件名稱及參數都印出來

package com.lrm.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
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 javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

@Aspect
@Component
public class LogAspect {

private final Logger logger= LoggerFactory.getLogger(this.getClass());
@Pointcut("execution(* com.lrm.web.*.*(..))")
public void log(){

}

@Before("log()")
public void doBefore(JoinPoint joinPoint){
logger.info("~~~~~~~before~~~~~~~");
ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request=attributes.getRequest();
String url =request.getRequestURL().toString();
String ip=request.getRemoteAddr();
String classMethod=joinPoint.getSignature().getDeclaringTypeName() + "." +joinPoint.getSignature().getName();
Object[] args= joinPoint.getArgs();
RequestLog requestLog= new RequestLog(url,ip,classMethod,args);
logger.info("Request : {}",requestLog);
}

@After("log()")
public void doAfter(){
logger.info("~~~~~~~After~~~~~~~");

}

@AfterReturning(returning = "result" , pointcut = "log()")
public void doAfterReturn(Object result){
logger.info("Result: {}",result);
}

private class RequestLog{
private String url;
private String ip;
private String classMethod;
private Object[] args;

public RequestLog(String url, String ip, String classMethod, Object[] args) {
this.url = url;
this.ip = ip;
this.classMethod = classMethod;
this.args = args;
}

@Override
public String toString() {
return "RequestLog{" +
"url='" + url + '\'' +
", ip='" + ip + '\'' +
", classMethod='" + classMethod + '\'' +
", args=" + Arrays.toString(args) +
'}';
}
}
}

再搭配上前面的IndexController, 開啟然後輸入 http://localhost:8081/1/hat

後面兩個參數是先隨意給的, 以下是結果的截圖




參考網址:

https://zhuanlan.zhihu.com/p/60842627

https://matthung0807.blogspot.com/2018/02/spring-aop.html

2021年1月31日 星期日

[SpringBoot 1.5] 錯誤處理 @ControllerAdvice, @ExceptionHandler, @ReponseStatus

 SpringBoot有預設的錯誤頁面處理

要實現這個我們首先可以在resource>templates>error的資料夾下

加入兩個頁面500.html , 404.html

並在resource>templates下面放index.html

然後造一個controller

@Controller
public class IndexController {

@GetMapping("/")
public String index(){
int i = 9/0;
return "index";
}
}

由於9/0會報錯

畫面就會自動導到命名為500的頁面

將報錯的那一行註解掉

我們在Build中點Recompile

然後刷新頁面

畫面就會來到首頁了

而當我們的URL打一個沒有資源的

ex http://127.0.0.1:8081/123456

他就會導到404的頁面

------------------------------------------------

而~如果我們要使用錯誤頁面, 並用其來顯示錯誤訊息, 可以以下搭配

1 先在error資料夾下產生一個error.xml檔案如下

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>錯誤</title>
</head>
<body>
<h1>錯誤</h1>

<div>
<div th:utext="'&lt;!--'" th:remove="tag"></div>
<div th:utext="'Failed Request URL : ' + ${url}" th:remove="tag"></div>
<div th:utext="'Exception message : ' + ${exception.message}" th:remove="tag"></div>
<ul th:remove="tag">
<li th:each="st : ${exception.stackTrace}" th:remove="tag"><span th:utext="${st}" th:remove="tag"></span></li>
</ul>
<div th:utext="'--&gt;'" th:remove="tag"></div>
</div>

</body>
</html>

2產生一個掛@ControllerAdvice的類別

他主要用法是一個搭配ExceptionHandler攔截Exception的一個功能

下列程式碼, 基本上攔截後, 如果他有掛ResponseStatus類別的話, 他會繼續throw e

而如果沒有掛ResponseStatus就會回傳一個ModelAndView物件並導到error頁面

@ControllerAdvice
public class ControllerExceptionHandler {

private final Logger logger = LoggerFactory.getLogger(this.getClass());
@ExceptionHandler(Exception.class)
public ModelAndView exceptionHandler(HttpServletRequest request, Exception e) throws Exception {
logger.error("Request URL : {}, Exception : {}",request.getRequestURL(),e);

if(AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class)!=null){
throw e;
}

ModelAndView mv= new ModelAndView();
mv.addObject("url",request.getRequestURL());
mv.addObject("exception",e);
mv.setViewName("error/error");
return mv;
}
}


而, 如果我們想要做出對應404的Exception

1 產生一個個性化的Exception類別並掛上ResponseStatus

@ResponseStatus(HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException{
public NotFoundException() {
}

public NotFoundException(String message) {
super(message);
}

public NotFoundException(String message, Throwable cause) {
super(message, cause);
}
}


2在indexController中加入以下這段

String blog=null;
if(blog==null){
throw new NotFoundException("博客不存在");
}

因為剛剛已經在ControllerExceptionHandler中設置要繼續拋出e了

他就會跳轉到404的頁面



2021年1月22日 星期五

[SpringBoot 1.5] SpringBoot 新增範例

我們說到新增功能

他應該要包含兩個步驟

1 點進去進入到一個可以填寫空白資訊的頁面(employee>Get)

2 輸入完新增的資訊然後按下儲存鍵, 完成新增項目的儲存(employee>Post)

從畫面過來, 一開始是點擊這個標籤

<a href="emp" th:href="@{/emp}" class="btn btn-sm btn-success">員工添加</a>

然後會進到controller, 因為裡面會要將所有可以選取的Department顯示出來,

所以要先getDepartments再放到model中

@GetMapping("/emp")
public String toAddPage(Model model){
// 來到添加頁面, 查出所有的部門, 在頁面顯示
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("depts",departments);
return "emp/add";
}

這裡的Department類別

public class Department {

private Integer id;
private String departmentName;
}

這是Dao

@Repository
public class DepartmentDao {

private static Map<Integer, Department> departments = null;

static{
departments = new HashMap<Integer, Department>();

departments.put(101, new Department(101, "D-AA"));
departments.put(102, new Department(102, "D-BB"));
departments.put(103, new Department(103, "D-CC"));
departments.put(104, new Department(104, "D-DD"));
departments.put(105, new Department(105, "D-EE"));
}

public Collection<Department> getDepartments(){
return departments.values();
}

public Department getDepartment(Integer id){
return departments.get(id);
}

}

然後畫面上的form是這樣

<form th:action="@{/emp}" method="post">

<input type="hidden" name="_method" value="put" th:if="${emp != null}">
<
input type="hidden" name="id" th:value="${emp != null} ? ${emp.id}">
<
div class="form-group">
<
label>LastName</label>
<
input name="lastName" type="text" class="form-control" placeholder="zhangsan" th:value="${emp != null} ? ${emp.lastName}">
</
div>
<
div class="form-group">
<
label>Email</label>
<
input name="email" type="email" class="form-control" placeholder="zhangsan@atguigu.com" th:value="${emp != null} ? ${emp.email}">
</
div>
<
div class="form-group">
<
label>Gender</label><br/>
<
div class="form-check form-check-inline">
<
input class="form-check-input" type="radio" name="gender" value="1" th:checked="${emp != null} ? ${emp.gender == 1}">
<
label class="form-check-label">男</label>
</
div>
<
div class="form-check form-check-inline">
<
input class="form-check-input" type="radio" name="gender" value="0" th:checked="${emp != null} ? ${emp.gender == 0}">
<
label class="form-check-label">女</label>
</
div>
</
div>
<
div class="form-group">
<
label>department</label>
<
select class="form-control" name="department.id">
<option th:value="${dept.id}" th:each="dept : ${depts}" th:text="${dept.departmentName}" th:selected="${emp != null} ? ${dept.id == emp.department.id}"></option>
</
select>
</
div>
<
div class="form-group">
<
label>Birth</label>
<
input name="birth" type="text" class="form-control" placeholder="zhangsan" th:value="${emp != null} ? ${#dates.format(emp.birth, 'yyyy-MM-dd HH:mm:ss')}">
</
div>
<
button type="submit" class="btn btn-primary" th:text="${emp != null} ? '修改' : '添加'"></button>
</
form>

其中我們可以看到拿出depts塞到dept中, 他會需要將id塞到value中, 要特別注意一下













那麼他送出, 就是一個Employee的model

這樣畫面上是使用name來做對應屬性

而department則要用department.id來對應

這個form發出的是Post請求

@PostMapping("/emp")
public String addEmp(Employee employee){
System.out.println("保存的員工信息: "+employee);
employeeDao.save(employee);
return "redirect:/emps";
}

但是記得進入的時候要重新導向redirect

而以下是dao的 save功能

private static Integer initId = 1006;
public void save(Employee employee){
if(employee.getId() == null){
employee.setId(initId++);
}

employee.setDepartment(departmentDao.getDepartment(employee.getDepartment().getId()));
employees.put(employee.getId(), employee);
}

還有一個日期要做補充

通常預設日期會為20xx/xx/xx 

如果要格式化日期, 是可以在application.properties中做控制的, 如以下

spring.mvc.date-format=yyyy-mm-dd


[SpringBoot 1.5] SpringBoot 查詢範例

當我們要將員工資訊顯示出來

這就是一個查詢功能







我們的model為一個Employee類別 屬性如下

public class Employee {

private Integer id;
private String lastName;

private String email;
//1 male, 0 female
private Integer gender;
private Department department;
private Date birth;

然後我們設置一個controller中的方法

@Repository
public class EmployeeDao {
@GetMapping("/emps")
public String list(Model model){
Collection<Employee> employees = employeeDao.getAll();

model.addAttribute("emps",employees);
return "emp/list";
}

我們使用model.addAttribute來設置model中內容

當他點選到該連結, 就可以導向過來controller再return給list頁面

<a class="nav-link active"
th:class="${activeUri=='emps.html'?'nav-link active':'nav-link'}"
href="#" th:href="@{/emps}">

其中搭配著employeeDao為

@Repository
public class EmployeeDao {
private static Map<Integer, Employee> employees = null;

@Autowired
private DepartmentDao departmentDao;

static{
employees = new HashMap<Integer, Employee>();

employees.put(1001, new Employee(1001, "E-AA", "aa@163.com", 1, new Department(101, "D-AA")));
employees.put(1002, new Employee(1002, "E-BB", "bb@163.com", 1, new Department(102, "D-BB")));
employees.put(1003, new Employee(1003, "E-CC", "cc@163.com", 0, new Department(103, "D-CC")));
employees.put(1004, new Employee(1004, "E-DD", "dd@163.com", 0, new Department(104, "D-DD")));
employees.put(1005, new Employee(1005, "E-EE", "ee@163.com", 1, new Department(105, "D-EE")));
}

private static Integer initId = 1006;


public Collection<Employee> getAll(){
return employees.values();
}
}

而前端的頁面為

<tr th:each="emp:${emps}">
<td th:text="${emp.id}"></td>
<td>[[${emp.lastName}]]</td>
<td th:text="${emp.email}"></td>
<td th:text="${emp.gender} == 0 ? '女' : '男'"></td>
<td th:text="${emp.department.departmentName}"></td>
<td th:text="${#dates.format(emp.birth, 'yyyy-MM-dd HH:mm:ss')}"></td>
<td>
<!--<a th:href="@{/emp/} + ${emp.id}" class="btn btn-sm btn-primary">编辑</a>-->
<a th:href="@{/emp/} + ${emp.id}" class="btn btn-sm btn-primary">编辑</a>
<button th:attr="del_uri = @{/emp/} + ${emp.id}" type="submit" class="btn btn-sm btn-danger deleteBtn">删除</button>
</td>
</tr>

為一個使用th:each來遍歷model中emps的每一筆資料, 時間格是可以用dates.format

查出每筆資料有兩種寫法, 一種為th:text="${model.attribute}"

另外一種為[[${model.attribute}]]


2026半年度目標

 1 每個月看看能不能都做專案, 一年12專案 2 刷題每周4題 3 跑個人scrum, 並組讀書會 4規律讀英文ex每天早上8:30-9:30 5規律運動7:35-8:30 6減肥97->85 這是今年年初的目標, 有刷一點題, 但是差不多三月初AI大爆發, 不得不開始接...